手拉手教你实现一门编程语言 Enkel, 系列 8

270 阅读3分钟

本文系 Creating JVM language 翻译的第 8 篇。 原文中的代码和原文有不一致的地方均在新的代码仓库中更正过,建议参考新的代码仓库。

源码

Github

1. 语法改动

基本的算数操作包括:

  • /

本节需要改动的语法规则仅是 "expression"。 表达式通俗来讲就是求值(方法调用,值,变量引用等)。 而语句会做一些操作,但不一定会产生值,例如 if 语句。 既然算数操作总是返回值,那么他就是表达式:

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 为每一条规则生成新的类和回调。为每个操作新建一个类是个不错的选择,这样会让字节码的生成看起来更加干净:

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 代码被解析和匹配到表达式对象后,我们可以进行下一步,字节码生成了。这里我们还需要创建另一个类,类方法中的参数是表达式的类型,方法体内生成对应的字节码。

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 代码:

First {
        void main (string[] args) {
            var result = 2+3*4
        }
}

编译后的字节码如下所示:

$ 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
}