3.1 制作crowbar ver.0.1语言的基础部分
本书首先制作一门无变量类型的语言。像Perl、Ruby、Python、PHP这些近些年火起来的脚本语言,基本都没有变量类型。我们把将要制作的语言命名为crowbar。
本章首先对crowbar的初始版本(ver.0.1)进行简要说明。
3.1.1 crowbar是什么
crowbar不是那种如果找到有四片叶子就会有好运降临的植物(那叫三叶草),而是如图3-1这样形状的工具。
图3-1 名为crowbar的工具
之所以起名叫crowbar,主要是因为这次要做的语言会生成分析树并执行。单就这点来说是与Perl比较接近的。有句话是怎么说来着,对了,就是那句经常能从新闻里听到的:
撬棍状的物体 [1]
于是我就以crowbar命名了。喂,别向我扔石头啊。
如前文所述,crowbar的语法应当照顾本书读者的习惯,所以沿袭了C语言的语法。
首先将初版的crowbar命名为crowbar book_ver.0.1,示例代码如代码清单3-1所示。
代码清单3-1 fizzbuzz_0_1.crb
1: for (i = 1; i < = 100; i = i + 1) {
2: if (i % 15 == 0) {
3: print("FizzBuzz\n");
4: } elsif (i % 3 == 0) {
5: print("Fizz\n");
6: } elsif (i % 5 == 0) {
7: print("Buzz\n");
8: } else {
9: print("" + i + "\n");
10: }
11: }
与代码清单1-1不同的是,由于自增运算符 ++尚未实现,所以写成了 i = i + 1。
这个版本的crowbar还没有实现一门编程语言应当具备的所有基本功能(可能有读者会说,就这样也敢与Perl相提并论呀),当前版本所实现的功能,会在以后的章节中加以说明。
3.1.2 程序的结构
crowbar与Perl一样,支持在 顶层结构 书写代码。所谓的顶部结构,即函数或类的外侧。
C语言中,在函数的外面可以定义变量却不能书写执行语句,因此即便只写一句“hello, world”,也需要 main() 函数。Java 就更悲惨了,必须写长 长 的 一 串 public class HelloWorld 还 有 public static void main(String[] args)这种外行人看来像咒语一样的东西。如果仅仅想写几行简单的脚本,这实在很麻烦,而对于初学者来说也增加了学习的难度。
在crowbar中,如果想写一个显示“hello, world”的程序,只需简单地写成下面这样就可以了。
print("hello, world\n");
无需再包裹函数或者类。
函数的定义,需要使用保留字 function,按如下方式书写:
显示将a与b相加的值,并且作为返回值返回的函数
function hoge(a, b) {
c = a + b;
print("a+b.." + c + "\n");
return c;
}
函数定义在程序中可以写在任意位置。程序执行时,首先将顶层结构中的语句从上往下顺序执行,函数定义部分会被跳过。直至函数被调用时,才执行该函数内的语句。
函数如果不存在 return语句,将返回特殊的常量 null。
3.1.3 数据类型
可以使用的数据类型如下所示。
布尔型。值可以为 true或 false。
整数型。其实就是crowbar底层运行环境的C语言的 int型。
实数型。即crowbar底层运行环境的C语言的 double型。当整数型与实数型混合运算时,整数型将被扩充为实数型。
字符串型。可以通过 +运算符连接。另外,如果字符串在左侧数值在右侧,用 +连接的话,右侧将被转换为字符串型。
例如:
print("10 + 5.." + (10 + 5)); ←将显示10 + 5..15
原生指针型(Native Pointer)。请读者不要根据名字将其想象成那种可以直接访问内存的邪恶指针,crowbar的原生指针型类似于C语言的 FILE*,是用于在crowbar内部移动跳转的类型。详细请参考3.1.7节。
在book_ver.0.1中,不存在数组、关联数组(associative. array)、类、对象等类型。
3.1.4 变量
crowbar与Perl、Ruby等相同,都是静态无类型(即变量无需声明类型)语言。
crowbar无需变量声明,赋初始值时就包含了声明过程(和Ruby非常类似)。如果直接引用一个还没有赋值的变量则会报错。
变量的命名规则与C基本一样,必须以字母开头,第二个字符开始可以使用字母数字,也支持下划线。与Perl等不同的是,变量开头无需书写$符号。
函数内首次进行赋值的变量会作为函数的局部变量,局部变量的生命周期及作用域仅限于当前函数内部。C语言等还可以在函数中用{}再开辟一个块(Block),并在块内有更小作用域的局部变量,crowbar则不支持这种特性。
变量是在赋值语句执行时进行声明的,如下例所示:
if(a==10){
b=10;
}
print("b.."+b);
a只有为10的时候b才被声明,print语句可以正常显示。如果a不为10则会报出未定义变量的错误。
在顶层结构中赋值的变量会成为全局变量。函数中引用全局变量时,需要用global语句进行声明。
global语句可以按以下的方式使用:
global变量名,变量名,…;
比如函数内用globala;声明之后,在该函数内就可以引用全局变量a(如果全局变量a不存在则会报运行错误)。
比如运行代码清单3-2,运行结果如下所示:
a..30
a..20
运行结果第1行的a..30是代码清单3-2第10行的print输出结果,因此这里显示的是func2()中被赋值的局部变量a的值。
第2行的..20则是第15行的print结果,显示的是全局变量a的值。
因为有了global语句,所以第5行赋值的是全局变量a的引用,而第9行只引用了局部变量,因此即使对其赋值也不会对全局变量产生影响。
代码清单3-2 global.crb
1:a=10;←定义全局变量a的声明
2:
3:functionfunc(){
4:globala;
5:a=20;←这里的a是全局变量
6:}
7:
8: function func2() {
9: a = 30; ←这里的a是局部变量
10: print("a.." + a + "\n");
11: }
12:
13: func();
14: func2();
15: print("a.." + a + "\n");
那么,为什么一定要使用 global语句声明后才可以引用全局变量呢?这样的设计有以下两个原因。
如果没有任何约束就可以直接引用全局变量,那么编写函数时就必须随时掌握所有全局变量的情况,而对于强调高内聚性的函数来说,这种设计会产生致命的错误。
全局变量的使用频率并不高,因此设置这样一点障碍对编写程序不会产生太大影响。
话虽如此,在使用 STDIN(标准输入的文件指针)这样的全局变量时也必须声明,还是多少有些不方便的。
补充知识 初次赋值兼做变量声明的理由
如上文所述,crowbar会在变量初次赋值时兼做变量声明,即如果直接使用没有赋值的变量会报错。
比如在Perl中,默认情况下,即使没有赋值的变量仍然可以使用。此时该变量值会根据上下文自动转换。像下面这样书写的话:
print 123 * $a; #对未赋值的变量$a进行乘法运算
运行结果为 0,因为未赋值的变量 $a的值被自动转换为0了。
但是这样的设计容易因为变量名输入有误而引起BUG [2]。因此在crowbar的设计中,只能使用进行过初次赋值的变量。
还需要注意的是,crowbar在执行变量的赋值语句时才会被声明,而Ruby只要书写了赋值语句就完成了变量声明,即赋值语句的执行不是必须的。因此,像下面这样:
x = x; #这个例子中,赋值语句执行前,x也可以使用
或
if false
a = 1
end
print a; #赋值语句没有执行,也可以使用a。
这些程序在Ruby中都是合法的。关于这样设计的理由,Ruby的作者松本行弘先生做了如下说明(请参考ruby-list邮件列表的No.33798):
全局变量的作用域应当通过静态方式决定,也就是说,在赋值语句开始执行才检查变量是否存在,这样的设计并不好。
因为动态的变量作用域用户理解起来有难度,同时也失去了一次编程语言中为数不多的可以进行性能优化的机会。
关于这一点我是持同意态度的 [3],那为什么crowbar中没有这样去做呢?理由其实很简单,只是想要偷懒一下而已。
补充说明 各种语言的全局变量处理
下面来看一看其他语言中全局变量的处理方法。
Perl : 变量默认是全局的,只有加上 local或 my等定义后才会变成局部变量。
Ruby : 用 $开头的变量是全局变量。
PHP : 与crowbar一样,函数内要引用全局变量的话,用 global语句定义。
一般来说,程序中应该避免到处使用全局变量,而尽可能优先保证局部变量的内聚性。从这个角度来讲,Perl式的设计是不能借鉴的(当然如果是一次性的脚本,这样倒是很方便)。Ruby式的设计是比较合理的,但按这个设计写出来的程序可能到处是记号,丧失了程序的美感(这只是我主观的感受)。因此crowbar采用了PHP风格的 global语句的设计。
3.1.5 语句与结构控制
crowbar与C语言一样,有 if、 while、 for等结构控制语句。
与C、C++、Java等语言有以下两处比较大的区别:
crowbar中不允许出现悬空else(花括号 {}是强制书写的);
因为不允许悬空else,所以引入了 elsif语句。
具体来说是下面这样的形式:
if语句的例子
if (a == 10) {
a == 10 时执行
} elsif (a == 11) {
a == 11 时执行
} else {
a 不为10也不为11时执行
}
while语句的例子
while (i < 10) {
i比10小时,此处循环执行
}
for语句的例子
for(i = 0; i < 10; i = i + 1) {
这里循环10次
}
此外,在crowbar中也可使用下列语句,其意义与C语言相同。
break:从最内层的循环中跳出。
continue:跳过最内层循环中剩余的代码。
return:从函数退出,并将后面的值作为返回值返回。
break或 continue,最好能像Java那样附加一个标签,但当前版本还没有这个功能(book_ver.0.3实现了标签功能)。
补充知识 elif、elsif、elseif的选择
C等语言中,if语句允许没有花括号的写法(也称作悬空语句),也可以像下例这样用 else if排列书写。
if (a < 10) {
…
} else if (a < 20) {
…
} else if (a < 30) {
…
} else {
…
}
C或Java虽然设置了这种特别的结构控制语法,但偶尔也有初学者会误解其意义,以为 else if不是一个专用语句,而是 else语句后省略花括号又写的一个 if语句。说起来在工作中的确会遇到很多项目,在编码规范中明确规定了“禁止省略花括号”,这样就可以放心地去写 else if了。
crowbar中直接废弃了悬空语句,无法书写上述形式的 else if,为此特别引入了 elsif。不过不同语言对于 elsif的设计都不太一样,实在让人有些头疼。
因为crowbar的目标就是成为“Perl那样的东西”,所以我就私自决定采用 elsif了。
3.1.6 语句与运算符
首先,crowbar支持以下形式的常量作为语句。
整数字面常量,如123等。
实数字面常量,如123.456等。
字符串字面常量。双引号包裹的字符串,如 "abc"等。
另外变量也可以作为语句。
进而可以和运算符结合构成更复杂的语句,当然还支持括号。
crowbar可使用的运算符如表3-1所示(按运算优先级排序)。
表3-1 crowbar可使用的运算符
%运算也可以用在实数上,本质上是在内部调用了C的函数 fmod()。
无论C语言还是crowbar,都没有用常量直接表示负数。想使用负数时,可以使用单目取负符 -。
而与C 语言一样, &&、 || 都是短路运算符。也就是说,像下面这样的条件语句:
if (a < 10 && b < 20) {
…
当 a < 10的条件不成立时,不再判断 b < 20这一条件语句(已经短路,所以表达式无论真伪都不会在 if语句中执行)。
3.1.7 内置函数
内置函数是crowbar最开始就包含的用C语言编写的函数。crowbar当前版本的内置函数如表3-2所示。
表3-2 crowbar book_ver.0.1的内置函数
显而易见,基本上所有文件操作函数的设计都沿袭了C语言的stdio.h。只是因为crowbar有字符串类型,所以 fgets()等的用法会稍有不同。
此外, fopen()返回的类型是crowbar才有的“原生指针型”。上例中只是单纯指向C的 FILE*,但是这个类型的特殊之处远不止于此。比如用内置函数实现GUI时,创建一个打开新窗口的函数 create_window(),其返回值应当能表示一个“窗口”,此时就可以考虑使用原生指针型来实现。
crowbar中已经默认声明了 STDIN、 STDOUT、 STDERR等全局变量,分别对应C语言中的 stdin、 stdout、 stderr。
3.1.8 让crowbar支持C语言调用
考虑到crowbar的用途之一是扩展应用程序,那么应当让C语言编写的其他应用程序可以很容易地调用crowbar解释器。
代码清单3-3是与当前版本crowbar所属的main.c基本一样的代码段。调用里面这些函数,需要用 #include包含CRB.h文件。
代码清单3-3 crowbar被C语言调用
CRB_Interpreter *interpreter;
FILE *fp;
/ 中间省略 /
/ 生成crowbar解释器/
interpreter = CRB_create_interpreter();
/ 将FILE作为参数传递并生成分析树 */
CRB_compile(interpreter, fp);
/ 运行 /
CRB_interpret(interpreter);
/ 运行完毕后回收解释器 /
CRB_dispose_interpreter(interpreter);
3.1.9 从crowbar中调用C语言(内置函数的编写)
反过来,从crowbar中调用C语言的函数(内置函数)也同样容易。
首先用 #include包含面向开发人员的头文件CRB_dev.h,像下面这样表示C函数:
CRB_Value hoge_hoge_func(CRB_Interpreter *interpreter,
int arg_count, CRB_Value *args)
{
/ 中间省略 /
return value;
}
这里调用的 interpreter是指向解释器的指针, arg_count代表向该函数传递的参数的数量, args是参数的值(CRB_Value类型详见3.3.8节)。
crowbar是无类型语言,因此参数的数量与类型的检查都必须在内置函数进行。
通过这种方式制作出的C 函数,通过 CRB_add_native_function() 函数即可注册到解释器中,成为crowbar的内置函数。
/* 将C的函数hoge_hoge_func注册为一个crowbar可以调用的内部函数
并命名为hoge_hoge */
CRB_add_native_function(interpreter,
"hoge_hoge", hoge_hoge_func);