打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(6)
2011-12-05 22:42 1472人阅读 评论(2) 收藏 举报
使用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:
4
5 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);

10         pipeParser parser = new pipeParser(tokens);
11         pipeParser.starting_return r = parser.starting(); // launch parsing
12         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) (NODE
pipe_c)) (CLASS peach (NODE stage_1 PARA 123) (NODE stage_2)) (CLASS
bowser (NODE lose_1 PARA 123) (NODE lose_2) (NODE lose_3) (NODE lose_4
PARA 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         // walker
17         try
18         {
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 tree
parser test rig is that, in order to access the text for a tree, you
must 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 cleaning
2 rm -rf output && \
3 rm -rf method_chaining_demo && \
4 echo mkdir && \
5 mkdir output && \
6 mkdir output/classes && \
7 mkdir method_chaining_demo && \

9 echo header file generating
10 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: mario
I am peach, created in: peach
I am bowser, created in: bowser
I am running in pipe_a
data: 123
I am running in pipe_b
I am running in pipe_c
I am running in stage_1
data: 123
I am running in stage_2
I am running in lose_1
data: 123
I am running in lose_2
I am running in lose_3
I am running in lose_4
data: 234
I am mario, and game is over in: ~mario
I am peach, and game is over in: ~peach
I am bowser, and game is over in: ~bowser


这像一首诗或者歌曲,让我想起另一个宣言 "I'm youth, I'm joy"。少年总会
成长,承担起责任。不是保护公主,而是其他的什么人。


承担责任,也不是念两句诗,或者唱几句歌,甚至也不是声明 我愿意为你承受何
种苦难。


承担责任,是虽然这些日子没有什么不同,但是如果没有你的工作,这些日子将
非常不同,非常糟烂;承担责任,是拿起工具,开几亩自留地,种上土豆白菜。


这样的工具,能让你使某些人的世界不同的,有很多。其中有两种,分别叫做
antlr 和 stringtemplate。


祝你开垦顺利,有收获。
更多0
0
0
查看评论
2楼 younggift 2011-12-06 00:12发表 [回复]
所有代码:http://antlr.org/share/1323101639207/antlr.zip
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
hibernate HSQL语法文件分析(连载)[CowNew开源社区]
ANTLR笔记1
StringTemplate权威指南(1) ? Alice And Bird
ANTLR——安装配置 - 6DAN - 博客园
错误java.lang.NoSuchMethodError: antlr.collecti...
How To Build a Yacc
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服