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

使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(6)
- 题外话
在森林里迷路的时候,我们希望手里有一张地图,还要有个指南针。我们心里有一个目标,要到那里去。
这是很多人首先想到的。还缺什么呢?还缺少我们当前的位置。你只有知道到自己在哪里,才能接下来的步骤。
我们要开发一个杨氏语言编译器,用 input.pipe 中的那些指令,生成C++代码。在这条路上,我们已经走了多远。
我们根据输入的格式确定了语法 pipe.g,根据输出的格式确定了模板st/header.stg,根据语法制导写翻译出了语义动作
decl.g。我们在decl.g中应用了模板。
接下来,我们需要一个东西,它能够把 调用 pipe.g 和  decl.g,并且输入文件 pipe.g 输给它们。严格的说,被调用的不是
*.g,而是antlr由 *.g 生成的词法解析、语法解析、AST遍历的java程序。
这个推动大跑的东西,可以名之为 driver,它是个java程序。
- driver java
这个程序是用于header生成的,所以我们称之为 header.java。你可能还记得,我们不只要生成头文件,还有cpp和go.cpp。
代码不复杂,但是略微有点长,我们分成三段来看。
-- 头部
我得承认,我没有命名的天份。除了头部,我还是想不出什么名字称呼这一段。在java中,它应该有专门的术语吧。
代码1:1 import java.io.*;2 import org.antlr.runtime.*;3 import
org.antlr.runtime.tree.*;
因为我们要在程序里用到这些类,所以import进来。这是常规的java写法。
题外话:有的时候,我们因为初涉足一个全新的领域,动物本能让我们保持恐惧和谨慎。在进化中,这具有优势,凡是连那是什么都不知道,就敢去碰敢吃的家伙,都年纪轻轻时候就死掉了,没有机会成为我们的祖先。所以,我们每个人的身上都保留了这样的特质。但是在学习中,有的时候,恐惧和谨慎可能过了头,阻碍我们。
我初中的时候参加数字竞赛培训。通化的初中分为山上片和山下片,山下片--我不记得那个时候的术语了--山下片的的生源较好,或者说那是富人区。我惊恐地看到老师才把题写到黑板上,有的学校的同学答案就出来了。这令我震恐。你可以想像一个非常非常难,你一辈子可能都编不出来的程序,一位大牛抽着烟喝着茶,可能还看着碟,谈笑间就写出来了。当你佩服得五体投地时,他说:没啥,就是个小小地练习。
这就是我当时的感觉。后来我看到老师写了一个式子,要因式分解的:
a^2 - b^2
全班同学瞬间就解出来啦。(a+b)(a-b)。而我完全不知道他们是怎么解出来的。我毛了,小声问旁边的同学,"这是咋整出来的啊。"如果我现在不问,老师马上就讲过去啦。
他说:这非常简单。
是的,那确实非常简单,是因式分解中最简单的公式之一,叫平方差公式,就是这个公式本身,不是灵活应用。
你明白我的意思了。恐惧,阻碍我们思考,让我们不敢假设。
其实上面的那些import就是java本身,因为我们正写的,就是java程序。纯正的,不是.g文件中的。我这么说的意思就是:.g文件的那些{}中的动作,也不过就是java程序而已,只是出现的位置略有些奇怪。如果你知道它们会在什么时候执行,就与java无异。
-- 词法和语法
接下来,我们在一个类 header 里跑 main函数。
代码2:45 public class header {6     public static void main(String
args[]) throws Exception {7         pipeLexer lex = new pipeLexer(new
ANTLRFileStream(args[0]));8         CommonTokenStream tokens = new
CommonTokenStream(lex);9 10         pipeParser parser = new
pipeParser(tokens);11         pipeParser.starting_return r =
parser.starting(); // launch parsing12         if ( r!=null )
System.out.println("parser tree:
"+((CommonTree)r.tree).toStringTree());13 14
System.out.println("---------------");15
这个main函数的前半段,如上所述。
第7行,我们构造了一个 词法分析器。
7         pipeLexer lex = new pipeLexer(new ANTLRFileStream(args[0]));
其中 pipeLexer 这个类的名字是这么来的:pipe是我们的grammar的名字,参见pipe.g(请参考昨天博客里的pipe.g源代码。);
Lexer是词法分析器的意思。
new ANTLRFileStream(args[0]) 的意思,是以此作为词法分析器的输入。
我们用这个lexer做什么呢?
8         CommonTokenStream tokens = new CommonTokenStream(lex);
我们用它作为参数,构造了一个 CommonTokenStream。token 的流。
这个流用来做什么呢?
10         pipeParser parser = new pipeParser(tokens);
我们用这个流构造了 pipeParser,这是一个
(语法的)解析器。类似pipeLexer,pipeParser的名字由两部分组成:pipe是grammar的名字,Parser是解析器。
pipeLexer,pipeParser这两个类的名字,是antlr处理pipe.g时生成的两个类。就是我这一篇博客上面提到的
"而是antlr由 *.g 生成的词法解析、语法解析"。
当终于沿 输入文件 input.pipe (即new
ANTLRFileStream(args[0]))、词法分析器pipeLexer、语法解析器pipeParser这条线走到这里,我们就可以调用语法解析器了。
11 pipeParser.starting_return r = parser.starting(); // launch parsing
我们调用了parser。调用的方法是
parser.starting()。starting()这个名字,来自我们在pipe.g中的一条规则的名字,starting。请参考昨天博客里的pipe.g源代码。
parser.starting()的返回值的类型 pipeParser.starting_return,其中starting_return
的命名,就是规则 starting 加 下划线 _,再加上 return。
以上这些命名规则,是 antlr 约定的。由antlr处理 .g 文件后,生成的lexer& parser 将遵循这样的规则,我们也遵循这样的规则来调用。
这个世界遵循两类规则。一种是强制性的。比如,如果你的C代码写得不符合C编译处标准,它就啪地给你个错误,然后甩脸子不干了。还有传说故事里的美国交警拦住你的车,要求你出示驾照,你要是敢醉么哈的冲过去,还敢动武把超啥的,他就可能会一枪把你撂倒。这是强制性的规则,有些是自然的法则,有些是人为的。
还有一种规则,是约定,即使你违反了,没有严重后果的时候似乎也没有惩罚。比如当红灯亮起,如果车辆还是强行压过斑马线,如果没有行人,也没有其他车辆,也没有摄像头和交警叔叔,那么,似乎,什么也不会发生。似乎。我们考试都做过弊,可能你没有,我有。我们口口声声说这于人无害,只要监考老师对我们仁慈一些就可以了。我们并非于人无害,这个世界上,于己有益,却于人无害的事情不多--罗素的观点,大致,你拥有很多数学知道是无害的。当我们作弊,我们无疑地伤害了没有作弊的那些同学。更严重的,我们破坏了规则。前面我说了,我也做过弊,之所以这么说的意思就是,即使我也做过,也并不意味着这件事就是正确的。
antlr的约定,大致类似于第二种。你没有遵循约定,它似乎也没有什么抱怨的。事实上,不是。它只是以另一种方式抱怨,它不工作,或者说,它不按你想像的方式工作。
当我们不认真对待代码,她也将以相同的方式回报你。君视民如草芥,民当视君如寇仇。然后我们只能感叹德国人如何如何,中国人如何如何,好像能把自己摘出去,中国人里没有你我一份似的。
如果你前面全都按 antlr 的规则,那么现在,你可以得到结果了。
12 if ( r!=null ) System.out.println("parser tree:
"+((CommonTree)r.tree).toStringTree());
那个 (CommonTree)r 里的 r,就是刚刚的规则返回值 starting_return 。它是一棵AST。为什么?因为我们在
pipe.g 里面写着 output = AST,请参见昨天博客里的 pipe.g。这不是 delc.g 里的同一条语句,还没到它。
第12行的意思是,把 pipe.g (严格地说,antlr用它生成的 lexer & parser)处理输入 input.pipe
的结果,那棵AST,转化为 toStringTree() 打印到控制台上。
我之所以写这一条语句的目的,是检查解析输入文件是否正确。
我输入了
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
header.java运行到此处,我得到了:
parser tree: (CLASS mario (NODE pipe_a PARA 123) (NODE pipe_b)
(NODEpipe_c)) (CLASS peach (NODE stage_1 PARA 123) (NODE stage_2))
(CLASSbowser (NODE lose_1 PARA 123) (NODE lose_2) (NODE lose_3) (NODE
lose_4PARA 234))
我们看到了那些大写字母,它们是 imaginary tokens,在pipe.g中定义的。
有的同学可能发现,这里为什么没有NEXT,我们明明在 pipe.g 中定义了它,
    NEXT='|';
而且,在输入文件中,我们看到了那些非常明显的 |。
因为,此处我们得到的,是 pipe.g 的输出树,而不是解析时的树。它的输出树,应用了 rewrite 规则,我们整理了这棵树,把 |
这样不携带信息的结点砍掉了。有了AST,我们可以通过结点在树中的位置确定它的语法功能,进而决定语义, | 就没有必要存在了。以下是
pipe.g 中的一段,供懒人同学们查看。我之所以没有总是贴上引用的代码,是因为那会打乱我们叙述的线索。
game    : SYMBOL_NAME ':' node? ( NEXT node)*         -> ^(CLASS
SYMBOL_NAME (node)*)    ;
以上,我们完成了词法分析和语法解析,得到了AST。这棵抽象树,就供下面的步骤遍历,并在遍历过程中执行语义。
-- 语义
在 decl.g 中描述语义很复杂,但是调用则简单的多。
代码3:16         // walker17         try18         {19
CommonTreeNodeStream nodes = new
CommonTreeNodeStream((CommonTree)r.tree);20
nodes.setTokenStream(tokens);21             decl walker = new
decl(nodes);22             walker.starting();23         }24
catch (RecognitionException e) { 25             System.err.println(e);
26         }27 28     }29 }
我们从前往后看。
第19行,我们由AST构造出了 节点的流。
19 CommonTreeNodeStream nodes = new CommonTreeNodeStream((CommonTree)r.tree);
第20行,我们指定,这个 节点的流 里的 tokens 将使用 tokens
20             nodes.setTokenStream(tokens);
这里的 tokens,就是在代码2第8行里定义的那个。为什么需要这一步呢?
回顾代码2和代码3,我们生成这些东西的流程:
 args[0](即 input.pipe) -> lex -> tokens -> parser -> r -> nodes
注意,nodes 是由 tokens 间接生成的。既然 r 是由 tokens 生成的,那么 r中原本就应该包含 token
的信息,为什么还要多余地再设置由 r 而来的 nodes的 tokenstream呢?
antlr的作者在 The Definitive ANTLR Reference 一书中这样说:
"The one key piece that is different from a usual parser and
treeparser test rig is that, in order to access the text for a tree,
youmust tell the tree node stream where it can find the token stream:"
我猜测可能在上述生成的流程中,tokens的信息被抛弃了。这一猜测是否正确,感谢哪位老师同学指点。
不过,我注释了第20行,似乎也没有什么改变,运行结果没有什么不同。也许,新的版本中,tokens信息始终携带着?
我们得到了由AST构造出的stream,接下来,我们要遍历它了,并在遍历的过程中动作。
第21行,我们用 nodes 这个 tree nodes stream 构造出一个遍历器-- walker。
21             decl walker = new decl(nodes);
你注意到了,这个 walker 的类型是 decl,这个名字从 decl.g 中的 grammar的名字而来,它被声明为 tree
grammar。请参见昨天博客中的 decl.g 。
然后,我们用这个遍历器开始:
22             walker.starting();
startinging() 的名字来自 decl.g 中的一条规则。请参见昨天博客中的decl.g 。
从 starting 开始,遍历AST,然后在遍历的过程中,执行语义动作。
有的同学可能会问,在以上java代码中,动作在哪里?动作在 decl.g 的动作部分中。当遍历 starting
这个树枝(也就是根)时,动作同时执行着。这,就是那些动作被调用的时机,解析或遍历到特定的结点,动作就开始执行。
你是不是想起了 龙书 里如何表示动作的位置。
以上,这个 header.java 调用了 *.g 产生的 *.java(里的类和方法),一边解析 input.pipe
(或遍历树),一边做着这个杨氏语言源代码要求的动作。
我们看到,一台大机器在精确地运行,输入 input.pipe 中的字符,不断地转换状态,输出 input.pipe 所规定的产品。
- 脚本,或者 调用/跑起来的 方法
调用antlr把*.g翻译为*.java,编译以上的*.java和header.java,编译并运行得到的那些c++代码,这些动作在写编译器的时候,会不断地重复。
会不断重复很多次的动作,我们应该写个程序来完成。换句话说,我们描述重复很多次的动作并命名它。
实现这个需求的最简单的工具是shell脚本。
题外话,昨天,给同学们看我写的一小段脚本,用来把一个叫做 unicode
的程序输出的东西转换为特定的格式。建一说,windows下也肯定有这样的程序,能求一个字符的 unicode 编号,弹出一个窗口……
那个弹出窗口的程序估计是存在的,它与linux下的这个程序的区别在于,linux下的这个,能用shell非常方便地取出数据,然后加工成另一种形式。易于自动化。弹出窗口那个,你如何取其中的数据呢?用hook么,是的,我们会有很多办法,但是,那是多么地不方便。因为GUI程序特意地关闭了允许你取得输出的途径,它封闭如国内的很多站点,根本就不想提供API供你调用。
张炜同学建议我贴博文的时候,同时提供主博客的URL。我还是犹豫,因为我的主博客在
blogspot,它在这个世界上是不存在的,至少在我看来。是的,我看不到我的博客。我为什么坚持使用呢?因为在那上面发贴子真的非常简单,简单到它支持向某个信箱发封信,那信的正文就是博文。
如果一个人关闭自己的心灵,不喜欢你了解他,还有什么理由抱怨大家不愿意了解和理解他呢。难道他喜欢破门而入或者喜欢各种猜测--还是他所要的不是了解和理解,而仅仅是关注。
Linux,承袭了Unix shell的血统,他一直对你张开怀抱。
代码4:1 echo cleaning2 rm -rf output && 3 rm -rf method_chaining_demo
&& 4 echo mkdir && 5 mkdir output && 6 mkdir output/classes && 7
mkdir method_chaining_demo && 8 9 echo header file generating10 echo
generating code && 11 java -cp
/home/young/Downloads/antlr-3.4-complete-no-antlrv2.jar org.antlr.Tool
-o output pipe.g decl.g && 12 echo compiling lexer and parser && 13
javac output/*.java -cp ~/Downloads/antlr-3.4-complete-no-antlrv2.jar
-d output/classes && 14 echo compiling header.java && 15 javac -cp
/home/young/Downloads/antlr-3.4-complete-no-antlrv2.jar:output/classes
header.java && 16 echo running test.java && 17 java -cp
.:/home/young/Downloads/antlr-3.4-complete-no-antlrv2.jar:output/classes
header input.pipe
第8行以前,是删除前次运行的结果,避免对本次运行造成干扰。
那些 echo 是提醒我他运行到了哪里,避免我担心。你看,他不会一直停在那不动,跟个青春期叛逆少年一样什么也不告诉你。
第11行,11 java -cp
/home/young/Downloads/antlr-3.4-complete-no-antlrv2.jar org.antlr.Tool
-o output pipe.g decl.g && 告诉 antlr 由 pipe.g 和 decl.g 两个文件,生成
*.java,放在 output 目录下。
生成了以下东西:
decl.java  decl.tokens  pipe.tokens  pipeLexer.java  pipeParser.java
你可以根据名字猜测它们的用途,相信你还看到了熟悉的面孔。
第13行,13 javac output/*.java -cp
~/Downloads/antlr-3.4-complete-no-antlrv2.jar -d output/classes &&
编译这些东西。生成一堆 .class。
我把 antlr-3.4-complete-no-antlrv2.jar 放在了 ~/Downloads/
目录下,一个糟糕的选择,它表明我没有良好的组织文件位置的习惯。
"-cp" 是做什么的?请 javac -help ,然后 RTFM。
第15行,15 javac -cp
/home/young/Downloads/antlr-3.4-complete-no-antlrv2.jar:output/classes
header.java &&
编译 header.java,我们今天写的东西。
第17行,跑。17 java -cp
.:/home/young/Downloads/antlr-3.4-complete-no-antlrv2.jar:output/classes
header input.pipe
JVM启动,许多class争先恐后装载进来,header 读入 input.pipe,然后调用那些载入的 class。大机器开动,产品在源代码的指令下生产出来。
- 后记
头文件以外,我们还需要 *.cpp 和 go.cpp 的生成,但是其余的那些,也没有什么不同。就像,当你坐上班车地铁公交,一切日子,看似没有什么不同。
当它们全部生成,我们执行:
g++ -I. *cpp -o go
*.h & *.cpp 被编译链接成了一个可执行程序 go。当我们运行go,它说:
I am mario, created in: marioI am peach, created in: peachI am bowser,
created in: bowserI am running in pipe_adata: 123I am running in
pipe_bI am running in pipe_cI am running in stage_1data: 123I am
running in stage_2I am running in lose_1data: 123I am running in
lose_2I am running in lose_3I am running in lose_4data: 234I am mario,
and game is over in: ~marioI am peach, and game is over in: ~peachI am
bowser, and game is over in: ~bowser
这像一首诗或者歌曲,让我想起另一个宣言 "I'm youth, I'm joy"。少年总会成长,承担起责任。不是保护公主,而是其他的什么人。
承担责任,也不是念两句诗,或者唱几句歌,甚至也不是声明 我愿意为你承受何种苦难。
承担责任,是虽然这些日子没有什么不同,但是如果没有你的工作,这些日子将非常不同,非常糟烂;承担责任,是拿起工具,开几亩自留地,种上土豆白菜。
这样的工具,能让你使某些人的世界不同的,有很多。其中有两种,分别叫做antlr 和 stringtemplate。
祝你开垦顺利,有收获。

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

使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(5)- 一个悲惨的开始这个悲惨的开始是我今天的生活,而不是 antrl+stringtemplate。如果你从头
看到这里,最苦的地方已经过去了。昨天一点或两点睡的时候,心里想,这早晨6:40起来赶班车,对付吧。结果没睡
着,3:17,爬起来写了半张白板,又睡下,又爬起来写了不少。早晨,如果我没
记错的话,第一次,我没有听到闹表。后来复查了一下设置,没错,它响了,我
没有听到。约的是9点,8:55,孙同学电话来,她到应化所了。我当时头脑蒙
着,对话大致是这样的:啊,你去那干什么。应该在软件所啊。你回来吧。不用
急,我也得迟到。结果,迟到26分钟。屋子不大,但是有一屋子人坐好了等你。唉。我们之悲惨缘于很多因素,其中之一希望一蹴而就。我们希望开个企业,然后它
自己就能赚钱了,再也不用管;我们希望上了大学,然后就能玩了;我们希望在
什么什么之前表现得非常怎么怎么的,然后以后就可以再也不怎么怎么的了。后来发现,由于阻尼的存在,理想的无磨擦世界竟然是不存在的。我也也希望一下子就把什么学明白,或者得到什么结果。坐着抻懒腰也能减肥
啦,在家里玩也能赚钱啦,吃点什么病就好了,从此不再复发啦。而现实世界的规律有时原本就复杂,或者简单的那些规律,比如麦克斯韦四个方
程、爱因斯坦的质能方程,这些简单的规律却需要复杂的基础才能理解。我们往
往忘了,理解这些规律的复杂基础,这一工作本身也异常艰苦。编译器生成器 antlr 的使用也是如此。- 上次忘了的在语法解析中,其中有个符号 ^ ,它是AST的根。至于什么是AST,什么是
根,RTMF。我提到这个概念的动机,就是想说它时挺重要,你可能会用到。另外,我们写的语法解析的表达式,叫 EBNF,即 扩展的巴克斯-诺尔范式。编
译原理书中提到。- 语义我们用语法匹配了输入的源代码,接下来,就要在匹配的时候做点什么。这就是
语义。我们仍然以生成头文件为例。语义也写在一个.g文件里,叫decl.g,后面会和昨天我们写的 pipe.g 一起由
antlr一起生成为杨氏语言的编译器。因为anltr用于生成编译器,所以是编译器
生成器。之所以在使用语义的decl.g文件时还需要pipe.g这个语法文件,是因为我们要使
用pipe.g中的语法规则。decl.g和pipe.g共享一些语法规则,decl.g利用这些语
法规则为纲,在匹配到某个结点的时候执行特定的动作,这称为 语法制导的翻
译。语法制导,就是因为沿语法树遍历结点。这个语法树,即AST,pipe.g(严格的
说,由它生成的编译器的parser部分)的输出。我们来看decl.g的内容。-- 头部我也是词汇贫乏,想不起来什么新词,还是叫头部吧。手册里可能有专门的名字
人,但是我猜距离优雅应该同样很远。代码1:
1 tree grammar decl;
2
3 options {
4 tokenVocab=pipe;
5 output=AST;
6 ASTLabelType=CommonTree;
7 }
8
9
10 @header {
11 import org.stringtemplate.v4.*;
12 import java.util.HashMap;
13 import java.io.FileWriter;
14 }第1行,表示这是grammar,并且,这是用来遍历树的,不是lex or parser。这里的decl必须与文件同名,也就是说 tree grammar 什么东西必须放在 什么东
西.g文件 里。不然,其实也好解决,antlr会报错。上次忘了,grammar pipe 的那个文件,也同理,要叫做 pipe.g。第3行至第7行,一些配置。其中第4行,表示这一grammar将与pipe共享相同的
token。说到这里,题外话,读技术文章与小说有一处相同:如果你从中间读起,要么读
不懂,需要看前面,要么你已经看过这个故事的电影或者电视剧或者缩写版了。
还有一种可能,就是那个小说非常地好,或者非常得简单。我曾经捡到几张当时
称为大书的小说页面,看了半个下午。后来知道那是 天龙八部,萧峰用拳头慢
慢钻透墙,救段的那个场景。相信如果你看我这篇,从中间看起的话,断断不会
有那个效果,一定如坠五里雾中,除非你是来指点我的。第5行,
5 output=AST;
是很有意思的一行。它告诉antlr,我要输出一个AST。这pipe.g是一样的。后
面,我们会看到,有个东西能遍历AST。这里,因为马上就要有语义,已经是杨氏语言编译器的末端,其实也可以输出别
的东西,比如直接的结果。我们之所以选择AST的原因,请参见
[http://www.cnblogs.com/sonce/archive/2011/03/13/1982555.html],探索
Antlr(Antlr 3.0更新版)。感谢这位牛人教导我明白了AST与SAX/DOM间的类比
关系。谢谢。第10行至第14行,是因为我们的语义动作中要用到这些类,所以import进来。你
猜对了,实现动作的,就是Java语言。-- 规则,语法制导的翻译接下来的部分,就是在语法的指引下,我们来告诉antlr,遇到某些结点或者
token,我们需要做哪些特定动作。代码2:
1 starting : game+ ;这一行简单,简单到与昨天的pipe.g没有任何区别。也就是说,遇到starting的
时候,解析为+个game,然后呢,啥也不错,没有动作。接下来是规则game,这段相当之长,请有心理准备。我把它拆成了几段来介绍。--- 开始之前代码3:
1 //^(CLASS SYMBOL_NAME (node)*)
2 game
3 :
4 {
5 STGroup header = new STGroupFile("st/header.stg");
6 ST class_delc = header.getInstanceOf("class_delc");
7 }
8 ^(CLASS SYMBOL_NAME
9 {
10 class_delc.add("CLASS_NAME", $SYMBOL_NAME.text);
11 class_delc.add("CLASS_UPPER",
$SYMBOL_NAME.text.toUpperCase());
12 }第1行是用来我自己备忘的。下边的动作把语法打得七零八碎的,不然我根本记
不住自己正写的动作匹配的是哪个结点。1 //^(CLASS SYMBOL_NAME (node)*)
这一行,刚好就是pipe.g的game规则的rewrite规则。如果你还记着的话。不,
你十有八九不会记得,你得翻回昨天的博客去看pipe.g的game规则。这里,就匹配pipe.g的AST输出的东西。{} 里面的东西,就是动作;{} 以外的,就是被折散了的这个东西:
1 //^(CLASS SYMBOL_NAME (node)*)第4行至第7行意思是,在开始匹配子结点以前,初始化模板相关的东西。模板,
就是StringTemplate。5 STGroup header = new STGroupFile("st/header.stg");
6 ST class_delc = header.getInstanceOf("class_delc");第5行,从文件 st/header.stg 中载入 string template group。文件
st/header.stg 的内容和解释,请参见昨天的博客。第6行,我们要使用这个 string template group 中的 class_delc 模板。这一
模板的定义和解释,请参见昨天的博客。--- 一个简单的动作接下来,
8 ^(CLASS SYMBOL_NAME
9 {
10 class_delc.add("CLASS_NAME", $SYMBOL_NAME.text);
11 class_delc.add("CLASS_UPPER",
$SYMBOL_NAME.text.toUpperCase());
12 }这是我们遇到的第一个真正的动作,语法导制下的语义。CLASS 结点是一个imaginary结点,有印象没?参见……当我们遇到SYMBOL_NAME结点的时候,我们要执行第10行至第11行的动作。这个动作的意义是,向模板 class_delc(它是谁呢,看上面第6行)中填加变
量,这个变量的名字叫做 CLASS_NAME,它的值是$SYMBOL_NAME.text,即
SYMBOL_NAME 这个结点的文本。比如 mario。CLASS_NAME,参见昨天的博客中 header.stg 文件中的 class_delc。你是不是
把今天和昨天的博客都打开来对比着往下行进呢?我也在这么做。如果你也是的
话,请感慨一下,这个世界没有多少记忆超群的人,至少你我不是。握手。11 class_delc.add("CLASS_UPPER", $SYMBOL_NAME.text.toUpperCase());这行就简单了,我们要再加入一个变量--你可能已经找到了,模板class_delc有
三个参数,还有一个在后面--这第二个变量是CLASS_UPPER,值是
$SYMBOL_NAME.text.toUpperCase()。$SYMBOL_NAME.text是一个String,所以toUpperCase()可以RTFM
[http://docs.oracle.com/javase/1.5.0/docs/api/java/lang/String.html#toUpperCase()]
。有的同学已经想到了,有时还可以把这个text转成int,转成float。此外,有的同学可能也注意到了,加入变量的时候,我们总是关注两个东西:变
量的名字,变量的值,这就像map,键 和 值。以上,就是语义动作。没有看到输出?因为我们的动作,就是把一些变量放到模
板里,后面统一渲染。没错,还是这个小资词汇,render。那个时候,我们就得
到了真正的输出。我们为什么不直接输出,而要使用stringtemplate这么个间接的东西呢?跟我们
不用CGI perl的道理是一样的,因为很多要输出的东西,是固定的,而要填充的
东西,那些占位符,只是其中的少数。我们不想把固定的东西放在程序的逻辑中
输出。--- 一个复杂的动作,带有聚合的,应用模板于变量之上接下来,我们遇到了一个复杂的语义动作。代码4:
13 (node
14 {
15 HashMap mf = new HashMap();
16 mf.put("class_name", $SYMBOL_NAME.text);
17 mf.put("function_name", $node.node_name);
18 mf.put("para_name", $node.para_name);
19 class_delc.add("member_function_list", mf);
20 }
21 )*)看第19行,我们也是加入了一个变量,名为 member_function_list 。也许你还
记得,这本身就是一个模板(函数),我们要 apply that template on 这个传
入的变量的值上。那个函数只有一个参数,就是mf;而在那个函数中,我引用了
这个参数的成员。代码4.1
1 member_function (mf) ::= <<
2 $mf.class_name$* $mf.function_name$($mf.para_name$);
3 >>第15行,我们建立一个HashMap,它有来形成聚合(aggregation),也就是说,
传一个对象,就是mf,进去。我们看到,这个对象有三个成员,
16 mf.put("class_name", $SYMBOL_NAME.text);
17 mf.put("function_name", $node.node_name);
18 mf.put("para_name", $node.para_name);
刚好与代码4.1,也就是header.stg中的函数里引用的变量的成员对应。19 class_delc.add("member_function_list", mf);
我们把做好的这个对象做为变量加进去。以上,是一个复杂的动作,带有聚合的,需要应用模板(函数)的。--- 渲染当我们把模板中所有的变量都赋了值,就可以渲染模板了。代码5:
22 {
23 String result = class_delc.render();
24 System.out.println(result);
25
26 try{
27 FileWriter fw = new
FileWriter("method_chaining_demo/"+$SYMBOL_NAME.text+".h");
28 fw.write(result);
29 fw.flush();
30 }
31 catch (java.io.IOException e)
32 {
33 System.err.println(e);
34 }
35 }
36 ;第23行,渲染模板。渲染这个词挺优雅的,实质就是得到一个字符串--把模板里
的占位符,那些洞,都用变量填上,然后把模板作为字符串返回来。第24行,把这个字符串输出到控制台。第26至第35行,是把这个字符串输出到磁盘文件中,并以
$SYMBOL_NAME.text+".h" 作为文件名。这里的$SYMBOL_NAME,根据语法
1 //^(CLASS SYMBOL_NAME (node)*)
正是 类名mario。之所以要操作文件的原因,是因为我还要生成 cpp,要生成go.cpp(driver),并
且不希望自动为输出文件命名,而不希望由 go.sh 负责命名。我们以上为模板中的占位符赋值了变量。事实上,即使不对任何变量赋值,也可
以渲染模板。stringtempalte会认为那些变量都是null,直接跳过,输出占位符
没有被代换的模板。--- 语义动作的返回值在 antlr 中,动作还可以有返回值。我们在上面的代码6的第17行和第18行,引
用过node规则的返回值。17 mf.put("function_name", $node.node_name);
18 mf.put("para_name", $node.para_name);接下来,是node规则的动作。代码6:
1 //^(NODE SYMBOL_NAME (PARA INT)?)
2 node
3 returns [String node_name, String para_name]
4 @init {
5 $node_name = "";
6 $para_name = "";
7 }
8 :
9 ^(NODE SYMBOL_NAME (PARA INT
10 {
11 $para_name = "int par";
12 }
13 )?)
14 {
15 $node_name=$SYMBOL_NAME.text;
16 }
17 ;第3行,表示 返回值分别为 String node_name, String para_name。当在上一
级规则中引用的时候,我们就使用 $node.node_name.text,
$node.para_name.text 这样的形式。是的, antlr的规则可以有多个返回值,这与C/C++不太一样。第4行至第7行,是初始化部分,在匹配这条规则之前执行。这与前面代码3中的第
4行至第7行的不同之处在于,代码3是执行于某个 alternative(若干个由 | 分
隔开的规则匹配"路径") 之前,而这里的初始化,是在整个规则所有的
alternative之前。我们在初始化部分中把要返回值赋值为空串了。也可以赋值为报错信息,如果在
语义动作中没有正确赋值,就报错。9 ^(NODE SYMBOL_NAME (PARA INT
10 {
11 $para_name = "int par";
12 }
13 )?)第9行至第13行的动作,是当 (PARA INT)? 存在的时候执行的,因为我们把动作
放在了这个位置: (PARA INT 这个位置 )?这完成了一个逻辑判断,即 只有当参数 PARA INT 存在的时候,才会对
$para_name 赋值。这样,当输入的杨氏语言源代码中有参数时,函数的声明就有
参数;当源代码中没有参数时,$para_name 就是空串,模板渲染以后,在函数
的参数列表里什么也没有。14 {
15 $node_name=$SYMBOL_NAME.text;
16 }这个动作,不同于带?的部分,只要匹配了 node 这条规则,就一定会在最后执
行--为 $node_name 赋值。这就是目标代码中的函数名。今天又整了这么多,相信你也累了。明天继续,将介绍header.java将如何调用
lexer, paser, and tree walker,也将介绍脚本如何编译和运行一切。附录 以上涉及到的源代码1. st/header.stgdelimiters "$", "$"class_delc(CLASS_UPPER, CLASS_NAME, member_function_list) ::= <<
#ifndef _$CLASS_UPPER$_H_
#define _$CLASS_UPPER$_H_#include <iostream>class $CLASS_NAME$
{
private:
int data; public:
$member_function_list:member_function(); separator="n"$
$CLASS_NAME$();
~$CLASS_NAME$();
};

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

使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(4)- 题外话昨天看到一个笑话。有个人在论坛上问,为什么车总是不走直路呢。后面一堆问
细节的。楼主又跑上来说,难道不是方向盘上的那个横档是平的,车就应该一直
向前走么。以方向盘判定车应该走直线,如果方向盘和车轮都不偏的话,那应该是正确的一
种渠道,而且比用眼睛看车轮更间接,也更便利一些。不过,再后面回贴的人提
到:应该眼前向前看,向远处看。换句话说,以车走直线作为车走直线的标准。写程序也是一样,"难道不应该是...",我们就是以输出结果为准的。现实世界
也是一样,到底牛顿对爱因斯坦对还是玻尔对,判定的标准再直接不过,以事实
为准。至于说事实如何判定,那就是更深入的另一个问题了。想起这个笑话,是因为今天安装宜家风格的柜子。宜家风格,就是一堆板子和一
堆螺丝,你自己把它们装配起来。我发现自己看不清螺丝顶部和螺丝刀结合的地
方。用手摸着对上,卡住--对了,螺丝刀能卡住的地方,就是结合正确的位置。马克思说:实践是检验真理的唯一标准。近年来,小资老资们对各种东西都开始质疑了。不过,在工程中,实践仍然是检
验真理的唯一标准。车怎么才能开得直呢,当你检验结果是车走直线的时候,你
开得就直了。当我们生成的代码与我们期望生成的代码一致时,我们就成功了。其他的无论什
么,权威、领导、老师、教科书,说你对了,都不一定是正确的。有人可能问,
为什么我制造与我期待的一致,但是却没有达到我想要的效果呢--比如我们的
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

使用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_cpeach:
stage_1 123 | stage_2bowser:
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.cpp3. go.sh 调用以上过程。

shell助我去战斗

shell助我去战斗客户催进度的消息传来几次,我恨恨地想:再催再催,我就把你们全带去封闭开
发。好吃好喝,不让睡觉,天天干活。我估算了一下,担保不等项目结束,客户全过劳死光,我一定还能幸存。编码间歇看邮件,ZHUMAO同学带来噩耗,他要暂时关闭我们的GIT服务器,得备
份。不少项目都有单儿的 git repository,在ZHUMAO的服务器上中央存储。关键是,不
少,我得把它们全pull一次,有些不紧急的,我已经一段时间没pull了。恩。背影知识:git是一种版本控制系统,版本控制就是你写一大篇文章,中间
的很多过程都要记录下来,留着有用。pull,就是把git服务器上的东西整下来。一个个项目。一个个进入那些目录,pull,然后再进入下一个目录。非常不好。那么,编段程序完成这个任务吧。编程序完成的好处在于:虽然花了时间,可能是等长甚至更长的时间,却可以更
容易保证质量。这好比你打算用蜡烛做十个小兔子,如果一个个用手雕刻,那个每个的质量都是
无关的。如果花时间做个模具,那么,只要有一个小兔子是质量过关的(并且工
艺上,比如温度,控制得当),那么其他所有的小兔子就都是质量过关的。1. 统一规格我找到所有的配置文件,把 remote 和 url (就是远程的git服务及指定上面的
项目)都改成相同的。当然,可以编个awk&sed程序直接改了,但是我没那个功力,且学会了"对付一下
得了,不要么通用"的原则。所以,我找到每一个 不符合 要求的配置文件,然
后手动修改。这么找:: find . -name config | xargs grep -i ".231" -nH-nH参数的作用是打印是哪个文件的哪行命中了。2. 写个程序,遍历目录,挨个pull就这样:1 for i in $(ls -d */)
2 do
3 echo pulling $i
4 cd $i
5 convmv * -r -f utf-8 -t gb2312 --notest > /dev/null 2>&1
6 git pull origin master
7 convmv * -r -f gb2312 -t utf-8 --notest > /dev/null 2>&1
8 cd ..
9 done啥意思呢?第1行,for是个循环,它将令变量i遍历 $() 里的东西,即 ls -d */。ls -d */ 的作用是列出当前目录下所有的目录。第2行至第9行,是循环体。在每次循环时,都完成以下任务。1. 第3行,显示当前正pull谁呢,让我心里有个数。不然,通常我就等不及强制
结束了。2. 第4行,进入以$i这个变量为名的目录。3. 第5行和第7行,是把文件名在utf-8和gb2312间编码转换。原因是:a.祖国尚
未统一世界,万码奔腾的局面还会存在很多年;b.虽然通知了所有的程序员不要
使用中文文件名,大家还是有时候会忘。解释一下"> /dev/null 2>&1"的意思。> /dev/null 是把输出重定向到黑洞里
去,免得烦我。你肯定也有那感觉,多余的信息,不如没有。2>&1 的意思是把错
误信息,即出错时的报错信息也扔到黑洞里去。你看,我连错误也不想看到...跟
我们中的某些人类似。4. 第6行,是核心,也就是 git pull origin master,从origin指向的url中
pull下东西,放到master分支中去。为了核心的,我们真正要做的事,我们做了多少准备工作啊。而且,还有收尾工
作。5. 第8行,收尾,回到上一级目录,以便继续遍历下一个项目。故事再次上演,
没有一点改变。这个故事告诉我们,真正的我们想做的事,往往在一大堆看似无关和无聊的事情
当中。我们需要发现本质,同时,我们也需要有能力完成核心以外的事情。这个故事还告诉我们,第8行,收尾工作非常重要。项目的开始并非开始于项目开
始的时候,而是开始于上一个项目结束的时候。所以,令我们安慰的,如果当前有什么事情无法完成,那不是此刻的你的错误,
而是更早的你积累的结果。非常简单的推论,如果未来的你有什么事情无法完
成,其错肇始于此刻。

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

