这次要实现的是一个形式最简单的脚本。这种脚本仅有命令、标号及跳转构成,看起来就跟汇编一样,不过好是比较好读的。虽然这种脚本语言的语法非常简单,但是最基本的要素还是要有的。
作为一个脚本引擎,为了可以在各种各样的合适的宿主程序中使用,脚本本身最好不要涉及到具体的领域。当然,如果这个脚本被创建的目的仅仅是为了某个领域的话,那就无所谓了。因此,一个脚本引擎需要一个检查和运行代码的机制、运行时环境的维护以及一个功能足够使用的插件系统。一个完整的脚本引擎至少需要如下部件:
1、代码数据结构。代码的数据结构用来存放经过分析的脚本代码。事实上解释型的脚本引擎,也就是边执行边分析代码字符串的脚本引擎是比较难做,而且效率也不高的。脚本代码经过事先分析,可以检查一出一些在运行之前就能够检查的错误。而且我们把脚本的代码重新处理成一个数据结构之后,执行也变得更加容易控制。
2、运行时环境。运行时环境用于存放脚本在运行的过程中产生的数据,譬如堆栈、变量和状态信息等。对于一个已知的代码,不同的运行时环境代表不同的脚本执行流程。为了让脚本可以同时(但不一定是并发)执行,将运行时环境独立出来也就显得必要了。
3、语法分析器。语法分析器用于将代码转换成等价的代码数据结构,并在发现代码出错的时候输出合适的错误信息。
4、插件。插件是脚本与外部环境交互的途径之一。有了插件系统,我们可以为脚本引擎添加额外的、跟脚本引擎无关的功能,譬如文件操作、屏幕输入输出等。如果必要的话,插件系统可以将脚本引擎与领域信息互相隔离,系统将变得更加容易使用。
5、虚拟机。虚拟机用于执行代码并返回相应的结果。我们在使用脚本引擎时直接跟虚拟机进行交互,虚拟机则协调上述4个部件的相互协作。
在知道了这些之后,我们就可以开始开发一个基于命令的脚本引擎了。为了更加详细以及明确地讲述开发过程以及原理,在这里将构造一门简单的基于命令的语言。一门语言至少还是要有分支和循环的。但是为了简化,我们将分支和循环分解成判断与跳转。语言可以自由添加标号,标号将作为跳转的目标而出现。这门语言使用如下语法:
<值>:值可以是整数、小数、字符串或名字。
<名>:名可以是变量名或者标号等,使用字母与下划线开始,后接不定数量的字母、下划线与数字。
<名>::名字后接冒号代表一个标号。这个标号代表着一个指令的位置,用于指定跳转目标。
goto <名>:goto用于直接跳转到一个位置继续执行。
set <名> <值>:set用于将一个值赋值给一个指定名字的变量。这个变量不存在则创建。
opcode <名> <值> <值>:opcode可以是add、minus、mul、div、idiv或mod。这6个命令将两个值进行加、减、乘、除、整除及求余,并将结果赋值给一个指定名字的变量。这个变量不存在则创建。
if <值>[ opcode <值>] goto <名>:if用于判断一个条件并在条件满足被满足的时候跳转到指定的地方。条件可以是一个值,这个值必须是整数,并且在这个值不为0的时候条件被满足。条件也可以是一个比较,这个时候opcode可以是is、is_not、less_than、greater_than、less_equal或greater_equal,分别在第一个值等于、不等于、小于、大于、小于或等于、大于或等于第二个值的时候满足条件。
exit:结束执行
<名> <值>*:如果命令名称不是上面的5种的其中一种的话,那么这个命令将被传递给插件进行执行。这个时候,命令可以有任意的参数。
在这种语法下,我们可以假设宿主程序给了我们write、writeln和read命令用于输入输出,并得到一个判断输入的数字是否质数的程序:
write "请输入一个数字:"
read Number
if Number less_then 2 goto FAIL
if Number is 2 goto SUCCESS
set Divisor 2
LOOP_BEGIN:
if Number is Divisor goto SUCCESS
mod Remainder Number Divisor
if Remainder is 0 goto FAIL
add Divisor Divisor 1
goto LOOP_BEGIN
SUCCESS:
writeln Number "是质数。"
exit
FAIL:
writeln Number "不是质数。"