使用Ruby与Lex/Yacc一起构建编译器的第一步

首先这个编译器的目标语言已经不叫oz了。对,只是不叫而已。毕竟不能让自己老是在做技术选型,从纯C(并搭配Ruby生成C代码……)到CoffeeScript再到Ruby。所以改个名字,警示自己一下。新的名字叫Daze(迷糊),嗯,起贱名好养活。现在GitHub上的代码仓库还是之前Ruby生成C的那一版,等手上这个完成一次merge的工作量后再上传。

目前的语言/编译器的进展可以概括如下。源代码(凑合看吧)

1
2
3
4
use "./Standard" >> int
Main = () -> int
return int(42)

编译(或者说解析)结果(经过手动调整格式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
"type"=>"def_list",
"def"=>{
"type"=>"use_def",
"path"=>"./Standard",
"used"=>"int"
},
"next"=>{
"type"=>"def_list",
"def"=>{
"type"=>"func_def",
"head"=>"Main",
"body"=>{
"type"=>"block",
"inner"=>{
"type"=>"stat_list",
"stat"=>{
"type"=>"return_stat",
"expr"=>{
"type"=>"expr3",
"func"=>"int",
"expr"=>42
}
},
"next"=>{
"type"=>"stat_empty"
}
}
}
},
"next"=>{
"type"=>"def_empty"
}
}
}

之所以层次这么多是因为所有的列表都以双元素链表字典的方式存在。这样可以在一定程度上减轻编译器的实现难度(就是懒)。不过说真的,写到这里我开始有点不懂了。这么好的技术搭配,为什么从来没见到有人用过?从来??显然,代码的世界还有许多的未知之地在等着我去冒险。

目前项目的Source目录下有以下内容

  • Main.rb入口,命令行界面
  • Compile.rb主要且唯一的编译封装界面
  • Parser文件夹,下属各源码解析文件
    • Scanner.l/Parser.y是Lex和Yacc的输入文件
    • Bridge.h/Bridge.c定义了上述两个文件中用到的Ruby方法,以及整个Parser暴露给Ruby项目的接口
    • extconf.rb用于生成Makefile,build.sh负责文件夹的整个构建过程。编译过后,Parser文件夹旁边会多出来一个Parser.bundle文件,在Ruby看来和一个普通的Parser.rb没什么区别。

这篇文章主要集中在讨论Parser文件夹下。我们知道在Yacc的输入文件当中,每一个终结符/非终结符都是有值的。通常来说这个值的类型是不统一的,但是有了Ruby就不一样了。Ruby良好的C API使得所有结果具有同一个类型VALUE,哪怕不像我这般几乎全用Hash也是一样。不仅如此,Ruby提供的接口会让你感觉你写的并不是C,只是语法稍微拙劣了一点的Ruby。下面的函数为一个非终结符创建起对应在AST上的节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
VALUE CreateNode(const char *type, const int child_count, ...)
{
VALUE node = rb_hash_new();
rb_hash_aset(node, rb_str_new_cstr("type"), rb_str_new_cstr(type));
va_list ap;
va_start(ap, child_count);
for (int i = 0; i < child_count; i++)
{
const char *name = va_arg(ap, const char *);
VALUE child = va_arg(ap, VALUE);
rb_hash_aset(node, rb_str_new_cstr(name), child);
}
va_end(ap);
return node;
}

其中rb_hash_aset(hash, key, value)对应着hash[key] = value,而rb_str_new_cstr则以C风格的字符串为参数,创建Ruby中类型为String的变量(在C中的类型当然是VALUE)。用着Ruby的C API,真的让我感觉哪怕我不是为了给Ruby写拓展,仅仅是在写一个纯C的项目,我都想把它当成是一个代码库来用。一个有类型检查的void *,还有内建的垃圾收集,以及大量的实用工具……除了速度太慢以外堪称完美。

顺便一说,上面的C代码成功地把Atom的代码高亮玩坏掉了……真是太脆弱了。

有了统一类型VALUE,那么把YYSTYPE定为VALUE也就成了顺理成章的事情。然而也就是因为这个我发现了Yacc(Bison)的一个bug,哪怕到Google去搜也没法找到类似的情况。难道说我是第一个用这种姿势使用Yacc的人?震惊。

这个bug就是,用-d参数指示Yacc生成y.tab.h时,与YYSTYPE以及yylval相关的代码如下

1
2
3
4
5
6
7
8
#if ! defined YYSTYPE && ! defined YYSTYPE_IS_DECLARED
typedef int YYSTYPE;
# define yystype YYSTYPE /* obsolescent; will be withdrawn */
# define YYSTYPE_IS_DECLARED 1
# define YYSTYPE_IS_TRIVIAL 1
#endif
extern YYSTYPE yylval;

虽然我在Parser.y/y.tab.c里的确定义了YYSTYPE,但是y.tab.h不可能知道这件事,因此不管我在Yacc里把它的类型定义成啥,在Lex里看到的yylval都是一个int类型的变量。由于某些兼容性问题,在链接的时候这个全局变量并不会引起报错,但我知道在Lex里面给yylval赋值时赋的值一定是先强制类型转化为int再赋的值,因为VALUE实际上就是unsigned long,在Ruby里相当于句柄一样的存在,而被转换为int以后,哪怕是简单的输出到屏幕都会引起Ruby虚拟机段错误崩溃。

哦对,只要简单的调用rb_p就可以把任何VALUE类型的变量输出到屏幕,这大概是我见过的最简单的print调试工作流了。

总之,在没有SO问题+没有文档相关+没有GH issue的情况下,我只好在Lex的头部首先把YYSTYPE也定义为VALUE在引用y.tab.h文件。这也许是最好的解决办法了吧。


作为Daze语言的第一个用例,这个程序还是相当简单的,我只要解决int的问题就可以让它运行了。是的,int作为基本类型,却不是这个语言的一部分。这种允许用户自定义任何类型,无论基本还是复合的疯狂举动主要是为了回答自己的一个疑问:“为什么语言的类型要分为内建的和自定义的?”这样,我就不用回答这个问题了。