20111202

使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(3)

使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(3)

- 前面忘了交待的事

之所以要写这篇博客的原因,是因为在网上看到的 antlr 教程大部分都是进行四
则运算。四则运算的确很经典,不过对于学生来说,有另一个例子比只有一个例
子更好。

前几天查别的资料的时候,我抱怨过到处都是同一个贴子,赵秋实同学:那你就
自己写一个吧。写一个挺累的,但是他说的对,所以我就写了一个不是四则运算
的。

- 开发工具及版本

上次提到要生成的东西,现在终于说到正题,生成这些产品的工具,代码生成我
们的开发工具版本是antlr-3.4-complete-no-antlrv2.jar。为了方便看语法树和
简单调试,你还可以下载 antlrworks-1.4.3.jar。这两个东西都可以从
[http://antlr.org/download.html]下载。其中已经包含stringtemplate了,不
必另外下载。

此外,antlr需要java运行时库,目前的版本要求是1.5或更高。

为了跑我们生成的代码,还需要g++。我用的版本是 g++ (Ubuntu
4.4.3-4ubuntu5) 4.4.3。因为生成的代码涉及规范都很基本,理论上,你用啥版
本都行。

我在Linnux下跑所有这些东西,Ubuntu。因为这些东西都是跨平台的,你用什么
操作系统都应该可以。不过,请原谅我给的运行脚本--相当于批处理,只有
Linux版本。脚本只是为了运行方便,即使你不懂脚本,根据我的解释,手动重现
或编个批处理应该都不是难事。

- 生成

仅有工具而没有操纵工具的灵魂,是无法赋予工具以智慧的。所以,我们需要一
些文件,用以指导 antlr+stringtemplate 工作。

我们一共要生成三种东西:.h, .cpp. go.cpp(driver),还要再写两个脚本,一
个用于调用生成的过程,另一个用于编译和执行生成的产品。

为了生成这三种产品,我们需要以下文件,作为指导 antlr+stringtemplate 运
行的指令。这几个文件,除了扩展名,可以认为都是瞎起的,只是为了方便我们
记忆,没有别的意义。

以下,以生成头文件为例说明。除了pipe.g和go.sh,头文件、cpp文件、go.cpp
每个目标都需要一组以下这些东西。

1. pipe.g:.g表示这是grammar,文法文件。这里,存有我们要实现的杨氏语言的
语法。

语法,在自然语言中,是规定一个句子的主谓宾和时态等如何表达的规则。现在
的中国人,都很熟悉英语的语法。你没看错,是中国人,熟悉的是英语的语法,
另一个国家的。其实汉语本身也是有语法的,只是当今有些年轻人不再知道了。

当年我们语文课中学到:主谓宾定状补,这都是句子的成份。还在偏正短语啥的。
当然,与英语不同(这也谈不上特殊,请不要走到另一个极端,和咱们类似的也
有的是),汉语使用助词而不是动词的变形表示时态。

你吃了吗 和 你吃吗 的区别就在于时态。

汉语和英语,都是 主+谓+宾 这种形式。而日文(还有德文?)就是 主 + 宾 +
谓 这种形式。

位置、变形等决定了词的语法意义。

比如我们的输入文件:

mario:
pipe_a 123 | pipe_b | pipe_c

其中的 "mario:" 表示打算建个类,名字叫做 mario。

"pipe_a 123 | pipe_b | pipe_c"表示打算在这个类里建三个方法,其中pipe_a
有个参数。调用的时候传参123进去,调用的顺序依次是 pipe_a, pipe_b,
pipe_c。

这些打算,就是语法(syntax)告诉我们的。根据语法判断输入文件
input.pipe,即杨氏语言的源文件打算做什么,这个过程叫做解析(parser)。

2. decl.g:.g表示这是grammar,确切地说,是tree grammar文件。我们使用了
AST(抽象语法树)来帮助实现pipe.g的语法中指定的那些"打算"。

我们把为了实现这些打算而写的代码,称为动作(action)。

打比方来说。一句话,对于源代码而言,通常是一个命令,比如"吃饭"。这是个
祈使句。

通听懂"吃饭",分解为 动词吃 和 宾语名词饭,这就是语法分析。用什么样的动
作才能实现吃这个动作,怎么把饭作为动词吃执行的对象,这就是动作需要指定
的。杨氏语言的编译器,就像一台机器,语法指定了它能听懂你的指令,而动作
规定了它执行这些命令的措施--移动哪个肢张开多大口--包括这些针对不同规格
的饭的行为。

我们把这些动作--针对语法而执行的语义(semantics)。

3. header.java

语法和语义文件,会经antlr处理生成几个java类,这些类的调用是由
header.java完成的。header.java这个名字也基本是随意起的,之所以称为
header是因为要用于生成头文件,之所以.java,那是因为它就真的是个java源
文件,后来会被编译为.class。

这个比较简单,基本是套路的,抄来改吧改吧就能用。

4. input.pipe

就是它:

mario:
pipe_a 123 | pipe_b | pipe_c

peach:
stage_1 123 | stage_2

bowser:
lose_1 123 | lose_2 | lose_3 | lose_4 234

我们的杨氏语言编译器读了这个输入的杨氏语言源代码以后,会生成三个类,分
别是mario,即马利,peach,那个公主,还有bowser,乌龟壳boss。它们分别有
两三四个类,如上所示。估计你完全能看懂,这比C++简单多了。

5. go.sh

这个是脚本,是用来调用header.java的,及一些前期处理工作,删除以前的生成
结果啦,建个工作目录啦,编译那些java文件(antlr从我们.g里生成出来的,还
有header.java)啦啥的。对了,它还会把 input.pipe 作为 杨氏语言编译器的
输入,并且编译杨氏语言编译器输出的C++代码,然后执行一下。

脚本的动机是,我改一下.g文件,然后运行一次go.sh,就能自动地把上述工作完
成。顺便说一句为什么非得有个自动的东西,而不是手动执行那几行命令--因为
真的要执行非常非常多次。.g文件,也就是整个事情的核心,非常地不容易写。
非常不容易写的原因,如某位外国友人程序员说的:antlr的报错,也就是对.g处
理的报错信息,就像加过密一样难读。

我们之所以还要容忍它的原因,是因为同时它也真的很富有生产力。

6. header.stg:String Template Group,模板(组)。

header.stg源代码中有的地方看起来像这样:

#ifndef _$CLASS_UPPER$_H_
#define _$CLASS_UPPER$_H_

对比一下我们要生成的头文件

#ifndef _MARIO_H_
#define _MARIO_H_

是不是觉得似曾相识?

我们要做的,就是要在.g里把模板载入,然后用从源代码 input.pipe 中解析出
来的 mario 再改成大写,用来代替 CLASS_UPPER,即两个$中间的内容。

当然,实际要替换的东西比这要复杂,尤其是当模板中的某一区域要重复很多
次,而次数和内容取决于源代码 input.pipe 的时候。

我们一共就要生成这些东西。它们之间的关系,也就是整个系统跑起来的原理是:

下面的 "->" 表示数据流,而不是谁变成了谁。有括号"()"的节点,表示程序,
没 "()" 的,表示数据。

1. pipe.g + header.g -> (antlr) -> 一些java文件--parser们

2. input.pipe -> (header.java 调用 parser们) -> .cpp和.h们 + go.cpp

3. go.sh 调用以上过程。

No comments: