使用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;

3 options {
4     tokenVocab=pipe;
5     output=AST;
6     ASTLabelType=CommonTree;
7 }


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.stg


delimiters "$", "$"


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$();
};




#endif /* _$CLASS_UPPER$_H_ */
>>


member_function (mf)  ::= <<
$mf.class_name$* $mf.function_name$($mf.para_name$);
>>


2. pipe.g


// mario:
// pipe_a 123 | pipe_b | pipe_c


grammar pipe;


options {
        output = AST;
        ASTLabelType=CommonTree;
        language = Java;
}


tokens {
    NEXT='|';
    CLASS;
    NODE;
    PARA;
}




starting
    : game+
    ;


game
    : SYMBOL_NAME ':' node? ( NEXT node)* 
        -> ^(CLASS SYMBOL_NAME (node)*)
    ;


node
    : SYMBOL_NAME INT? -> ^(NODE SYMBOL_NAME (PARA INT)?)
    ;


SYMBOL_NAME
: ('A'..'Z'|'a'..'z'|'_') ('A'..'Z'|'a'..'z'|'_'|'0'..'9')*
;


WS
    : (' '|'\t'|'\n'|'\r')+ {$channel = HIDDEN;}
    ;


INT
    : ('0'..'9')+
    ;




3. decl.g


tree grammar decl;


options {
    tokenVocab=pipe;
    output=AST;
    ASTLabelType=CommonTree;
}




@header {
import org.stringtemplate.v4.*;
import java.util.HashMap;
import java.io.FileWriter;
}


starting
    : game+
    ;


//^(CLASS SYMBOL_NAME (node)*)
game
    : 
        {
            STGroup header = new STGroupFile("st/header.stg");
            ST class_delc = header.getInstanceOf("class_delc");
        }
        ^(CLASS SYMBOL_NAME
            {
                class_delc.add("CLASS_NAME", $SYMBOL_NAME.text);
                class_delc.add("CLASS_UPPER", $SYMBOL_NAME.text.toUpperCase());
            }
            (node 
                {
                    HashMap mf = new HashMap();
                    mf.put("class_name", $SYMBOL_NAME.text);
                    mf.put("function_name", $node.node_name);
                    mf.put("para_name", $node.para_name);
                    class_delc.add("member_function_list", mf);
                }
            )*)
        {
            String result = class_delc.render();
            System.out.println(result);


            try{
                FileWriter fw = new FileWriter("method_chaining_demo/"+$SYMBOL_NAME.text+".h");
                fw.write(result);
                fw.flush();
            }
            catch (java.io.IOException e)
                {
                    System.err.println(e); 
                }
        }
    ;


//^(NODE SYMBOL_NAME (PARA INT)?)
node
returns [String node_name, String para_name]
@init {
    $node_name = "";
    $para_name = "";
}
    : 
^(NODE SYMBOL_NAME (PARA INT
                {
                    $para_name = "int par";
                }
            )?)
        {
            $node_name=$SYMBOL_NAME.text;
        }
    ;




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
首先,你需要将antlr生成的AST转换为QuickGraph库所需要的数据结构。QuickGraph库支持使用有向图(Directed Graph)来表示AST,其中每个节点表示AST中的一个语法单元,每条边表示语法单元之间的依赖关系。 下面是一个基本的示例代码,用于将AST转换为QuickGraph数据结构: ```csharp using System.Collections.Generic; using Antlr4.Runtime.Tree; using QuickGraph; using QuickGraph.Graphviz; using QuickGraph.Graphviz.Dot; public static class AstToGraphConverter { public static AdjacencyGraph<string, Edge<string>> Convert(IParseTree ast) { var graph = new AdjacencyGraph<string, Edge<string>>(); // 遍历AST,为每个语法单元创建一个节点 var stack = new Stack<IParseTree>(); stack.Push(ast); while (stack.Any()) { var node = stack.Pop(); var name = node.GetType().Name; graph.AddVertex(name); // 将子节点与父节点连接起来 for (var i = 0; i < node.ChildCount; i++) { var child = node.GetChild(i); var childName = child.GetType().Name; graph.AddVertex(childName); graph.AddEdge(new Edge<string>(name, childName)); stack.Push(child); } } return graph; } } ``` 上述代码使用了QuickGraph库中的AdjacencyGraph类来表示有向图,并使用了Graphviz来将图形可视化。下面是一个示例代码,用于将AST转换为DOT格式并使用Graphviz绘制图形: ```csharp using System.IO; using QuickGraph.Graphviz; var ast = ...; // antlr生成的AST var graph = AstToGraphConverter.Convert(ast); // 将AST转换为DOT格式 var dot = graph.ToGraphviz(new GraphvizAlgorithm()); // 将DOT格式保存到文件中 var dotFile = "ast.dot"; File.WriteAllText(dotFile, dot); // 使用Graphviz将DOT格式转换为图形并保存到文件中 var pngFile = "ast.png"; var renderer = new GraphvizImageRenderer(); using (var dotReader = new StringReader(dot)) using (var pngWriter = new FileStream(pngFile, FileMode.Create)) { renderer.Render(dotReader, pngWriter, GraphvizImageType.Png); } ``` 上述代码将使用Graphviz将AST转换为PNG图像,并保存到文件中。你可以根据需要调整代码以满足你的需求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值