- 题外话
昨天看到一个笑话。有个人在论坛上问,为什么车总是不走直路呢。后面一堆问
细节的。楼主又跑上来说,难道不是方向盘上的那个横档是平的,车就应该一直
向前走么。
以方向盘判定车应该走直线,如果方向盘和车轮都不偏的话,那应该是正确的一
种渠道,而且比用眼睛看车轮更间接,也更便利一些。不过,再后面回贴的人提
到:应该眼前向前看,向远处看。换句话说,以车走直线作为车走直线的标准。
写程序也是一样,"难道不应该是...",我们就是以输出结果为准的。现实世界
也是一样,到底牛顿对爱因斯坦对还是玻尔对,判定的标准再直接不过,以事实
为准。
至于说事实如何判定,那就是更深入的另一个问题了。
想起这个笑话,是因为今天安装宜家风格的柜子。宜家风格,就是一堆板子和一
堆螺丝,你自己把它们装配起来。我发现自己看不清螺丝顶部和螺丝刀结合的地
方。用手摸着对上,卡住--对了,螺丝刀能卡住的地方,就是结合正确的位置。
马克思说:实践是检验真理的唯一标准。
近年来,小资老资们对各种东西都开始质疑了。不过,在工程中,实践仍然是检
验真理的唯一标准。车怎么才能开得直呢,当你检验结果是车走直线的时候,你
开得就直了。
当我们生成的代码与我们期望生成的代码一致时,我们就成功了。其他的无论什
么,权威、领导、老师、教科书,说你对了,都不一定是正确的。有人可能问,
为什么我制造与我期待的一致,但是却没有达到我想要的效果呢--比如我们的
method chaining 代码可能编译失败。
其实原因很简单的,你制造的与你期待的一致,但是那却不是你想要的。换句话
说,你还不知道你想要的是什么。
且慢哭泣,你的努力也没有白费,因为你至少知道了,这,不是你想要的。
你可能还想继续问:你想要的是什么。孩子,如果你自己都不知道,我又怎么会
知道呢?
解决方法有很多,但是并非你喜欢的。我当年剧烈头疼的时候发现,喝了酒头就
不疼了;当年抑郁的时候,发现喝了咖啡心情就舒畅多了。所以,当我头疼当我
抑郁的时候,我的解决手段就是喝酒喝咖啡。你该问了,如果一直一直头疼一直
一直抑郁,怎么办呢?那就一直一直喝酒一直一直喝咖啡啊。你可能还会追问,
那得到啥时候是个头啊。某关同学(不是小关,是韩师姐夫)这样解答:喝咖啡
能预防心脏病,为啥哩,因为它能让心脏PENGPENG跳。有人问,那心脏的寿命岂
不是要受到影响?是啊。但是,心脏通常能比人活得更长久,也就是说,你会因
为别的毛病死掉,在这种情况下,为什么还要担心心脏呢。
这也是一种解决之道,当然,对于希望 *一劳永逸* 地解决问题的同学,不太对
胃口。不过,让我告诉你一个事实,一劳永逸根本就是骗局,请回想你高中老师
是怎么对你描述美好的大学生活的。
我们还是继续来看这个简单的问题,antlr+stringtemplate 使用吧。
我们按这样的顺序来介绍:语法,模板,语义,脚本,或者 调用/跑起来的 方
法。因为语法规定了输入,模板规定了输出,这两个更简单和易于观察验证;语
义规定了如何把输入翻译为输出;脚本规定如何把上述这些东西整到一起跑起来。
我们仍然先讨论头文件 .h 的生成。
- 语法
回顾,我们的输入是这样的:
代码1:
1 mario:
2 pipe_a 123 | pipe_b | pipe_c
3
4 peach:
5 stage_1 123 | stage_2
6
7 bowser:
8 lose_1 123 | lose_2 | lose_3 | lose_4 234
它重复了很多次类的声明,类里面有几个方法。这些方法的调用顺序在当前的问
题头文件 .h中是不需要考虑的。
我们把语法在 pipe.g 中规定,这个文件由四部分组成。我们依次来看。
1. grammar
这部分非常短,是这样的,只有一行:
代码2:
1 grammar pipe;
上次我们提到,pipe.g 将由antlr处理,生成一些java源代码,我们把它们叫做
parser们。这些parser用于完成语法解析。这行代码的意思就是告诉 antlr ,我
要生成一个这个东西。
有的同学可能会问,.g文件们本来就是用来描述语法(grammar,又译作文
法)的,为什么还要特别指出要生成它呢,难道还能生成别的么。
是的,antlr能指定 词法lexer, 语法parser, treeparser,和混合的这几种grammar。我
们这里指定的是混合的,既有lerxer,也有parser。
有了这条指令,antlr就将试图把下面的东西作为grammar规定来看待,生成我们
指定的lexer+parser。
2. 头部(?)信息
我不知道应该怎么称呼这一部分,各种选项什么的。如果不指出选项的细节,选
项二字也没有什么意义,我们直接来看细节吧。
代码3:
1 options {
2 output = AST;
3 ASTLabelType=CommonTree;
4 language = Java;
5 }
6
7 tokens {
8 NEXT='|';
9 CLASS;
10 NODE;
11 PARA;
12 }
第1行和第7行,就那么写,分别代表它们英文本身的含义。token是个好玩的词,
极有历史,指法老手里的权杖。学习网络的同学也会觉得它面熟,令牌环网
IEEE802.5。它旁证了计算机专业的大师们是多么地没有文化,好不容易找到个
好词,到处用啊。不像人文类的,连 现如今,为了强调与众不同,都要重新起
个名字,叫做当下。当下啊当下,立马就小资情怀出众了。不是么?你把token
改成更有文化的词试试,antlr立马翻脸不认你,"滚犊子,能不能说人话?"。
第2行,表示我们要把输入变成啥东西,不是变成输出,那还得在挺后面模板那
里才能涉及到。在这一步,我们把输入变成 AST,抽象语法树。
如果简单理解AST,可以想像成语法描述的手段,在AST的结点里,存储着从输入
中不同位置剥离出来的信息--要创造的类的名字啦,它的方法都叫什么名字啦,
有没有参数啦。这些东西,都挂在AST的结点里,所以当你想办法遍历这棵树的
时候,你就看到了那些信息。
关于AST,建议参考两份资料。一份是本书,《编译原理》,随便哪本都有,《龙
书》最佳。另一份是一篇文章,伟大的七格同学的作品《语法树》,是一篇极其
光辉灿烂摇曳多姿的小说。
即然输出类型需要指出可能是AST,当然就还可以是别的什么。如果感兴趣,请
参考antlr手册,或者作者的两本书。官方网站上有提到。
第3行,ASTLabelType=CommonTree; 意思是生成CommonTree,当然也可能是别
的。别的,请参考手册,同上。以后这个请参考手册,同上,就不写全了,我们
简写为 RTFM。这个词不是我杜撰的,其含义请google。
第4行,language = Java; 表示目标语言,就是那些parser们的java代码的语
言是java。你猜对了,还可以是别的语言,比如C,C++啥的。RTFM。关于这个词
的使用,请参见上一段。
以上,RTFM,这个词在某处定义了,然后到处使用,正是编程的核心思想之一,
重用。编译器的存在,其意义也在于此。
3. parser
这一部分就是语法解析的核心了。
代码4:
1 starting
2 : game+
3 ;
4
5 game
6 : SYMBOL_NAME ':' node? ( NEXT node)*
7 -> ^(CLASS SYMBOL_NAME (node)*)
8 ;
9
10 node
11 : SYMBOL_NAME INT? -> ^(NODE SYMBOL_NAME (PARA INT)?)
12 ;
上述代码4,就是对杨氏语言的语法描述。
第1行至第3行,表示:杨氏语言的源文件是由很多个叫做 game的结点组成的。
有多个少这样的结点呢,+,这个符号的意思是 1 个或者更多。还有些别的符
号,*啊,?啊什么的,RTFM。
加号个game形成了一个叫starting的节点,后面我们解析的时候,就要告诉
header.java从这里开始动手。
第2行的冒号和第3行的分号,就这么写。
很多个game组成starting,那么game是什么呢?第5行至第8行回答了这个问题。
5 game
6 : SYMBOL_NAME ':' node? ( NEXT node)*
7 -> ^(CLASS SYMBOL_NAME (node)*)
8 ;
第6行表示:每个game在输入里,都是应该是这样的,先是一个SYMBOL_NAME,然
后跟一个冒号(输入里没引号的),然后是一个叫做node的结点(?表示它可能
存在也可能不存在);接下来是一堆东西 ( NEXT node)* ,*个(即0个或者更
多)NEXT node,其中的node和上述node是同一个东西。
有人说,停,SYMBOL_NAME和NEXT和node都是什么呢?类似于game,后面有定义。
我们一会再谈这个。
继续看,第7行有个有意思的东西。
7 -> ^(CLASS SYMBOL_NAME (node)*)
->,叫做 rewrite,有译作重写。->后面的东西,是我们要把输入变成什么样的
语法树传到输出里。估计你还记得,pipe.g的输出不是最终输出的C++代码,
而是AST。->就规定了这个输出的AST与输入(的语法树)间的对应关系。
为什么要rewrite呢?一个原因是我们希望在后继的解析和语义过程中,语法树能
以一种更方便我们(杨氏语言编译器程序员)一些,而不是更方便盟友(杨氏语
言源代码程序员)。我们在->之前的语法,是为了盟友提供服务的,要尽可能让
他们用起来方便,就是 input.pipe 的样子;这里,通过 -> 改变成方便我们工
作的形式。有时,我们还可以舍弃一些没用的节点,或者添上一些 虚的
(imaginary)结点。第7行中的CLASS就是一个虚的节点,有时候需要用虚结点来
区别语法树相同的规则--即两条规则都使用了相同的语法树。
对了,补充,类似第1第至第3行,或者类似第5行至第8行,这样的条目,我们称
为规则(rule)。
4. lexer
上面提到,还有些东西没有定义。比如SYMBOL_NAME和NEXT和node。node已经在
parser部分第10行定义了,与前两条规则没啥区别。
SYMBOL_NAME和NEXT不太一样,一个曲型的特征就是它们是全大写的。它们放在
lexer部分,称为token。
代码5:
1 SYMBOL_NAME
2 : ('A'..'Z'|'a'..'z'|'_') ('A'..'Z'|'a'..'z'|'_'|'0'..'9')*
3 ;
4
5 WS
6 : (' '|'\t'|'\n'|'\r')+ {$channel = HIDDEN;}
7 ;
8
9 INT
10 : ('0'..'9')+
11 ;
第1行至第3行表示:定义SYMBOL_NAME。SYMBOL_NAME是大小写字母或下划线开头,
后面接*个大小写字母或下划线或数字。就是C语言变量或函数名(合称symbol
name)的规范。
第5行至第7行,定义了要跳过的符号,空格,tab什么的。
第9行至第11行,定义了parser部分引用的一个token,整形数据INT。为了简单,我
们的目标代码,如果有参数,就只传int的,且只有一个。
语法(parser)和词法(lexer)看起来差不多。因为一些机制上的不同,所以
要分开对待。细节,RTFM。
- 模板
接下来我们针对输出的结果写一个模板文件。我放在了工作目录下的st目录下,
头文件生成要用的模板是 header.stg。
.stg只有三部分,在简单的案例中,甚至可以紧缩成一部分。
1. 头部。头部这个名字也是我瞎起的,不知道手册里叫做什么。
代码6:
1 delimiters "$", "$"
也只有一行。告诉stringtemplate,不是告诉antlr,我们要用$作为开始一个占
位符的标志,也用$作为结束一个占位符的标志。占位符这个词我们此前提到过,要
准备用一个变量去填充的东西。
2. 正文
如果紧缩为一个部分,这部分是必须有的。它规定了我们打算输出什么样的东
西,架子,以及放在架子某个位置的占位符。
代码7:
1 class_delc(CLASS_UPPER, CLASS_NAME, member_function_list) ::= <<
2 #ifndef _$CLASS_UPPER$_H_
3 #define _$CLASS_UPPER$_H_
4
5 #include <iostream>
6
7 class $CLASS_NAME$
8 {
9 private:
10 int data;
11
12 public:
13 $member_function_list:member_function(); separator="\n"$
14 $CLASS_NAME$();
15 ~$CLASS_NAME$();
16 };
17
18
19
No comments:
Post a Comment