3.1 制作crowbar ver.0.1语言的基础部分

本书首先制作一门无变量类型的语言。像Perl、Ruby、Python、PHP这些近些年火起来的脚本语言,基本都没有变量类型。我们把将要制作的语言命名为crowbar。

本章首先对crowbar的初始版本(ver.0.1)进行简要说明。

3.1.1 crowbar是什么

crowbar不是那种如果找到有四片叶子就会有好运降临的植物(那叫三叶草),而是如图3-1这样形状的工具。

figure_0080_0024

图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的设计都不太一样,实在让人有些头疼。

figure_0086_0025

因为crowbar的目标就是成为“Perl那样的东西”,所以我就私自决定采用 elsif了。

3.1.6 语句与运算符

首先,crowbar支持以下形式的常量作为语句。

整数字面常量,如123等。

实数字面常量,如123.456等。

字符串字面常量。双引号包裹的字符串,如 "abc"等。

另外变量也可以作为语句。

进而可以和运算符结合构成更复杂的语句,当然还支持括号。

crowbar可使用的运算符如表3-1所示(按运算优先级排序)。

表3-1 crowbar可使用的运算符

figure_0087_0026

%运算也可以用在实数上,本质上是在内部调用了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的内置函数

figure_0088_0027

显而易见,基本上所有文件操作函数的设计都沿袭了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);