本文系 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
}