前言
真正意义上的程序员都很懒,懒的连多余的一行代码也不写。
如果能将底层满手油污的活儿都可以交给别人去做,自己就扮演个智囊团成员的角色,生活会比想象中的还要惬意。
严格的按照指令执行长时间不知疲倦的计算是计算机所擅长的事情,那么给它一个代码模板,留些运行时它可以获取的值的占位符,再揉进一些固定套路的指令,程序员的生活也可以很美好。
尽管如此,但工具永远不是软件工程中的银弹(看看人月神话吧),适可而止,让富有攻击性的洞察力指引前进的方向,而在实际工作中持适当的保守性态度。
内容
1 StringTemplate基本概念
2 为什么要将代码/文本生成逻辑和所生成的代码/文本内容区分开来
3 重写器(rewriter)和生成器(generator)
1 StringTemplate基本概念
StringTemplate是一个模板引擎,模板的含义是带有占位符(hole/placeholder)的字符串/文本,属性是传递给模板的参数,用于填充占位符;在模板中可以定义模板表达式,模板表示式可以理解为属性的函数。
StringTemplate将模板划分为文本块和属性表达式,属性表达式的默认形式为,主要在不改变属性表达式界定符(即<>)的情况下,文本块中包含'<'、'>'字符时均需转义为'\<'、'\>'(这一点在文中字节码生成器的示例中没有指出,使得Java字节码中函数无法在模板中定义)。
API概述
详细的内容参见官方文档:StringTemplate4官方在线文档
模板文件后缀名为st,模板组文件为stg
模板组示例(模板的使用与此类似)
//demo.stg decl(type, name, value) ::= " ;" init(v) ::= " = "
decl(type, name, value)、init(v)为模板名和模板参数,为条件表达式(conditions),其它<...>为占位符。
Java解析实例
STGroup group = new STGroupFile("demo.stg");//绝对路径 ST st = group.getInstanceOf("decl");//获取模板实例 st.add("type", "int");//填充占位符 st.add("name", "x"); st.add("value", 0); String result = st.render();//模板输出
2 为什么要将代码/文本生成逻辑和所生成的代码/文本内容区分开来
Parr指出翻译器的最终步骤是从解析产生的内部数据结构中生成结构化文本,翻译器中提交结构化文本的元件称为提交器(emitter)。
根据提交器所提交的文本与输入文本之间的关系,存在两类翻译器:重写器(rewriter)和生成器(generator)。
翻译器中元件抽象:(1)模型:解析输入生成的内部数据结构;(2)视图:提交器输入的结构化文本;(3)控制器:输入解析器和中间表示IR的遍历器。
将提交器与代码生成逻辑和生成中相应的计算显示的区分开来,遵循松耦合的良好软件工程实践:可以在不影响代码生成逻辑和相应计算的前提下,替换提交器提交的文本,而模板引擎的引入极大的便利化这一文本替换操作过程。
3 重写器(rewriter)和生成器(generator)
重写器、生成器的概念
重写器生成的输出与输入很类似,这种翻译器可以直接在输入上直接做修改,典型的应用包括读入源代码文件,做小幅度的修改、插入stub代码用于调试、代码覆盖度和性能度量,Evernote的"悦读"也可以算是重写器。
生成器生成的输入与输入存在很大的不同,典型的应用包括生成一段信息的内容摘要、生成源代码信息的报告、从Java代码中生成接口、从Java类中提取公共接口等。
自然的,重写器也可以理解为生成器。
在动作中引用模板
语法 | 说明 |
%foo(a={}, b={},...) | 模板构建语法:创建模板foo,设置其属性a、b... |
%({<<nameExpr>>}(a={}...)) | 间接模板构造引用,nameExpr是计算出的模板名称,其他部分与模板构造语法相同 |
%x.y=<<z>> | 将模板x的属性y赋值为值z |
%{<<expr>>}.y=<<z>> | 将StringTemplate类型表达式expr的属性赋值为表达式z的值 |
%{<<stringExpr>>} | 从String类型stringExpr创建匿名模板 |
生成器案例
说明:生成Java字节码
工具:jasmin(Jasmin Home Page)
jasmin是最初设计用于"Java虚拟机"一书(该书由Jon Meyer,Troy Downing撰写),是SourceForge上的开源项目。
jasmin是JVM的汇编器,其接受的文件形式是Java字节码的ASCII形式(后缀名j),这种文件是采用JVM指令集以类似于汇编语言风格编写的。jasmin将这类文件转换为Java字节码文件,可直接在运行时环境中运行。
JVM指令集请参考JVM规范。
sample:
input:
3+4*5
output:
; public class Calc extends Object { ...} .class public Calc .super java/lang/Object ; public Calc() { super(); } // calls java.lang.Object() .method public ()V aload_0 invokenonvirtual java/lang/Object/()V return .end method ; main(): Expr.g will generate bytecode in this method .method public static main([Ljava/lang/String;)V .limit stack ; how much stack space do we need? .limit locals ; how many locals do we need? getstatic java/lang/System/out Ljava/io/PrintStream; ; code translated from input stream ; compute 3+4*5 ldc ldc ldc imul iadd ; print result on top of stack invokevirtual java/io/PrintStream/println(I)V return .end method
将输出拷贝至文件generator.j中,执行jasmin转换命令java -jar jasmin.jar generator.j
生成Calc.class,直接运行java Calc.class,输出23。
用到的文件包括,见文后代码部分
(1)生成AST的文法Expr.g
(2)树文法Gen.g,根据模板内容输出
(3)模板组文件ByteCode.stg(其中jasminFile模板采用该书源码文件,文中未给出)
(4)测试文件ByteCodeGeneratorTest.java
重写器案例
说明:跟踪C-语言中过程、函数调用和变量赋值
过程指没有返回值的函数,翻译后以call()指明;函数调用用eval_r()指明;变量赋值用write_to()指明。
sample:
input:
int x; void foo(){ int y; y = 1; g(34, y); x = h(); }
output:
int x; void foo(){ int y; y = 1; write_to("y", y) g(34, y); call("g"); x = eval_r("h", h()); write_to("x", x) }
用到的文件包括,见文后代码部分
(1)输出template、设置rewrite选项为true的文法CMinus.g
(2)测试文件Test.java
当然也可以采用IR之AST的树文法实现重写器。
代码
(生成器1)生成AST的文法Expr.g
grammar Expr; options{output=AST;ASTLabelType=CommonTree;} @header{ package template; import java.util.HashMap; } @members { int numOps = 0;//operation count HashMap locals = new HashMap();//local variable name-count map int localVarNum = 1;//local variable count } prog : stat+; stat : expr NEWLINE -> expr | ID '=' expr NEWLINE { if(locals.get($ID.text)==null){ locals.put($ID.text, new Integer(localVarNum++)); } } -> ^('=' ID expr) | NEWLINE -> ; expr : multExpr ('+'^|'-'^) multExpr {numOps++;} ; multExpr: atom ('*'^ atom {numOps++;})* ; atom : INT | ID | '('! expr ')'!; ID : ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')* ; INT : '0'..'9'+; WS : ( ' '| '\t'| '\r'| '\n') {$channel=HIDDEN;} ; NEWLINE : '\r'?'\n';
(生成器2)树文法Gen.g
tree grammar Gen; options { tokenVocab=Expr; ASTLabelType=CommonTree; output=template; } @header { package template; import java.util.HashMap; } @members { HashMap locals; } prog[int numOps, HashMap locals] @init {this.locals= locals;} : (s+=stat)+ -> jasminFile(instructions={$s}, maxStackDepth={numOps+1+1}, maxLocals={locals.size()+1}) ; stat : expr -> exprStat(v={$expr.st}, descr={$expr.text}) | ^('=' ID expr) -> assign(id={$ID.text}, descr={$text}, varNum={locals.get($ID.text)}, v={$expr.st}) ; expr returns [int value] : ^('+' a=expr b=expr) -> add(a={$a.st}, b={$b.st}) | ^('-' a=expr b=expr) -> sub(a={$a.st}, b={$b.st}) | ^('*' a=expr b=expr) -> mult(a={$a.st}, b={$b.st}) | INT -> int(v={$INT.text}) | ID -> var(id={$ID.text}, varNum={locals.get($ID.text)}) ;
(生成器3)模板组文件ByteCode.stg
group ByteCode; jasminFile(maxStackDepth, maxLocals, instructions) ::= << ; public class Calc extends Object { ...} .class public Calc .super java/lang/Object ; public Calc() { super(); } // calls java.lang.Object() .method public \()V aload_0 invokenonvirtual java/lang/Object/\()V return .end method ; main(): Expr.g will generate bytecode in this method .method public static main([Ljava/lang/String;)V .limit stack ; how much stack space do we need? .limit locals ; how many locals do we need? getstatic java/lang/System/out Ljava/io/PrintStream; ; code translated from input stream ; print result on top of stack invokevirtual java/io/PrintStream/println(I)V return .end method >> assign(varNum, v ,descr, id) ::= << ;compute descr istore : id >> exprStat(v, descr) ::= << ; compute >> add(a, b) ::= << iadd >> sub(a, b) ::= << isub >> mult(a, b) ::= << imul >> int(v) ::= "ldc " var(id, varNum) ::= "iload ; "
(生成器4)测试文件ByteCodeGeneratorTest.java
package template; import java.io.File; import java.io.FileInputStream; import java.io.FileReader; import java.io.InputStream; import org.antlr.runtime.ANTLRInputStream; import org.antlr.runtime.CommonTokenStream; import org.antlr.runtime.tree.CommonTree; import org.antlr.runtime.tree.CommonTreeNodeStream; import org.antlr.stringtemplate.StringTemplate; import org.antlr.stringtemplate.StringTemplateGroup; public class ByteCodeGeneratorTest { public static void main(String[] args) throws Exception { FileReader groupFileReader = new FileReader("D:/workspace/maven/antlrv3/grammar/template/generator/ByteCode.stg"); StringTemplateGroup templateGroup = new StringTemplateGroup(groupFileReader); groupFileReader.close(); // [1]3+4*5 // [2]a=3+4 // a InputStream is = new FileInputStream(new File("D:/workspace/maven/antlrv3/language/template.test")); ANTLRInputStream inputStream = new ANTLRInputStream(is); ExprLexer lexer = new ExprLexer(inputStream); CommonTokenStream tokenStream = new CommonTokenStream(lexer); ExprParser parser = new ExprParser(tokenStream); ExprParser.prog_return r = parser.prog(); // tree walker to generate template's value CommonTree tree = (CommonTree) r.getTree(); CommonTreeNodeStream treeNodeStream = new CommonTreeNodeStream(tree); treeNodeStream.setTokenStream(tokenStream); Gen walker = new Gen(treeNodeStream); walker.setTemplateLib(templateGroup); Gen.prog_return r2 = walker.prog(parser.numOps, parser.locals); StringTemplate output = (StringTemplate) r2.getTemplate(); System.out.println(output.toString()); } }
(重写器1)输出template、设置rewrite选项为true的文法CMinus.g
grammar CMinus; options{output=template;rewrite=true;} program : declaration+; declaration : variable | function ; variable: type ID';'; function: type ID '(' (formalParameter (',' formalParameter)* )? ')' block; formalParameter : type ID; type : 'int'|'void'; stat scope{boolean isAssign;} : expr ';' | block | ID '=' {$stat::isAssign=true;} expr ';' -> template(id={$ID.text}, assign={$text}) " write_to(\"\", )" | ';' ; block : '{' variable* stat* '}'; expr : ID | INT | ID '(' (expr (',' expr)* )? ')' -> {$stat::isAssign}? template(id={$ID.text}, e={$text}) "eval_r(\"\", )" ->template(id={$ID.text}, e={$text}) "; call(\"\")" | '(' expr ')' ; ID : ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'0'..'9'|'_')* ; INT : '0'..'9'+ ; COMMENT : '' {$channel=HIDDEN;} ; WS : ( ' '| '\t'| '\r'| '\n') {$channel=HIDDEN;};
(重写器2)测试文件Test.java
package template.rewriter; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import org.antlr.runtime.ANTLRInputStream; import org.antlr.runtime.TokenRewriteStream; public class Test { public static void main(String[] args) throws Exception { InputStream is = new FileInputStream(new File("D:/workspace/maven/antlrv3/language/rewriter/CMinus.test")); ANTLRInputStream input = new ANTLRInputStream(is); CMinusLexer lexer = new CMinusLexer(input); TokenRewriteStream tokens = new TokenRewriteStream(lexer); CMinusParser parser = new CMinusParser(tokens); parser.program(); System.out.println(tokens.toString()); } }