Xie语言(中文名称为:谢语言,官网 xie.topget.org)是一门开源、免费的解释型编程语言(也叫脚本语言),最大的特色包括:跨平台;跨语言(目前仅支持Go语言)可嵌入;结合汇编语言和高级语言的优点;支持全中文编程(包括提示信息),语法简单易懂;单文件无依赖等。
谢语言支持各种基本的语法元素和结构,包括变量、条件分支、循环、函数、递归函数调用、多线程等,支持作为嵌入型语言在不同语言中调用,也支持单独运行,还支持作为后台微服务运行。同时,谢语言也提供一个命令行交互式编程环境。
谢语言特点比较鲜明:
下面是谢语言常见的欢迎程序代码:
输出行 `欢迎来到谢语言的世界!`
命令行上用下面的命令执行后可得结果如下:
D:\tmp>xie 欢迎.谢
欢迎来到谢语言的世界!
D:\tmp>
下面是常见用于性能测试的斐波那契数列生成代码(斐波那契.谢),使用了递归函数调用:
入栈 整数 35
调用 :斐波那契
输出行 $出栈
终止
:斐波那契
声明变量 $数字1
出栈 $数字1
整数小于 $数字1 2
是则 $出栈 :标号1
:否则
减一 $数字1
入栈 整数 $数字1
调用 :斐波那契
减一 $数字1
入栈 整数 $数字1
调用 :斐波那契
整数加
返回
:标号1
入栈 $数字1
返回
将计算出斐波那契数列第35位数字,如下所示:
D:\tmp>xie 斐波那契.谢
9227465
D:\tmp>
谢语言的出现,最初是因为希望有一个能够嵌入在各种语言(初期考虑的语言主要是Go、Java、JavaScript、C/C++、C#、Switch等)内的轻量级脚本语言,能够支持在后端微服务中热加载修改的代码,要求语言的语法简单而又速度相对较快,但是能够充分发挥宿主语言的丰富库函数优势。后来逐渐发现谢语言也具备可以成为一门全栈语言的潜力,希望它最终能够达到。另外,考虑到目前还没有一个开源免费并支持全栈编程的全中文编程语言,也希望谢语言能够作为填补空缺的一员。(* 多说一句,编程中完全不用到英文是不太可能的,希望尽量减少英文不好开发者的难度吧。为满足喜欢输入方便的开发者,谢语言也同时支持英语编程。)
借鉴汇编语言的思路,谢语言引入了寄存器和堆栈等概念,也因此在某些功能的实现上会比一般的高级语言显得复杂一些,但从速度上(包括语法解析的速度)考虑,还是值得的。但要求开发者对堆栈和寄存器做一些简单的了解。
设计的原则包括:
谢语言还在积极开发中,欢迎提出各种建议。
xie hello.xie
xie -example hello.xie
如果看到类似下面的输出,说明安装成功,并且开发环境准备就绪。
D:\tmpx>xie -example hello.xie
Hello world!
D:\tmpx>
C:\Users\Administrator# xie
> version
> pln $pop
0.0.1
>
交互式命令行程序可以用于快速测试一些语句,或进行简单的编程获取结果。
D:\tmpx>xie -version
谢语言 版本0.0.1
D:\tmpx>
push int 35
call :fib
pln $pop
exit
:fib
var $n1
pop $n1
<i $n1 2
if $pop :label1
:else
dec $n1
push int $n1
call :fib
dec $n1
push int $n1
call :fib
intAdd
ret
:label1
push $n1
ret
本例子代码是fib.xie
谢语言提供各种例子代码,可以在命令行中加上-example参数直接运行,例如上述斐波那契数列代码中文版就可以直接用下面的命令行运行:
xie -example 斐波那契.谢
运行后结果类似:
D:\tmpx>xie -example 斐波那契.谢
9227465
D:\tmpx>
如果需要查看例子代码,可以再加上-view参数,就可以看到:
D:\tmpx>xie -example -view 斐波那契.谢
入栈 整数 35
调用 :斐波那契
输出行 $出栈
……此处省略中间部分
:标号1
入栈 $数字1
返回
D:\tmpx>
当然,也可以用“>”等转向符将其输出到其他文件中:
xie -example -view 斐波那契.谢 > d:\test\new.xie
因此,如果我们后面说道:“请参看例子代码test1.xie”,那么就意味着可以通过
xie -example -view test1.xie
这样的命令行来参看所述的例子代码。
assign $a "abc"
其中的assign是指令名,后面的$a和"abc"都是指令参数。本条指令将把字符串"abc"赋值给变量a(注意不包括引号)。
assign $a `abc
123`
这将把变量a赋值为多行字符串"abc\n123";
谢语言中仅支持行注释,可以用“//”或“#”来引导注释,支持“#”是为了在文本编辑器中选用“Shell脚本”的语法高亮方案时,可以使用Ctrl+/组合键来切换改行是否注释。
谢语言使用var命令(中文命令为“声明变量”)进行变量声明:
var $a
这将定义一个名字为a的变量,谢语言中,变量名前都要加“$”字符以示区别。定义变量时可以加第二个参数指定变量类型,此时其中的值为该类型的空值;不指定类型时,变量默认为nil(无类型的空值)如下所示:
C:\Users\Administrator# xie
> var $a
> pln $a
<nil>
>
注意,pln命令类似于一般语言中的println函数,会将后面的变量和数值一个个输出到命令行,末尾再输出一个换行符。可以看到变量a中的值确实是nil。
如果变量未定义就使用,会显示“未定义”字样,例如,下面的代码:
pln "a:" $a
var $a
pln "a:" $a
运行后会显示如下结果:
a: 未定义
a: <nil>
因为第一次输出时,变量a尚未被定义。
C:\Users\Administrator# xie
> var $a float
> plo $a
(float64)0
>
可以看出,变量a被定义为float64即64位浮点数,并初始化为值0。
谢语言中的变量的类型可以任意改变,意味着谢语言是一门“弱类型”的语言,而不像Go、C、Java等“强类型”的语言那样,变量一旦声明后只能改变数值而不能改变类型。
谢语言中给变量赋值用的是assign/=/赋值(即选用assign、=或中文“赋值”都可以表示这条命令,后面都会用类似的写法)命令:
assign $a 123
这条命令将把变量a赋值为123,注意,这是字符串“123”,而不是数字123,因为谢语言中默认数值都是字符串类型。
如果想把变量a赋值为一个整数,可以选用下面两种方法之一:
assign $a #i123
assign $a int 123
第一种方法是谢语言中对数值指定类型的方法,在数值前加上“#”号开头带一个指定的英语字母,可以限定数值的类型,对于基本类型,“#i”表示整数,“#f”表示浮点数,“#b”表示布尔数值(后跟true或false),“#s”表示字符串,“#y”表示字节,“#r”表示如痕。
第二种方法是在数值前再加一个指定数据类型的参数,可以是“int”、“float”、“bool”、“str”、“byte”、“rune”等,分别对应中文的“整数”、“小数”、“布尔”、“字符串”、“字节”、“如痕”。
看下下面的输出,可以看到两种方法得到的结果是一样的:
D:\tmpx# xie
> assign $a #i123
> plo $a
(int)123
> assign $a int 123
> plo $a
(int)123
> 赋值 $变量1 整数 123
> 输出值类型 $变量1
(int)123
>
由于谢语言使用空格作为命令与参数之间的分隔符,因此带有空格的字符串必须做特殊处理,使用双引号或反引号括起来(不含空格的字符串可以不括起来直接使用),反引号还可以括起多行字符串(含有换行符“\n”的字符串),双引号中可以带有\n、\t等转义字符,反引号中则不进行转义。另外,由于使用了反引号,谢语言代码中不应出现其他用途的反引号,如果遇上确需使用的地方,需要用全局变量backQuoteG或者转义字符“\u0096”来代替。
assign $s abc
plo $s
assign $s "abc 123"
plo $s
assign $s `abc 123
and this`
plo $s
assign $b int 3
plo $b
assign $b #i3
plo $b
assign $b #f3
plo $b
本段例子代码(assign.xie)执行后的结果是:
(string)abc
(string)abc 123
(string)abc 123
and this
(int)3
(int)3
(float64)3
堆栈是各种语言都会用到的数据结构,当然除了汇编语言外,一般都是“暗中”使用。但谢语言中将堆栈放开了使用,这有利于程序的性能,以及开发者灵活地操控。当然,对于对编程底层不是很了解的开发者来说,需要有一个适应的过程,容易犯错导致程序运行出乎意料。但熟悉之后,会发现这是一个很有力、很高效的编程基础设施。
堆栈实质上是一个“后进先出”的队列,我们一般将其形象地想象为一个竖立的箱子,一般的操作包括“入栈”(英语为push,将一个数值压入堆栈,即放在堆栈的顶部)、“出栈”(英语为pop,将一个数值弹出堆栈,即从堆栈顶部取出一个数值)和“看栈”(英语为peek,即取到堆栈顶部的第一个数值,但并不做出栈操作,并不改变堆栈内容)。
形象化地,我们有时候将入栈操作也称为“压入堆栈”,将出栈操作称为“弹出堆栈”、“弹出栈顶数值”,将看栈操作称为“查看栈顶”等。如果后面说道“弹栈值”,是指做了出栈(弹栈)操作后得到的值。另外,堆栈内的数值有可能被称为“元素”。
堆栈在各种数值转移、计算、函数调用等场景中都发挥着重要的作用,谢语言中将其放在了明面上,给开发者提供一种高效的工具。
下面的代码(stack.xie)演示了堆栈的各种基本操作,代码中也写有详细的注释说明每条语句的作用,我们后面将大量使用这种方式来做代码示例和语法与指令讲解:
// 将整数2压入堆栈
push #i2
// 弹出栈顶数值到变量a中
pop $a
// 输出变量a的内容
plo $a
// 将整数3压入堆栈
push #i3
// 将小数2.7压入堆栈,此时栈内从下而上包含两个元素:整数的3和浮点数2.8
push #f2.8
// 查看栈顶元素并将其赋值给变量b
peek $b
// 输出变量b的内容
plo $b
// 弹出栈顶元素到变量c中
pop $c
// 输出变量b的内容
plo $c
// 中文的堆栈操作
// 压入一个布尔数值true
入栈 #btrue
看栈 $变量1
输出值类型 $变量1
出栈 $变量2
输出值类型 $变量2
运行这段代码将输出:
(int)2
(float64)2.8
(float64)2.8
(bool)true
(bool)true
可以根据代码中的注释,详细观察堆栈操作的结果与预期的是否一致。
谢语言中,有几个与堆栈操作有关特殊变量,属于系统预定义的变量,可以随时使用以便于一些灵活的数值操作。它们是“$push”、“$pop”和“$peek”,分别表示入栈值、出栈值、看栈值。下面是它们的使用例子(stackVar.xie):
// 将字符串压入堆栈
push "我们高兴!"
// 弹出栈顶数值,并输出
// 注意弹出的数值如果不赋值给变量将丢失
plo $pop
// 将整数18入栈
push #i18
// 将出栈的数值赋值给变量a
assign $a $pop
// 输出变量a
plo $a
// 将浮点数3.14入栈
push #f3.14
// 将栈顶值赋值给变量a
// 此时堆栈内该数值仍将继续存在
assign $a $peek
// 再次输出变量a
plo $a
// 用assign语句将整数18入栈
// $push变量表示将后面的数值压栈
assign $push #i3
// 输出栈顶元素
plo $peek
本段代码运行的结果是:
(string)我们高兴!
(int)18
(float64)3.14
(int)3
先看看这个加法的例子(add.xie)
// 将整数2入栈
push #i2
// 将整数5入栈
push #i5
// 将栈顶两个数值取出相加后结果压入栈中
// 此时栈中应仅有一个数值5
add
// 输出栈顶数值(同时该数值被弹出)
plo $pop
// 将浮点数1.5与2.6相加后压栈
add #f1.5 #f2.6
// 弹栈输出
plo $pop
// 将两个字符串相加(连接)后赋值给变量c
add $c `abc` `123 456`
// 输出变量c
plo $c
// 将变量c中的数值压栈
push $c
// 将字符串“9.18”压栈
push "9.18"
// 将栈顶两个字符串相加后赋值给变量d
add $d
// 输出变量d
plo $d
// 将整数18与190相加后,压入栈中
// 此处使用了预定义全局变量$push
// 表示入栈操作
add $push #i18 #i190
// 弹栈输出
plo $pop
谢语言中,加法运算指令是add/+/加,如果不带任何参数,add指令将把堆栈顶端的两个数值取出来后相加,然后结果再压入堆栈。如果带有两个参数,则将这两个参数(可以是变量)值进行相加操作后将结果入栈。如果带有1个或3个参数,则第一个参数是将要放置结果的变量,结果将放于这个变量中。这段代码的运行结果是:
(int)7
(float64)4.1
(string)abc123 456
(string)abc123 4569.18
(int)208
其他类似的运算指令还有sub/-/减、mul/*/乘、div///除、mod/%/取模等,用法类似。这些都属于二元运算指令,即参与运算的数值是两个。二元运算的两个数值必须是同一类型的。如果是不同类型,例如整数和浮点数相加,则需要进行类型转换。
谢语言中,使用convert指令来转换数值类型,至少需要两个参数,第一个参数是数值或变量,第二个参数是字符串,指定需要转换成为的数据类型,如果有参数有三个,那么第一个参数必须是一个变量,convert指令将会把转换后的结果存入该变量,否则会压栈。convert指令的使用示例(convert.xie)如下:
// 将整数15赋值给变量a
assign $a #i15
// 此时如果执行指令 add $a #f3.6
// 将会出现运行时错误
// 应为加法运算的两个数值类型不一致
// 一个是整数,一个是浮点数
// 输出变两个的数据类型和数值进行查看
// pl指令相当于其他语言中的printf函数,后面再多输出一个换行符\n
pl `a(%T)=%v` $a $a
// 将变量a转换为浮点数类型
// 结果将压入栈中
convert $a float
// 输出栈顶值(不弹栈)的类型和数值查看
pl `a(%T)=%v` $peek $peek
// 将栈顶值与浮点数3.6相加后压栈
add $pop #f3.6
// 输出栈顶值查看类型和结果
// 注意第一个参数使用$peek是为了不弹栈
// 以保证第二个参数$pop操作时还能取到该值
pl "result=(%T)%v" $peek $pop
代码中解释很详细,运行结果如下:
a(int)=15
a(float64)=15
result=(float64)18.6
谢语言中大多数指令会产生一个或多个结果值(类似于其他语言中的函数会有返回值),谢语言中指令的返回值多数情况下是一个(函数的返回值视情况会有0个、1个或多个)。
因此,很多指令需要的一个用于指定接收指令执行结果的参数,我们将其称作结果参数。结果参数一般都是一个变量,因此也称作结果变量。结果变量可以是$push(表示将结果压入堆栈中)、$drop(表示将结果丢弃)等预置全局变量。结果变量一般可以省略,此时表示将结果压入堆栈中(等同于$push)。但当指令的参数个数可变时,结果参数不可省略,以免产生混淆。
例如toUpper指令被用于将字符串转换为大写,toUpper "abc" 会将大写的ABC压栈, 而 toUpper $result "abc" 则会将ABC赋值给变量result。
另外,如果指令应返回结果,则文档中当不提结果参数时,“第一个参数”一般指的是除结果参数外的第一个参数,余者类推。
对于带有可选个数参数的指令,则一般第一个参数必须是结果变量,不可省略,这样最后才可以接可选的n个参数,否则容易产生混淆。例如getWeb指令,一个典型用法(参看httpClient.xie)是:
getWeb $resultT "http://127.0.0.1:80/xms/xmsApi" -method=POST -encoding=UTF-8 -timeout=15 -headers=`{"Content-Type": "application/json"}` $mapT
因为后面的参数除了URL是必须的外,其他都是可选的,不能确定有几个参数,因此只能把结果变量放在第一个,以便存放获取到的HTTP响应的内容。
上例中用到的pl指令,类似于一般语言中的printf函数,可以用占位符来控制输出的字符串内容。参数中第一个是格式字符串,可以含有%d、%f、%s、%v等占位符表示不同的数值输出形式,具体请参考Go语言等的参考文档。pln、plo、pl等指令在调试中经常会使用到,需要熟悉。
我们前面已经接触到了一些谢语言中常用的内置全局变量,例如$push,$pop,$peek等,这里再列出所有的全局变量作为参考。
谢语言中,由于采用接近汇编语言的快捷语法,因此在一般计算上或许要稍微复杂一些,一般的建议多步运算表达式采用分解后逐个进行的方式。例如,一个3+(9*1.5)/1.7的算式,需要用下面的代码(expression.xie)实现:
// 计算3+(9*1.5)/1.7
// 将浮点数9压栈
push #f9
// 将浮点数1.5压栈
push #f1.5
// 将栈顶两元素弹出相乘后结果入栈
mul
// 将栈顶元素和浮点数1.7相除后入栈
div $pop #f1.7
// 将浮点数3和栈顶数相加后入栈
add #f3 $pop
// 输出结果
pl "3+(9*1.5)/1.7=%v" $pop
运行结果如下:
3+(9*1.5)/1.7=10.941176470588236
可以看出,分解表达式的方法代码量比一般的高级语言多一些,但带来的好处是速度更快,因为省去了各种解析表达式的开销。
谢语言中,也可以进行复杂的表达式计算,这要用到eval指令,参看下面的代码(eval.xie):
// 给变量a复制整数12
assign $a #i12
// 计算表达式 a+(a+12+26) 的值,结果压栈
// 注意,表达式元素之间必须以空格分隔开
eval `$a + ( $a + #i12 + #i26 )`
// 输出弹栈值查看
pln $pop
// 将变量b复制为整数-9
assign $b #i-9
// 计算顺序括号优先,无括号时严格按照从左到右(注意不是乘除比加减优先等)
// 结果值放入变量r
// 表达式的数学表达是 a+((a-12.0)*abs(b)),其中abs表示取绝对值
// 注意由于计算顺序问题,数学表达中需要把a-12.0加上括号以保证计算顺序一致
eval $r `$a + ( $a - (?convert #f12.0 int) * (? abs $b) )`
// 输出变量r的值查看
pln $r
// 判断表达式 !((a-b)<10) 的计算结果值是否为布尔值true,是则跳转到标号next1处
// ifEval指令后第一个参数必须是一个字符串类型的数值或变量,表示要计算的表达式
// 第二个参数时满足条件后要跳转到的标号
ifEval `! (($a - $b) < #i10)` :next1
pln 条件不满足
exit
:next1
pln 条件满足
需要特别注意的是,谢语言中的表达式中,运算符是没有优先级之分的,因此一个表达式中是严格按照从左到右的顺序执行运算的,唯一的例外是括号,用圆括号可以改变运算的优先级,括号里的部分将被优先计算。
另外,如果括号里的内容以一个问号“?”开始,那么后面可以是一条指令,该指令必须通过堆栈返回一个结果值,这样可以是的表达式中实现基本运算符之外的运算功能,例如转换数值类型等。
ifEval指令是专门配合表达式计算使用的条件跳转指令,它后面必须跟一个字符串类型的表达式,其计算结果必须是一个布尔类型的值,ifEval指令将根据其结果,确定是否要跳转到指定的行号。ifEval指令,简化了一般的if和ifNot质量较为复杂的条件处理语法结构。
由于谢语言中表达式计算相对效率较低,因此对于需要反复高速计算或处理的场景,建议还是使用分解的方式更高效。
运行后的效果:
62
12
条件满足
谢语言中,表达式可以运用在指令的参数中,此时需要以英文问号“?”字符开头,例如(exprInParam.xie):
assign $a "abc"
pl "[%v] test params: %v" ?(?nowStr) $a
将输出:
[2022-05-17 14:30:59] test params: abc
其中,pl指令的第二个参数即是以问号开头的表达式,而这个表达式用(?...)的方式又运行了获取当前时间字符串的指令nowStr。注意,表达式内的指令,一定要保证将结果值压栈(一般都是一个值)。
谢语言中,可以在任意代码行的前一行添加标号,主要用于各种循环和条件分支等跳转场景。设置标号必须单独占一行,并以冒号“:”字符开头。
:lable1
pln 123
谢语言中,每行代码的头尾空白将被忽略,因此可以适当采用代码的逐级缩进来增加代码的可读性。
:lable1
pln 123
goto语句在一般的高级语言中并不推荐使用,但对于具备一定经验的开发者来说,反而有可能是提高效率的手段。谢语言中提供了goto/转到指令(为保持和汇编语言的一定关联,也可写作jmp),可以用于实现代码执行中无条件跳转到某个标号处的功能。例如(goto.xie):
pln start...
push #f1.8
goto :label1
pop $c
pln `c =` $c
:label1
pln "label1 =" $peek
由于无条件跳转的关系,这段代码中的goto语句与标号:lable1之间的代码将不被执行,最后输出结果是:
start...
label1 = 1.8
循环结构是一般语言中必然会有的基本语法结构。谢语言中,一般使用各种跳转语句来实现循环结构。goto语句是其中的一种方法,最常见的是实现无限循环。
// 将字符串压栈
push "欢迎"
// 设定标号loop1
:loop1
// 输出栈顶值
pln $peek
// 休眠2.5秒
sleep #f2.5
// 跳转到标号loop1处继续往下执行
goto :loop1
sleep指令的作用是休眠指定的秒数。本例(for1.xie)运行的结果是将每隔2.5秒输出一下“欢迎”两个字,直到按Ctrl-C等方法来终止程序的运行。
谢语言中的条件分支支持一般是由比较判断指令和条件跳转指令结合来实现的。直接看下面的例子(if.xie):
// 给变量i赋值整数11
assign $i #i11
// 比较变量i是否大于整数10
// 结果放入变量a中
> $a $i #i10
// 判断$a是否为布尔值true
// 如果是则跳转到标号label2
if $a :label2
// 否则执行下面的语句
pln "else branch"
//终止程序执行
exit
// 标号label2
:label2
// 输出“if branch”
pln "if branch"
// 将变量b赋值为整数8
assign $b #i8
// 比较变量b是否小于或等于变量i
<= $b $i
// 是则跳转到标号label3
ifNot :label3
// 否则输出
pln "label3 else"
// 终止代码执行
exit
// 标号label3
:label3
// 输出“label3 if”
pln "label3 if"
其中,出现了两个比较指令:“>”和“<=”,这些比较指令所带参数都和二元运算指令类似,可以从堆栈中取两个值做比较,也可以对后面所带的两个参数进行比较,当然还可以带一个参数(放在第一个)表示将结果赋值给某个变量,否则会将结果压栈。比较指令返回的结果都是布尔值true或者false。
而条件跳转指令if和ifNot可以带1或2个参数,最后一个参数都是符合条件要跳转到的标号,如果还有第一个参数则表明要判断的变量或数值(必须是布尔值),没有的话则从堆栈取数进行判断:if指令是true则跳转,ifNot是false则跳转。
这段代码的运行结果是:
if branch
label3 else
注意观察条件分支的流转是否符合预期。
比较指令主要包括:==(等于)、!=(不等于)、>、<、>=、<=等。
有了条件分支结构,就可以实现标准的for循环,这是一个可以具有终止条件的循环语法结构。
// 实现类似 for i = 0; i < 5; i ++ 的标准三段for循环结构
// 将变量i赋值为整数0
assign $i #i0
// 标号loop1
:loop1
// 将i的值加上整数10
// 结果压栈
add $i #i10
// 输出变量i中数值,和栈顶弹出值
pln $i ":" $pop
// 将变量i的值加1
inc $i
// 判断变量i中的数值是否小于整数5
< $i #i5
// 是则跳转到标号loop1(继续循环)
if $pop :loop1
// 否则执行下面的语句
// 也就是跳出了loop1的循环结构
// 输出字符串“end”
pln end
上面的例子代码(for.xie)实现了一个经典的三段for循环结构。其中用到了inc指令,作用是将变量值加1,如果不带参数则会弹栈值加1,结果都将压栈。inc指令实现了一般语言中 i++ 的效果。本段代码执行的结果是:
0 : 10
1 : 11
2 : 12
3 : 13
4 : 14
end
与inc指令对应的是dec指令,会将对应值减1。
谢语言中的函数调用分为快速函数调用、一般函数调用和封装函数调用,先介绍一般函数调用,一般函数调用的标准结构如下(func.xie):
// 将变量s赋值为一个多行字符串
assign $s ` ab c123 天然
森林 `
// 输出变量s中的值
// plv会用内部表达形式输出后面变量中的值
// 例如会将其中的换行符等转义
plv $s
// 将变量s中的值压栈
push $s
// 调用函数func1
// 即跳转到标号func1处
// 而ret命令将返回到call语句的下一行有效代码处
call :func1
// 弹栈到变量s中
pop $s
// 再次输出变量s中的值
plv $s
// 终止代码执行
exit
// 标号func1
// 也是函数的入口
// 一般称作函数func1
:func1
// 弹栈到变量v中
pop $v
// 将变量v中字符串做trim操作
// 即去掉首尾的空白字符
// 结果压入栈中
trim $v
// 函数返回
// 从相应call指令的下一条指令开始继续执行
ret
上面代码中,plv指令会输出后面值的内部形式,主要为了调试时便于看出其中值的类型。call标号加ret指令是谢语言实现函数的基本方法,call语句将保存当前程序所处的代码位置,然后调用指定标号处的代码,直至ret语句时将返回到call时代码位置的下一条指令继续执行。这就实现了一个基本函数调用的逻辑。
如果要给函数传递参数,则一般通过堆栈来进行。同样地,函数返回值也通过堆栈来传递。trim指令实际上是对后面的变量进行去字符串首尾空白的操作,结果将会压栈,除非带有第二个参数(注意,trim指令如果不带参数,则会将栈顶元素弹出后处理,之后再将结果入栈;带有一个参数时则是将其处理后入栈;两个参数时才会将结果放入第一个参数所指的结果变量中)。
一般函数中会具有自己的局部变量空间,在函数中定义的变量(使用var指令),只能在函数内部使用,函数返回后将不复存在。而对变量值取值使用的情况,函数会先从局部变量寻找,如果有则使用之,如果没有该名字的变量则会到上一级函数(如果有的话,因为函数可以层层嵌套)中寻找,直至寻找到全局变量为止仍未找到才会返回“未定义”。对变量进行赋值操作的情况(对变量),如果在进入函数前没有定义过,则也会层层向上寻找,如果全没有找到,则会在本函数的空间内创建一个新的局部变量。如果要在函数中创建全局变量,则需要使用global指令。global指令与var指令用法一致,唯一的区别就是global指令将声明一个全局变量。看下面的例子(local.xie)来了解全局变量和局部变量的使用:
// 给全局变量a和b赋值为浮点数
assign $a #f1.6
assign $b #f2.8
// 调用函数func1
call :func1
// 输出调用函数后a、b、c、d四个变量的值
pln $a $b $c $d
// 退出程序执行
exit
// 函数func1
:func1
// 输出进入函数时a、b、c、d四个变量的值
pln $a $b $c $d
// 将变量a与0.9相加后将结果再放入变量a中
add $a $a #f0.9
// 声明一个局部变量b(与全局变量b是两个变量)
var $b
// 给局部变量b赋值为整数9
assign $b #i9
// 将局部变量b中的值加1
inc $b
// 将变量c赋值为字符串
= $c `abc`
// 声明一个全局变量d
global $d
// 给变量d赋值为布尔值true
= $d #btrue
// 退出函数时输出a、b、c、d四个变量的值
pln $a $b $c $d
ret
注意其中的“=”是assign指令的另一种简便写法,另外assign指令前如果没有用global或var指令生命变量,相当于先用var命令声明一个变量然后给其赋值。这段代码的运行结果是:
1.6 2.8 未定义 未定义
2.5 10 abc true
2.5 2.8 未定义 true
注意其中4个变量a、b、c、d的区别,可以看出:变量a是在主代码中定义的全局变量,在函数func1中对其进行了计算(将a与0.9相加后的结果又放入a中)后,最后出了函数体之后的输出仍然是计算后的值,说明函数中操作的是全局变量;变量b则是在函数中定义了一个同名的局部变量,因此在函数中虽然有所变化,但退出函数后其值会变回原来的值,其实是局部变量b已经被销毁,此时的b是全局变量b;变量c完全是函数内的局部变量,因此入函数前和出了函数后都是“未定义”;变量c则是在函数中用global指令新建的全局变量,因此退出函数后任然有效。
快速函数与一般函数的区别是:快速函数不会有自己的独立变量空间。快速函数与主函数(指不属于任何函数的代码所处的环境)共享同一个变量空间,在其中定义和使用的变量都将是全局变量。使用快速函数的好处是,速度比一般函数更快,因为减少了分配函数局部空间的开销。对一些实现简单功能的函数来说,有时候这是很好的选择。
快速函数类似call与ret的配对指令,使用fastCall与fastRet两个指令来控制函数调用与返回。下面是例子(fastCall.xie):
// 将两个整数压栈
push #i108
push #i16
// 快速调用函数func1
// 而fastRet命令将返回到fastCall语句的下一行有效代码处
fastCall :func1
// 输出弹栈值(为函数func1压栈的返回结果)
plv $pop
// 终止代码执行
exit
// 函数func1
// 功能是将两个数相加
:func1
// 弹栈两个数值
pop $v2
pop $v1
// 将两个数值相加后压栈
add $v1 $v2
// 函数返回
// 从相应fastCall指令的下一条指令开始继续执行
fastRet
这里的“引用”可以理解成一般语言中的取变量地址的操作。使用引用的目的是为了直接修改其中的值,尤其是对一些复杂数据类型来说。这里先给出一个对基础数据类型的取引用与解引用操作的例子(ref.xie):
// 给全局变量a和b赋值为浮点数
assign $a #f16
// 获取变量a的引用并入栈
ref $a
// 调用函数func1
call :func1
// 输出调用函数func1后的变量a值
plo $a
// 退出程序执行
exit
// 函数func1
:func1
// 出栈到变量p
pop $p
// 输出变量p
plo $p
// 将引用变量p中的对应的数值放入变量v中
unref $v $p
// 输出变量v
plo $v
// 将引用变量p中的值重新置为整数9
assignRef $p #i9
// 函数返回
ret
代码中有详细的注释,运行结果为:
(*interface {})0xc00014e150
(float64)16
(int)9
其中,ref指令用于取变量的引用,unref指令用于获取引用变量指向的值(解引用),assignRef指令则直接将引用变量指向的值赋以新值。可以看出,使用变量引用,成功将全局变量中的数值进行了改变。
列表在其他语言中有时候也称作“数组”、“切片”等。在谢语言中,列表可以理解为可变长的数组,其中可以存放任意类型的值。列表的操作包括创建、增加项、删除项、切片(截取其中一部分)、合并(与其他列表合并)、遍历(逐个对列表中所有的数据项进行操作)等。下面的代码演示了这些操作的方法(list.xie):
// 定义一个列表变量list1
var $list1 list
// 查看列表对象,此时应为空的列表
plo $list1
// 给列表list1中添加一项整数8
addItem $list1 #i8
// 给列表list1中添加一项浮点数12.7
addItem $list1 #f12.7
// 再次查看列表list1中内容,此时应有两项
plo $list1
// 用赋值的方法直接将一个数组赋值给列表变量list2
// #号后带大写的L表示后接JSON格式表达的数组
assign $list2 #L`["abc", 2, 1.3, true]`
// 输出list2进行查看
plo $list2
// 查看list2的长度(即其中元素的个数)
len $list2
pln length= $pop
// 获取列表list1中序号为0的项(列表序号从零开始,即第1项)
// 结果将入栈
getItem $list1 #i0
// 获取list2中的序号为1的项,结果放入变量a中
getItem $a $list2 #i1
// 将变量a转换为整数(原来是浮点数)并存回a中
convert $a $a int
// 查看变量a中的值
plo $a
// 将弹栈值(此时栈顶值是列表list1中序号为0的项)与变量a相加
// 结果压栈
add $pop $a
// 查看弹栈值
plo $pop
// 将列表list1与列表list2进行合并
// 结果放入新的列表变量list3中
// 注意,如果没有指定结果参数(省略第一个,此时应共有2个参数),将把结果存回list1
// 相当于把list1加上了list2中所有的项
addItems $list3 $list1 $list2
// 查看列表list3的内容
plo $list3
// 将list3进行切片,截取序号1(包含)至序号5(不包含)之间的项
// 形成一个新的列表,放入变量list4中
slice $list4 $list3 #i1 #i5
// 查看列表list3的内容
plo $list4
// 循环遍历列表list4中所有的项,对其调用标号range1开始的代码块
// 该代码块必须使用continue指令继续循环遍历
// 或者break指令跳出循环遍历
// 遍历完毕或者break跳出遍历后,代码将继续从rangeList指令的下一条指令继续执行
// 遍历每项时,rangeList会先将当前遍历项和当前序号值(从0开始)先后压栈
rangeList $list4 :range1
// 删除list4中序号为2的项(此时该项为整数2)
deleteItem $list4 #i2
// 再次删除list4中序号为2的项(此时该项为浮点数1.3)
deleteItem $list4 #i2
// 修改list4中序号为1的项为字符串“烙红尘”
setItem $list4 #i1 烙红尘
// 再次删除list4中序号为0的项(此时该项为浮点数12.7)
deleteItem $list4 #i0
// 再次查看列表list4的内容
// 此时应只剩1项字符串“烙红尘”
plo $list4
// 结束程序的运行
exit
// 标号range1的代码段,用于遍历列表list4
:range1
// 弹栈获得遍历序号值放入变量i中
pop $i
// 弹栈获得遍历项放入变量v中
pop $v
// 判断i值是否小于3,结果压栈
< $i #i3
// 如果是则跳转到next1(继续执行遍历代码)
if :next1
// 否则跳出循环遍历
break
// 标号next1
:next1
// 输出提示信息
pl `第%v项是%v` $i $v
// 继续循环遍历,如欲跳出循环遍历,可以使用break指令
continue
代码中有详细注释,运行的结果是:
([]interface {})[]
([]interface {})[8 12.7]
([]interface {})[abc 2 1.3 true]
length= 4
(int)2
(int)10
([]interface {})[8 12.7 abc 2 1.3 true]
([]interface {})[12.7 abc 2 1.3]
第0项是12.7
第1项是abc
第2项是2
([]interface {})[烙红尘]
谢语言还有其他类型的列表,包括字节列表(byteList)和如痕列表(runeList)等,用法类似。
映射在其他语言中也称作字典、哈希表等,其中存储的是一对对“键(key)”与“值(value)”,也称为键值对(key-value pair)。谢语言中运用映射各种基本操作的例子如下(map.xie):
// 定义一个映射变量map1
var $map1 map
// 查看映射对象,此时应为空的映射
plo $map1
// 给映射map1中添加一个键值对 “"Name": "李白"”
// setItem也可用于修改
setMapItem $map1 Name "李白"
// 再给映射map1中添加一个键值对 “"Age": 23”
// 此处23为整数
setMapItem $map1 Age #i23
// 再次查看映射map1中内容,此时应有两个键值对
plo $map1
// 用赋值的方法直接将一个数组赋值给映射变量map2
// #号后带大写的M表示后接JSON格式表达的映射
assign $map2 #M`{"日期": "2022年4月23日","气温": 23.3, "空气质量": "良"}`
// 输出map2进行查看
plo $map2
// 查看map2的长度(即其中元素的个数)
len $map2
pln length= $pop
// 获取映射map1中键名为“Name”的项
// 结果将入栈
getMapItem $map1 Name
// 获取map2中的键名为“空气质量”的项,结果放入变量a中
getMapItem $a $map2 空气质量
// 将弹栈值(此时栈顶值是映射map1中键名为“Name”的项)与变量a相加
// 结果压栈
add $pop $a
// 查看弹栈值
plo $pop
// 循环遍映射map2中所有的项,对其调用标号range1开始的代码块
// 该代码块必须使用continue指令继续循环遍历
// 或者break指令跳出循环遍历
// 遍历完毕或者break跳出遍历后,代码将继续从rangeMap指令的下一条指令继续执行
// 遍历每项时,rangeMap会先将当前键值和当前键名先后压栈
rangeMap $map2 :range1
// 删除map2中键名为“气温”的项(此时该项为浮点数23.3)
deleteMapItem $map2 "气温"
// 再次查看映射map2的内容
plo $map2
// 结束程序的运行
exit
// 标号range1的代码段,用于遍历映射
:range1
// 弹栈获得遍历序号值放入变量i中
pop $k
// 弹栈获得遍历项放入变量v中
pop $v
// 输出提示信息
pl `键名为 %v 项的键值是 %v` $k $v
// 继续循环遍历,如欲跳出循环遍历,可以使用break指令
continue
其中详细介绍了映射类型的主要操作,代码的运行结果是:
(map[string]interface {})map[]
(map[string]interface {})map[Age:23 Name:李白]
(map[string]interface {})map[日期:2022年4月23日 气温:23.3 空气质量:良]
length= 3
(string)李白良
键名为 日期 项的键值是 2022年4月23日
键名为 气温 项的键值是 23.3
键名为 空气质量 项的键值是 良
(map[string]interface {})map[日期:2022年4月23日 空气质量:良]
谢语言中,复杂数据结构也是可以嵌套的,例如列表中的数据项可以是一个映射或列表,映射中的键值也可以是列表或映射。看下面的例子(toJson.xie):
var $map1 map
setMapItem $map1 "姓名" 张三
setMapItem $map1 "年龄" #i39
var $map2 map
setMapItem $map2 "姓名" 张胜利
setMapItem $map2 "年龄" #i5
var $list1 list
addItem $list1 $map2
setMapItem $map1 "子女" $list1
plo $map1
toJson $push $map1 -indent -sort
pln $pop
例子中建议了一个简单的父子关系的数据结构,父亲张三,孩子张胜利,父亲这个数据对象本身是用映射来表示的,而其子女是用列表来表示,列表中的数据项——他的孩子张胜利本身又是用一个映射来表示的。另外,为了展示更清楚,我们使用了toJson指令,这个指令可以将数据结构转换为JSON格式的字符串,第一个参数是结果放入的变量,这里用内置变量$push表示将结果压栈。目前,toJson函数支持两个可选参数,-indent表示将JSON字符串用缩进的方式表达,-sort表示将映射内的键值对按键名排序。代码运行结果如下:
(map[string]interface {})map[姓名:张三 子女:[map[姓名:张胜利 年龄:5]] 年龄:39]
{
"姓名": "张三",
"子女": [
{
"姓名": "张胜利",
"年龄": 5
}
],
"年龄": 39
}
注意对比谢语言对该数据的表达形式与JSON形式的区别。
我们对JSON编码的反操作就是将JSON格式的字符串转换为内部的数据。这可以通过定义参数时加上“#L”或“#M”形式来进行,也可以通过fromJson指令来执行。使用“#L”或“#M”的方式我们前面已经介绍过了,这里是使用fromJson关键字的例子,我们就直接用上面生成的JSON来反向操作试一下(fromJson.xie):
// 将变量s赋值为一个多行字符串
// 即所需解码的JSON文本
assign $s `
{
"姓名": "张三",
"子女": [
{
"姓名": "张胜利",
"年龄": 5
}
],
"年龄": 39
}
`
// 用fromJson指令将s中的文本解码到变量map1中
fromJson $map1 $s
// 获取map1的数据类型
// 可用于以后根据不同类型进行不同处理
typeOf $map1
// 输出类型名称
pln 类型是: $pop
// 输出map1的内容
plo $map1
// 获取map1中的键名为子女的项
// 结果放入变量list1中
getMapItem $list1 $map1 子女
// 获取list1中序号为0的项
// 结果放入变量map2中
getItem $map2 $list1 #i0
// 获取map2中键名为姓名的项
// 结果压栈
getMapItem $map2 姓名
// 输出弹栈值
pln 姓名: $pop
运行后得到:
类型是: map[string]interface {}
(map[string]interface {})map[姓名:张三 子女:[map[姓名:张胜利 年龄:5]] 年龄:39]
姓名: 张胜利
注意,typeOf指令可用于获取任意变量的数据类型名称,这在很多需要根据类型进行处理的场景下非常有用。
谢语言可以动态加载外部的代码文件并执行,这是一个很方便也很重要的功能。一般来说,我们可以把一些常用的、复用程度高的功能写成快速函数或一般函数放在单独的谢语言源代码文件中,然后在需要使用的代码中动态加载它们并使用其中的函数。可以构建自己的公共代码库,或者形成功能模块。
下面的例子演示的是在一个代码文件中先后载入两个外部模块文件并调用其中的函数。
首先编写1个模块文件module1.xie,其中包含两个快速函数add1和sub1,功能很简单,就是两个数进行相加和相减。
注意,由于快速函数与主函数共享全局变量空间,为避免冲突,建议变量名以大写的“L”结尾,以示只用于局部。另外还建议全局变量以大写的“G”结尾,一般的局部变量以大写的“T”结尾。这些不是强制要求,但也许能够起到一些避免混乱的效果。
:add1
pop $v2L
pop $v1L
add $v1L $v2L
fastRet
:sub1
pop $v2L
pop $v1L
sub $v1L $v2L
fastRet
然后再编写第二个模块文件module2.xie,其中包含一个普通函数mul1,作用是两个数相乘。
:mul1
pop $v2L
pop $v1L
mul $v1L $v2L
ret
最后编写动态加载上面两个模块的例子代码(loadModule.xie):
// 载入第1个代码文件module1.xie并压栈
loadText `scripts/module1.xie`
// 输出代码文件内容查看
pln 加载的代码: "\n" $peek "\n"
// 弹栈加载代码
// 并将结果值返回,成功将返回加载代码的第1行行号(注意是字符串类型)
// 失败将返回TXERROR:开头的错误信息
loadCode $pop
// 查看加载结果
plo $pop
// 压栈两个整数
push #i11
push #i12
// 调用module1.xie文件中定义的快速函数add1
fastCall :add1
// 查看函数返回结果(不弹栈)
plo $peek
// 再压入一个整数5
push #i5
// 调用module1.xie文件中定义的快速函数sub1
fastCall :sub1
// 查看函数返回结果(不弹栈)
plo $peek
// 载入第2个代码文件module2.xie并置于变量code1中
loadText `scripts/module2.xie` $code1
// 加载code1中的代码
loadCode $code1
// 由于不需要loadCode指令压栈返回的行号,因此弹栈将其丢弃
pop
// 再入栈一个整数99
// 此时栈中还有一个整数18
push #i99
// 调用module2.xie文件中定义的一般函数mul1
call :mul1
// 查看函数返回结果(弹栈)
plo $pop
// 退出程序执行
// 注意:如果不加exit指令,程序会继续向下执行module1.xie和module2.xie中的代码
exit
代码中的重点是loadText指令和loadCode指令。loadText从指定路径读取纯文本格式的模块代码文件内容。loadCode文件则从字符串变量中读取代码并加载到当前代码的后面,如果成功,会返回这段代码的起始位置(注意是字符串格式),有些情况下会用到这个返回值。对于以函数为主的模块,在动态加载包含这些函数的文件后,就可以用call或fastCall指令来调用相应的函数了。
代码运行的结果是:
加载的代码:
:add1
pop $v2L
pop $v1L
add $v1L $v2L
fastRet
:sub1
pop $v2L
pop $v1L
sub $v1L $v2L
fastRet
(string)18
(int)23
(int)18
(int)1782
封装函数与一般函数与快速函数的区别是:封装函数直接采用源代码形式调用,实际上会新启动一个谢语言虚拟机去执行函数代码,封闭性更好(相当于沙盒执行),也更灵活,参数和返回值通过堆栈传递;缺点是性能稍慢(因为要启动虚拟机并解析代码)。下面是封装函数调用的例子(callFunc.xie):
// 压栈准备传入的两个函数参数
push #f1.6
push #f2.3
// callFunc指令将代码块看做封装函数进行调用
// 第1个参数表示函数需要的参数,如果函数无须参数,第一个参数可以省略
callFunc 2 `
// 依次弹栈两个参数,特别注意,这里弹出的顺序不是逆序,而是与压栈顺序相同
pop $arg1
pop $arg2
// 输出两个参数检查
pln arg1= $arg1
pln arg2= $arg2
// 将两个参数相加,结果压栈
add $arg1 $arg2
// 输出栈顶值检查
pln $peek
// 需要在全局变量outG中返回函数返回值(压栈的)的个数(字符串形式)
// 这里只有1个压栈值需要返回,即两个数相加的结果
assign $outG 1
`
// 输出函数返回值(弹栈值)
// 注意如果有多个返回值,也是按封装函数入栈顺序出栈的,不是顺序
pln 函数返回值: $pop
代码中,封装函数直接用反引号扩起了多行的代码。callFunc指定需要一个参数指定要压入新虚拟机作为调用函数的参数,注意顺序不是逆序而是顺序压入栈中的。如果没有参数则可以省略这个参数。第二个参数是字符串类型的变量或值,这里传入了一个多行字符串,既是这个封装函数的代码,封装函数如果要返回值,需要在全局变量outG中返回一个字符串类型的数字,表示返回值的个数,这些返回值是以压栈的形式返回的,注意也是按封装函数入栈顺序返回而不是逆序。
注意,封装函数是以字符串形式的代码加载并执行的,这意味着封装函数也可以动态加载,例如从文件中读取代码后执行,这带来了很大的灵活性。另外,封装函数在单独的虚拟机中运行,和主函数的变量和堆栈空间都不冲突,因此可以编写更通用的函数。
上面代码的执行结果是:
arg1= 1.6
arg2= 2.3
3.9
函数返回值: 3.9
谢语言中支持对变量取引用和对引用解引用以取值,具体用法参看下面并发函数的例子。
谢语言中的并发是用类似于封装函数的并发函数来实现的。下面是并发函数调用的例子(goFunc.xie):
// 给变量a赋值浮点数3.6
// 变量a将在线程中运行的并发函数中被修改
assign $a #f3.6
// 输出当前变量a的值作参考
pln a= $a
// 获取变量a的引用,结果入栈
// 将被传入并发函数中以修改a中的值
ref $a
// 再入栈一个准备传入并发函数中的值
push `前缀`
// 调用并发函数
// 第一个参数表示需要压入并发函数所使用的堆栈中的值的数量
// 如果不需要传递参数,第一个参数可以省略
// 第二个参数是字符串形式的并发函数代码
goFunc 2 `
// 弹栈两个传入的参数,注意也不是逆序弹出的而是顺序弹出的
pop $arg1
pop $arg2
// 查看两个参数值
pln arg1= $arg1
pln arg2= $arg2
// 解引用第一个参数(即主函数中的变量a的应用)
unref $aNew $arg1
// 输出变量a的值以供参考
pln 外部的变量a的值为 $aNew
// 无限循环演示不停输出时间
// loop1是用于循环的标号
:loop1
// 输出sub和变量arg2中的值
pln sub $arg2
// 获取当前时间并压栈
now
// 将弹栈值(当前时间)赋值给变量arg1指向的变量
// assignRef的第一个参数必须是一个引用
assignRef $arg1 $pop
// 休眠2秒
sleep #f2
// 跳转到标号loop1(实现无限循环)
goto :loop1
`
// 主线程中输出变量a的值
// 此时刚开始启动并发函数,变量a中的值有可能还未改变
pln main $a
// 注意,这里的标号loop1虽然与并发函数中的同名,但由于运行在不同的虚拟机中,因此不会冲突,可以看做是两个标号
:loop1
// 休眠1秒
sleep #f1.0
// 输出变量a中的值查看
// 每隔一秒应该会变成新的时间
pln a= $a
// 跳转到标号loop1(实现无限循环)
goto :loop1
代码中有详细的注释,主线程中启动了一个子线程,也就是调用了并发函数,看看运行效果:
a= 3.6
main 3.6
arg1= 0xc00017e350
arg2= 前缀
外部的变量a的值为 3.6
sub 前缀
a= 2022-04-28 14:58:50.6204045 +0800 CST m=+0.014610101
sub 前缀
a= 2022-04-28 14:58:52.6308323 +0800 CST m=+2.025024001
a= 2022-04-28 14:58:52.6308323 +0800 CST m=+2.025024001
sub 前缀
a= 2022-04-28 14:58:54.6329674 +0800 CST m=+4.027145301
a= 2022-04-28 14:58:54.6329674 +0800 CST m=+4.027145301
sub 前缀
a= 2022-04-28 14:58:56.636559 +0800 CST m=+6.030723101
a= 2022-04-28 14:58:56.636559 +0800 CST m=+6.030723101
sub 前缀
a= 2022-04-28 14:58:58.6391475 +0800 CST m=+8.033297801
a= 2022-04-28 14:58:58.6391475 +0800 CST m=+8.033297801
exit status 0xc000013a
仔细观察程序的输出,可以看出并发函数中的输出每两秒1次,主线程中的输出每2秒一次,变量a中的值确实从最初的浮点数3.6到后来被并发函数变成了当前时间。
这个例子中也演示了对变量取引用与对引用解引用后取变量值的方法。
谢语言提供一个通用的可扩展的对象机制,来提供集成宿主语言基本能力和库函数优势的方法,对象可以自行编写,可以使用宿主语言也可以使用谢语言本身编写(建设中),同时,谢语言也已经提供了一些内置的对象供直接使用。
下面是使用内部对象string的一个例子(object.xie),这个对象非常简单,仅仅封装了一个字符串,但提供了一些成员方法来对其进行操作。
注意,谢语言的对象一般包含本体值(例如string对象就是其包含的字符串)及可以调用的成员方法,还可能包含成员变量。
// 新建一个string对象,赋以初值字符串“abc 123”,放入变量s中
newObj $s string `abc 123`
// 获取对象本体值,结果压栈
getObjValue $s
// 将弹栈值加上字符串“天气很好”,结果压栈
add $pop "天气很好"
// 输出弹栈值供参考
pln $pop
// 设置变量s中string对象的本体值为字符串“very”
setObjValue $s "very"
// 输出对象值供参考
pln $s
// 调用该对象的add方法,并传入参数字符串“ nice”
// 该方法将把该string对象的本体值加上传入的字符串
callObj $s add " nice"
// 再次输出对象值供参考
pln $s
// 调用该对象的trimSet方法,并传入参数字符串“ve”
// 该方法将把该string对象的本体值去掉头尾的字符v和e
// 直至头尾不是这两个字符任意之一
callObj $s trimSet "ve"
// 再次输出对象值供参考
pln $s
代码运行的结果是:
abc 123天气很好
very
very nice
ry nic
谢语言使用一个简化的错误处理模式,参看下面的代码(onError.xie):
// 设置错误处理代码块为标号handler1处开始的代码块
// onError指令后如果不带参数,表示清空错误处理代码块
onError :handler1
// 故意计算1除以0的结果,将产生运行时异常
div #i1 #i0
// 此处代码正常情况应执行不到
// 但错误处理代码块将演示如何返回此处继续执行
:next1
// 输出一个提示信息
pln 计算完毕(错误处理完毕)
// 退出程序
exit
// 错误处理代码块
:handler1
// 发生异常时,谢语言将会依次入栈错误原因提示信息和出错代码的行号
// 错误处理代码块应该将这两个值弹栈后处理(或丢弃)
pop $errMsg
pop $lastLine
// 输出错误信息
pl "代码运行到第%v行时发现错误:%v" $lastLine $errMsg
// 跳转到指定代码位置继续执行
goto :next1
关键点是使用onError指令,它带有一个参数,一般是如果代码运行发生异常时将要跳转到的错误处理代码块的标号。onError指令既是指定代码运行错误时,用于处理错误的代码块。这样,如果代码运行发生任何运行时错误,谢语言将会依次将错误提示信息和出错代码的行号压入堆栈,然后从该标号处开始执行。错误处理代码块一般需要先将两个压栈值出栈,然后进行相应的错误处理,最后可以选择跳转到指定位置执行,或者终止程序运行等操作。还有一种常用的处理方式是跳转到出错行号的下一个行号处继续执行。
本段代码的运行结果是:
代码运行到第1行时发现错误:runtime error: integer divide by zero
计算完毕(错误处理完毕)
谢语言主程序支持常见的关系型数据库的访问与操作,直接参看下面访问SQLite3数据库的代码例子(sqlite.xie):
// 判断是否存在该库(SQLite库是放在单一的文件中的)
// 注意请确保c:\tmp文件夹已存在
// 结果放入变量b中
fileExists $b `c:\tmpx\test.db`
// 如果否则跳到下一步继续执行
// 如果存在则删除该文件
// removeFile指令的运行结果将被丢弃(因为使用了内置全局变量drop)
ifNot $b :next
removeFile $drop `c:\tmpx\test.db`
:next1
// 创建新库
// dbConnect用于连接数据库
// 除结果参数外第一个参数是数据库驱动名称,支持sqlite3、mysql、godror(即Oracle)、mssql(即MS SQLServer)等
// 第二个参数是连接字符串,类似 server=129.0.3.99;port=1433;portNumber=1433;user id=sa;password=pass123;database=hr 或 user/pass@129.0.9.11:1521/testdb 等
// SQLite3的驱动将基于文件创建或连接数据库
// 所以第二个参数直接给出数据库文件路径即可
dbConnect $db "sqlite3" `c:\tmpx\test.db`
// 判断创建(或连接)数据库是否失败
// rs中是布尔类型表示变量db是否是错误对象
// 如果是错误对象,errMsg中将是错误原因描述字符串
isErr $rs $db $errMsg
// 如果为否则继续执行,否则输出错误信息并退出
ifNot $rs :next2
pl "创建数据库文件时发生错误:%v" $errMsg
exit
:next2
// 将变量sqlStmt中放入要执行的建表SQL语句
assign $sqlStmt = `create table TEST (ID integer not null primary key, CODE text);`
// 执行SQL语句,dbExec用于执行insert、delete、update等SQL语句
dbExec $rs $db $sqlStmt
// 判断是否SQL执行出错,方式与前面连接数据库时类似
isErr $errStatus $rs $errMsg
ifNot $errStatus :next3
pl "执行SQL语句建表时发生错误:%v" $errMsg
// 出现错误时,因为数据库连接已打开,因此需要关闭
dbClose $drop $db
exit
:next3
// 进行循环,在库中插入5条记录
// i是循环变量
assign $i #i0
:loop1
assign $sql `insert into TEST(ID, CODE) values(?, ?)`
// genRandomStr指令用于产生随机字符串
genRandomStr $str1
dbExec $rs $db $sql $i $str1
isErr $errStatus $rs $errMsg
ifNot $errStatus :next4
pl "执行SQL语句新增记录时发生错误:%v" $errMsg
dbClose $drop $db
exit
:next4
inc $i
< $i #i5
if $pop :loop1
// 进行数据库查询,验证查看刚刚新增的记录
assign $sql `select ID, CODE from TEST`
// dbQuery指令用于执行一条查询(select)语句
// 结果将是一个数组,数组中每一项代表查询结果集中的一条记录
// 每条记录是一个映射,键名对应于数据库中的字段名,键值是相应的字段值,但均转换成字符串类型
dbQuery $rs $db $sql
// dbClose指令用于关闭数据库连接
dbClose $drop $db
pln $rs
// 用toJson指令将结果集转换为JSON格式以便输出查看
toJson $jsonStr $rs -indent -sort
pln $jsonStr
执行结果是(确保c:\tmpx目录已经存在):
[map[CODE:YRKOEt ID:0] map[CODE:moODkc ID:1] map[CODE:we7Ey9 ID:2] map[CODE:fF7dRd ID:3] map[CODE:9X6KAu ID:4]]
[
{
"CODE": "YRKOEt",
"ID": "0"
},
{
"CODE": "moODkc",
"ID": "1"
},
{
"CODE": "we7Ey9",
"ID": "2"
},
{
"CODE": "fF7dRd",
"ID": "3"
},
{
"CODE": "9X6KAu",
"ID": "4"
}
]
可以看到,数据库中被新增了5条记录,并查询成功。
谢语言主程序自带一个服务器模式,支持一个轻量级的WEB/应用/API三合一服务器。可以用下面的命令行启动:
D:\tmp>xie -server -dir=scripts
[2022/04/30 17:18:11] 谢语言微服务框架 版本0.0.6 -port=:80 -sslPort=:443 -dir=scripts -webDir=scripts -certDir=.
[2022/04/30 17:18:11] 在端口:443上启动https服务...
在端口:80上启动http服务 ...
[2022/04/30 17:18:11] 启动https服务失败:open server.crt: The system cannot find the file specified.
可以看到,谢语言的服务器模式可以用-server参数启动,并可以用-port参数指定HTTP服务端口(注意加冒号),用-sslPort指定SSL端口,-certDir用于指定SSL服务的证书文件目录(应为server.crt和server.key两个文件),用-dir指定服务的根目录,-webDir用于指定静态页面和资源的WEB服务。这些参数均有默认值,不输入任何参数可以看到。
输出信息中的错误是因为没有提供SSL整数,SSL服务将启动不了,加上证书就可以了。
此时,用浏览器访问本机的http://127.0.0.1:80就可以访问一个谢语言编写的网页服务了。
假设在指定的目录下包含xmsIndex.xie、xmsTmpl.html、xmsApi.xie三个文件,可以展示出谢语言建立的应用服务器支持的各种模式。
首先浏览器访问 http://127.0.0.1/xmsTmpl.html ,这将是访问一般的WEB服务,因为WEB目录默认与服务器根目录相同,所以将展示根目录下的xmsTmpl.html这个静态文件,也就是一个例子网页。
可以看到,该网页文件中文字“请按按钮”后的“{{text1}}”标记,这是我们后面展示动态网页功能时所需要替换的标记。xmsTmpl.html文件的内容如下:
<html>
<body>
<script>
function test() {
let xhr = new XMLHttpRequest();
xhr.open('POST', 'http://127.0.0.1:80/xms/xmsApi', true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
xhr.onload = function(){
alert(xhr.responseText);
}
xhr.send("param1=abc¶m2=123");
}
</script>
<div>
<span>请按按钮{{text1}}:</span><button onclick="javascript:test();">按钮1</button>
</div>
</body>
</html>
然后我们尝试进行动态网页输出,也就是类似PHP、ASP或其他类似的架构支持的后台动态渲染网页的方式。访问 http://127.0.0.1/xms/xmsIndex ,URL中加上xms路径,这是一个虚拟路径,表示服务器将去根目录下寻找xmsIndex.xie文件来执行,该代码将输出网页内容。我们来看下xmsIndex.xie文件的内部。
// 设定默认的全局返回值变量outG为字符串TX_END_RESPONSE_XT
// 默认谢语言服务器如果收到处理请求的函数返回结果是TX_END_RESPONSE_XT
// 将会终止处理,否则将把返回值作为字符串输出到网页上
assign $outG "TX_END_RESPONSE_XT"
// 获得相应的网页模板
// joinPath指令将把多个文件路径合并成一个完整的文件路径
// 第一个参数表示结果将要放入的变量,这里的$push表示压栈
// basePathG是内置全局变量,表示服务的根目录
joinPath $push $basePathG `xmsTmpl.html`
pln $basePathG
pln $peek
// 将该文件作为文本字符串载入,结果压栈
loadText $pop
// 替换其中的{{text1}}标记为字母A
strReplace $pop "{{text1}}" "A"
// 将弹栈值写网页输出
// responseG也是内置的全局变量,表示要写入的网页输出对象
writeResp $responseG $pop
// 终止请求响应处理微服务
exit
谢语言服务器模式中,每一个http请求都将单开一个虚拟机进行处理,可以看做一个微服务的概念。例子中的微服务仅仅是将载入的网页模板中的指定标记替换掉然后输出到网页,虽然简单,但已经展现出了动态网页的基本原理,即能够在输出网页前进行必要的、可控的渲染。
我们访问 http://127.0.0.1/xms/xmsIndex 这个网址(或者叫URL路径),将会看到如下结果:
可以发现原来的标记确实被替换成了大写的字母A,验证了动态网页的效果。
再看上面的网页模板文件xmsTmpl.html,其中的按钮点击后将执行JavaScript函数test,其中进行了一个AJAX请求,然后将请求的结果用alert函数输出出来。这是一个典型的客户端访问后台API服务的例子,我们来看看如何实现这个后台API服务。下面是也在服务器根目录下的xmsApi.xie文件中的内容:
// 获取当前时间放入变量t
nowStr $t
// 输出参考信息
// 其中reqNameG是内置全局变量,表示服务名,也就是访问URL中最后的部分
// argsG也是全局变量,表示HTTP请求包含的URL参数或Form参数(可以是GET请求或POST请求中的)
pl `[%v] %v args: %v` $t $reqNameG $argsG
// 设置输出响应头信息(JSON格式)
setRespHeader $responseG "Content-Type" "text/json; charset=utf-8"
// 写响应状态为整数200(HTTP_OK),表示是成功的请求响应
writeRespHeader $responseG #i200
// 用spr指令拼装响应字符串
spr $push "请求是:%v,参数是:%v" $reqNameG $argsG
// 用genJsonResp生成封装的JSON响应,也可以自行输出其他格式的字符串
genJsonResp $push $requestG "success" $pop
// 将响应字符串写输出(到网页)
writeResp $responseG $pop
// 结束处理函数,并返回TX_END_RESPONSE_XT以终止响应流的继续输出
exit TX_END_RESPONSE_XT
这样,我们如果点击网页中的按钮1,会得到如下的alert弹框:
这是因为网页xmsTmpl.html中,通过AJAX访问了 http://127.0.0.1:80/xms/xmsApi 这个服务,而我们的谢语言服务器会寻找到xmsApi.xie(自动加上了.xie文件名后缀)并执行,因此会输出我们希望的内容。
至此,一个麻雀虽小五脏俱全的WEB/应用/API多合一服务器的例子就完整展现出来了,已经足够一般小型的应用服务,并且基本无外部依赖,部署也很方便,只需一个主程序以及拷贝相应目录即可。
用谢语言实现一个网络客户端也非常容易,以上面的网络服务端为例,访问这些服务的客户端代码(httpClient.xie)如下:
// getWeb指令可以用于各种基于HTTP的网络请求,
// 此处是获取某URL处的网页内容
// 第一个参数pageT用于存放访问的结果内容
// -timeout参数用于指定超时时间,单位是秒
getWeb $pageT "http://127.0.0.1/xms/xmsIndex" -timeout=15
// 输出获取到的内容参考
pln $pageT
// 定义一个映射类型的变量mapT
// 用于存放准备POST的参数
var $mapT map
// 设置示例的POST参数
setMapItem $mapT param1 value1
setMapItem $mapT param2 value2
// 输出映射内容参考
pln $mapT
// 以POST的方式来访问WEB API
// getWeb指令除了第一个参数必须是返回结果的变量,
// 第二个参数是访问的URL,其他所有参数都是可选的
// method还可以是GET等
// encoding用于指定返回信息的编码形式,例如GB2312、GBK、UTF-8等
// headers是一个JSON格式的字符串,表示需要加上的自定义的请求头内容键值对
// 参数中可以有一个映射类型的变量或值,表示需要POST到服务器的参数
getWeb $resultT "http://127.0.0.1:80/xms/xmsApi" -method=POST -encoding=UTF-8 -timeout=15 -headers=`{"Content-Type": "application/json"}` $mapT
// 查看结果
pln $resultT
示例中演示了直接获取网页和用POST形式访问API服务的方法,运行效果如下:
<html>
<body>
<script>
function test() {
let xhr = new XMLHttpRequest();
xhr.open('POST', 'http://127.0.0.1:80/xms/xmsApi', true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
xhr.onload = function(){
alert(xhr.responseText);
}
xhr.send("param1=abc¶m2=123");
}
</script>
<div>
<span>请按按钮A:</span><button onclick="javascript:test();">按钮1</button>
</div>
</body>
</html>
map[param1:value1 param2:value2]
{"Status":"success","Value":"请求是:xmsApi,参数是:map[param1:value1 param2:value2]"}
可以看到,程序顺利获得到了所需的服务器响应。
谢语言也支持自己手动编写各种基于HTTP的服务器,下面是一个API服务器的例子(apiServer.xie):
// 新建一个路由处理器
newMux $muxT
// 设置处理路由“/test”的处理函数
// 第4个参数是字符串类型的处理函数代码
// 将以新的虚拟机运行
// 虚拟机内将默认有4个全局变量:
// requestG 表示http请求对象
// responseG 表示http响应对象
// paraMapG 表示http请求传入的query参数或post参数
// inputG 是调用setMuxHandler指令传入的第3个参数的值
setMuxHandler $muxT "/test" #i123 `
// 输出参考信息
pln "/test" $paraMapG
// 拼装输出的信息字符串
// spr类似于其他语言中的sprintf函数
spr $strT "[%v] 请求名: test,请求参数: %v,inputG:%v" ?(?nowStr) $paraMapG $inputG
// 设置输出的http响应头中的键值对
setRespHeader $responseG "Content-Type" "text/json; charset=utf-8"
// 设置输出http响应的状态值为200(表示成功,即HTTP_OK)
writeRespHeader $responseG 200
// 准备一个映射对象用于拼装返回结果的JSON字符串
var $resMapT map
setMapItem $resMapT "Status" "success"
setMapItem $resMapT "Value" $strT
toJson $jsonStrT $resMapT
// 写http响应内容,即前面拼装并转换的变量jsonStrT中的JSON字符串
writeResp $responseG $jsonStrT
// 设置函数返回值为TX_END_RESPONSE_XT
// 此时响应将中止输出,否则将会把该返回值输出到响应中
assign $outG "TX_END_RESPONSE_XT"
`
pln 启动服务器……
// 在端口8080上启动http服务器
// 指定路由处理器为muxT
// 结果放入变量resultT中
// 由于startHttpServer如果执行成功是阻塞的
// 因此resultT只有失败或被Ctrl-C中断时才会有值
startHttpServer $resultT ":8080" $muxT
运行后,用浏览器访问下面的网址进行测试:
http://127.0.0.1:8080/test?param1=abc¶m2=123
可以看到网页中会显示类似下面的JSON格式的输出:
{
"Status": "success",
"Value": "[2022-05-17 15:11:57] 请求名: test,请求参数: map[param1:abc param2:123],inputG:123"
}
当然,一般API服务都是用编程的形式而非浏览器访问,用浏览器比较适合做简单的测试。
谢语言实现静态WEB服务器则更为简单,见下例(webServer.xie):
// 新建一个路由处理器
newMux $muxT
// 设置处理路由“/static/”后的URL为静态资源服务
// 第3个参数是对应的本地文件路径
// 例如:访问 http://127.0.0.1:8080/static/basic.xie
// 而当前目录是c:\tmp,那么实际上将获得c:\scripts\basic.xie
setMuxStaticDir $muxT "/static/" "./scripts"
pln 启动服务器……
// 在端口8080上启动http服务器
// 指定路由处理器为muxT
// 结果放入变量resultT中
// 由于startHttpServer如果执行成功是阻塞的
// 因此resultT只有失败或被Ctrl-C中断时才会有值
startHttpServer $resultT ":8080" $muxT
运行后,访问http://127.0.0.1:8080/static/basic.xie,将获得类似下面的结果:
// 本例演示做简单的加法操作
// 将变量x赋值为浮点数1.8
assign $x #f1.8
// 将变量x中的值加上浮点数2
// 结果压入堆栈
add $x #f2
// 将堆栈顶部的值弹出到变量y
pop $y
// 将变量x与变量y中的值相加,结果压栈
add $x $y
// 弹出栈顶值并将其输出查看
// pln指令相当于其他语言中的println函数
pln $pop
实际上读取了当前目录的scripts子目录下的basic.xie文件展示。
如果想要实现动态网页服务器,类似PHP、JSP、ASP等,可以参考之前的微服务/应用服务器和手动编写API服务器等例子,很容易实现。
目前暂时请参看代码中的InstrNameSet数据结构的代码注释,后面文档会慢慢补齐。
目前暂时请参看代码中的各个Xie...对象(如XieString)的代码内文档说明。
谢语言的目标是使用简单的语法结构减少脚本语言的语法解析开销以便提升速度,并且通过广泛使用内置指令避免使用速度很慢的反射。具体速度评估可以参考例子代码中的斐波那契数列产生的两个例子(递归方式fix.xie和循环方式fibFlat.xie)。
扩展谢语言一般来说有两个方法:
增加内置指令:请fork本库,参考xie.go中的源代码,参看各个指令的写法编写自己的新指令,然后编译出可执行代码即可。
增加内置对象:请fork本库,参考xie.go中的源代码,各个Xie...对象(如XieString)的代码内文档说明,重点是实现XieObject接口,然后编译出可执行代码即可。
注:更多示例请参考cmd/xie/scripts目录
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。