手拉手教你實現一門程式語言 Enkel, 系列 8

KevinOfNeu發表於2018-09-06

本文系 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
}
複製程式碼

相關文章