使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(2)前传,母模 method chaining我们要用 antlr+stringtemplate 开发的东西,是 method chaining 的生成器。
因此,我们得先知道 method chaining 个什么样子。正如当我们想做个蜡烛小东
西的时候,我们得先做出这个小东西的母模来。母模与最终的批量产品看起来样子完全相同,但是,母模是用手工打造的。method chaining 是实现 Fluent interface 的一种手段,这俩在 wikiepdia
上都是条目,详情请自己去查。Fluent interface 是软件工程中的一种方法,希望能让代码更可读。试对比:方案A:o_mario->pipe_a(123);
o_mario->pipe_b();
o_mario->pipe_c();方案B:o_mario->pipe_a(123)->pipe_b()->pipe_c();或者方案C:o_mario->pipe_a(123)
->pipe_b()
->pipe_c();是不是觉得方案B和方案C的可读性更好一些?况且,我们少键入几次o_mario,
也减少了错误的可能。Fluent interface 的提出者是牛人 Martin Fowler。如果你读书不注意作者的
话,可能会不记得他。他的作品包括 分析模式(不是四人帮的设计模式),UML
精粹,重构,Domain-Specific Languages 等。他是敏捷方法、极限编程、UML
和模式领域的专家,还推动了 依赖注入(控制反转)一词的流行 。以上八卦结束,无外乎想说明 method chaining 这技术系出名门,有纯正的民间
血统。Fluent interface 能够让调用这一方法的程序员写出的代码,看起来像是一种
领域定义语言,更专门,也更容易被本领域的专家(或人类)识读。也就是说,
读者可以经受更少的训练即可读懂。凡是具有平易近人特性的东西,似乎也都具有另一个特性,那就是精美的包装。
而包装是需要代价的。不过,我们遵循这样的原则:当你是一个库函数的程序员的时候,库函数的接
口,应该以令使用它的人感到愉悦为原则。你在此时多花费的时间,不仅楚人失
之楚人得之,而且将以十倍百倍在别人那里得到节省。这也正是我们的价值所在。那种认为"我依赖你对你撒娇,那都是看得起你"的
人,可能从来没想过它的反命题,"我不允许你依赖不允许你撒娇,那都是看得起
你",因为把你视为平等的人类而不是低一等级的什么。工程中没有小情小调。你真的以为现在撒娇的你,将来遇到问题的时候能够替大
家挡下风雨么?所以,如果我们是库函数程序员,我们要提供令别人觉得享受的接口去调用。
method chaining就令人愉悦,所以我们要考虑一下怎么实现了。我们的盟友需要的效果是这样的:o_mario->pipe_a(123)->pipe_b()->pipe_c();那么,我们考虑一下实现。1. o_mario->pipe_a(123),第一个函数的调用o_mario(严格的说,是这个指针类型的变量的类类型)需要有一个方法,这
个方法是pipe_a.这个容易,就是这样:代码1:class mario
{
public:
pipe_a(int par);
};以上是头文件中的内容,函数的实现非常简单,先不讨论.2. o_mario->pipe_a(123)->pipe_b(),第二个函数的调用,及第一个函数的声明后面这个pipe_b()是个什么东西呢?这应该是另一个方法。谁的方法呢。从库函数使用者的角度看,它应该是
o_mario->pipe_a(123) 这段代码的返回值 的成员函数。这里有两件重要的事。第一,重申:它应该是o_mario->pipe_a(123) 这段代码的
返回值 的成员函数。因为pipe_b()的前面有 ->,所以无疑的,前面是个类的实
例的指针。看不懂的同学,请回去复习一下C语言中的structure的成员变量如何
引用 和 这个structure的指针如何引用成员变量。第二,一种思维方法。当我们讨化如何实现的时候,我们可以从接口(广义的)
的形势入手,而不从对实现的猜测本身入手。也就是说,我们先明确它应该是个
什么样子,而不是应该如何实现。因为在这里,接口,是动机,要首先明确,而
实现手段,可以有千千万万,我们一个个穷举起来代价比较大。以上两件重要的事讨论完毕,我们再回头来看,这句话有点长:o_mario->pipe_a(123)->pipe_b()中的pipe_b()是 o_mario->pipe_a(123) 这段
代码的返回值 的成员函数。即 第二个函数,是第一个函数的返回值的成员。理解了这一点以后,我们可以再进入一步问,我们需要的是第一个函数返回值的
成员,那么,第一个函数的返回值是什么东西么?它应该是一个类类型(class type)实例的指针。哪一个类类型为好呢? class mario 就非常适合。所以,我们把代码1改一下,确定第一个函数的返回值类型,变成下面这样:代码2:
class mario
{
public:
mario* pipe_a(int par);
};以上是这个函数的"接口",即怎么去调用它。3. 第一个函数的实现我们需要的已经明确,下一步才是如何得到。在这里,我还是想再一次强调,明确自己需要的是什么非常重要,远远比知道如
何去实现要重要。始终坚守并提醒自己想要的是什么,希望能有效地避免走上与
自己的愿望相反的道路。WG同学提到过一个问题,如果把一群人关起来,不让他们了解外部的世界,给他
们好吃好喝,或者教育他们认为自己得到的是最好的,这是否给了他们幸福。这个问题可以用另外的两个似乎无关的事情来回答。一是,有日本人称中国人为支那人,我们很不喜欢,认为受到了侮辱。有人提到
过,那不就是个名字么,为什么认定这是侮辱。提这个问题的人显然没有意识到
这样一个道理:当你认定自己受到了侮辱的时候,那么就是受到了侮辱。这与发
出行为的人的动机甚至关系都不那么紧密。所谓尊重,就是不做对方认为侮辱的
事情。所以前面提到的类似观点"我欺负你正是爱你",可以问问对方,他喜欢这样的爱
么。这类似于百年前的男人问问自己的老婆,我殴打你才是爱你,你接受么?对方不接受的,就不是他想要的。当我们讨论对方的感受时,我们必须讨论对方
的感受,而不是你的感受。你没看错,我说的就是"当我们讨论对方的感受时,我
们必须讨论对方的感受",即,当我们讨论A时,我们必须讨论A。道理朴素到可以归结为 A就是A,但是有些人仍然不懂,因为他们加了很多附加
条件,却无视这些条件都与A无关。WG同学的方案里,如果被关起来的那些人觉得好,那就是好呗。问题是,那些人
*真的*觉得好么?家长包办婚姻(这个词对你来说,是不是史前时代的古拉丁语)的时候对孩子
说,"我都是为了你好啊"。且住,那得由你来判定。第二个回答WG同学的例子。扯淡的。我喜欢一个人,我把他,对不起,我把她捆
起来,不放走,每天喂猴头燕窝鲨鱼翅。最后她终于逃脱,找来一群人要揍我。我可不可以说:至少,你吃到并消化了那么多好东西,那都是我卖血换来的啊。
你如何报偿我呢。这个例子如此浅显,以至于你都愤慨了吧。但是它和WG同学那个看似合理的方案
有一个共同点,即 被捆起来饲养的那位,她愿意吗。愿意,意愿,希望得到的是什么,这非常重要。这比如何实现重要一百倍。以上扯淡结束。我们已知库函数使用者想要的是 代码2。其实,至此我们还没有
做一点自己的库函数开发的工作,只是明确了盟友的需求。应该是这样实现的。代码3,加了行号。1 mario* mario::pipe_a(int par)
2 {
3 std::cout << "I am running in " << __FUNCTION__ << std::endl;
4 this->data = par;
5 std::cout << "data: " << data << std::endl;
6 return this;
7 }其中,第1行就是声明的重复。第3行和第5行,是为了调试的时候方便,能看着点啥。__FUNCTION__ 是编译成
debug版内置的,函数名。第4行,是为了显示效果,有一个成员变量,data,int型
的。第6行是有意思的一行,它的作用,即我们刚刚讨论的,我们的盟友需要的,返
回值。返回值是什么呢?是this指针。它是一个指针,指向了这个类类型的这一个实例。所以,pipe_a 和 pipe_b 是同一个实例(的指针)调用的成员函数,这些方法
的数据将存储在(或读取自)同一个实例中。也正因为同一实例这一点,经常有
人用 method chaining 来初始化类类型的变量。比如这样;
代码4,出自[http://en.wikipedia.org/wiki/Fluent_interface#C.2B.2B] FluentGlutApp(argc, argv)
.withDoubleBuffer().withRGBA().withAlpha().withDepth()
.at(200, 200).across(500, 500)
.named("My OpenGL/GLUT App")
.create();4. o_mario->pipe_a(123)->pipe_b()->pipe_c()我们目前实现到 pipe_b(),容易看出,后面的 pipe_c() 与 pipe_b() 没有区
别。这样,代码5:mario* mario::pipe_c()
{
std::cout << "I am running in " << __FUNCTION__ << std::endl;
return this;
}5. 调用者我们替盟友写一段代码,测试一下是否能工作,然后再交付。交付的时候才发现
很多麻烦没有解决,无论是多小的麻烦,都应该认识到,那是我们的责任,不能
推给库函数使用者去完成,因为他不替你领工资。我们把这段调用库函数的代码称为 driver,推动事情运行的东西。代码6:#include <mario.h>
int main(int argc, char *argv[])
{
mario* o_mario = new mario();
o_mario->pipe_a(123)->pipe_b()->pipe_c();
delete o_mario;
return 0;
}6. 对生成 method chaining 的展望这样,我们需要三个文件:mario.h 头文件,类及其成员的声明;
mario.cpp cpp文件,类及其成员的定义;
go.cpp driver文件,负责调用mario的函数们,即
o_mario->pipe_a(123)->pipe_b()->pipe_c()。7. 重提母模如果我们仅只需要马利这样一个类,那么手写就可以了,手写完交给库函数程序
员成千上万次像driver那样调用.但是我们可能需要很多个这样的类,而它们的结构如此类似。所以,我们需要大
量地成批地生成它们。对于每一个类,我们都需要 .h和.cpp文件各一个,用类的名字命名;所有这些
类,我们还需要一个driver.cpp,调用它们。以上,明确了1.我们的代码需要生成,2.我们需要生成哪些东西,3.这篇博客的
主体,这些生成的代码都应该是什么样子的。8. 附录,那些文件 的代码以下是我们明天要生成的代码。它们现在也可以编译,这样:g++ -I. *.cpp -o go执行的时候:./go此外,从下面的driver.cpp中你可以看出,其实,我已经有了不止mario一个类。
它们都能通过我们接下来要介绍的杨氏语言编译器生成出来。你猜到了,下面的代码,确实就是生成出来的。8.1 mario.h1 #ifndef _MARIO_H_
2 #define _MARIO_H_
3
4 #include <iostream>
5
6 class mario
7 {
8 private:
9 int data;
10
11 public:
12 mario* pipe_a(int par);
13 mario* pipe_b();
14 mario* pipe_c();
15 mario();
16 ~mario();
17 };
18
19
20