本文系 Creating JVM language 翻译的第 8 篇。 原文中的代码和原文有不一致的地方均在新的代码仓库 中更正过,建议参考新的代码仓库。
源码 Github
1. 语法改动 基本的算数操作包括:
本节需要改动的语法规则仅是 “expression”。 表达式通俗来讲就是求值(方法调用,值,变量引用等)。 而语句会做一些操作,但不一定会产生值,例如 if 语句。 既然算数操作总是返回值,那么他就是表达式:
1 2 3 4 5 6 7 8 9 10 11 12 expression : varReference #VARREFERENCE | value #VALUE | functionCall #FUNCALL | '('expression '*' expression')' #MULTIPLY | expression '*' expression #MULTIPLY | '(' expression '/' expression ')' #DIVIDE | expression '/' expression #DIVIDE | '(' expression '+' expression ')' #ADD | expression '+' expression #ADD | '(' expression '-' expression ')' #SUBSTRACT | expression '-' expression #SUBSTRACT ;
说明:
#
标号表示为当前规则创建可选的回调。Antlr 会在 ENkelVisotor
中生成诸如 visitDIVIDE(), visitADD()
的接口。
规则的定义先后顺序至关重要。假设我们有如下表达式: 1 +*3
。这样会产生歧义,因为有很多解释:1+2=3 3*3=9
或者 2*3=6 6+1=7
。Antlr 通过选择第一个符合的规则来解决歧义。因此,规则定义的顺序会影响到算数表达式的执行顺序。
()
里的表达式优先级高于普通优先级。因此诸如 (1+2)*3
的表达式能被正确解析和执行。
2. 匹配 Antlr 上下文对象 Antlr 为每一条规则生成新的类和回调。为每个操作新建一个类是个不错的选择,这样会让字节码的生成看起来更加干净:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class ExpressionVisitor extends EnkelBaseVisitor<Expression> { //some other methods (visitFunctionCall, visitVaraibleReference etc) @Override public Expression visitADD(@NotNull EnkelParser.ADDContext ctx) { EnkelParser.ExpressionContext leftExpression = ctx.expression(0); EnkelParser.ExpressionContext rightExpression = ctx.expression(1); Expression leftExpress = leftExpression.accept(this); Expression rightExpress = rightExpression.accept(this); return new Addition(leftExpress, rightExpress); } @Override public Expression visitMULTIPLY(@NotNull EnkelParser.MULTIPLYContext ctx) { EnkelParser.ExpressionContext leftExpression = ctx.expression(0); EnkelParser.ExpressionContext rightExpression = ctx.expression(1); Expression leftExpress = leftExpression.accept(this); Expression rightExpress = rightExpression.accept(this); return new Multiplication(leftExpress, rightExpress); } //Division //Substration }
Multiplcation,Addition,Division 和 Substraction 都是不可变的 POJO,存储了操作符的左侧和右侧的表达式(1+2,其中1 是左侧,2 是右侧)。
3. 生成字节码 当 Enkel 代码被解析和匹配到表达式对象后,我们可以进行下一步,字节码生成了。这里我们还需要创建另一个类,类方法中的参数是表达式的类型,方法体内生成对应的字节码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class ExpressionGenrator { //other methods (generateFunctionCall, generateVariableReference etc.) public void generate(Addition expression) { evaluateArthimeticComponents(expression); methodVisitor.visitInsn(Opcodes.IADD); } public void generate(Substraction expression) { evaluateArthimeticComponents(expression); methodVisitor.visitInsn(Opcodes.ISUB); } public void generate(Multiplication expression) { evaluateArthimeticComponents(expression); methodVisitor.visitInsn(Opcodes.IMUL); } public void generate(Division expression) { evaluateArthimeticComponents(expression); methodVisitor.visitInsn(Opcodes.IDIV); } private void evaluateArthimeticComponents(ArthimeticExpression expression) { Expression leftExpression = expression.getLeftExpression(); Expression rightExpression = expression.getRightExpression(); leftExpression.accept(this); rightExpression.accept(this); } }
算数表达式中用到的字节码非常通俗易懂。字节码指令将两个操作数从出栈,执行计算,结果入栈。
iadd - 整数相加。
isub - 整数相减
imul - 整数相乘
idiv - 整数相除
其他数据类型的指令以此类推。
4. 结果 假设我们有如下 Enkel 代码:
1 2 3 4 5 First { void main (string[] args) { var result = 2+3*4 } }
编译后的字节码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 $ javap -c First public class First { public static void main(java.lang.String[]); Code: 0: bipush 2 //push 2 onto the stack 2: bipush 3 //push 3 onto the stack 4: bipush 4 //push 4 onto the stack 6: imul //take two top values from the stack (3 and 4) and multiply them. Put result on stack 7: iadd //take two top values from stack (2 and 12-result of imul) and add em. Put result back on stack 8: istore_1 //store top value from the stack into local variable at index 1 in local variable array of the curennt frame 9: return }
全文完。