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

KevinOfNeu發表於2018-09-06

本文系 Creating JVM language 翻譯的第 9 篇。 原文中的程式碼和原文有不一致的地方均在新的程式碼倉庫中更正過,建議參考新的程式碼倉庫。

原始碼

Github

1. 語法規則改動

我們新建一個規則 “returnStatement”。 那為什麼不叫 “returnExpression” 呢?畢竟表示式總是返回值的,語句沒有返回值麼? 這聽起來有點繞口,但是返回值並不總是返回一個值。在 Java 中,程式碼 int x = return 5; 沒有意義, 在 Enkel 中也是如此。換句話說,表示式總可以給一個變數賦值。這就是為什麼返回是語句,而不是表示式。

statement : variableDeclaration
           //other statements rules
           | returnStatement ;

variableDeclaration : VARIABLE name EQUALS expression;
printStatement : PRINT expression ;
returnStatement : 'return' #RETURNVOID
                | ('return')? expression #RETURNWITHVALUE;
複製程式碼

返回語句有兩種形式:

  • RETURNVOID - 用在沒有返回值的方法中。return 關鍵字是必須的,後面不需要表示式
  • RETURNWITHVALUE - 用在有返回值的方法中。return 關鍵字不是必須的,但是需要一個表示式

因此,方法可以顯示或者隱士的返回一個值:

SomeClass {
    fun1 {
       return  //explicitly return from void method
    }
    
    fun2 {
        //implicitly return from void method
    }
    
    int fun2 {
        return 1  //explicitly return "1" from int method
    }
    
    int fun3 {
        1  //implicitly return "1" from int method
    }
}
複製程式碼

上述程式碼經過解析後,AST 圖形展示如下:

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

我們可以看到,AST 中並沒有處理 fun2 中的隱士返回值。這是因為方法是空的語句塊,匹配空的語句塊作為返回值並不是一個好的想法。因此,確實的返回語句會在位元組碼生成階段手動新增。

2. 匹配 Antlr 上下文物件

經過解析後,返回語句從 antlr 的上下文物件轉換成 POJO 類 ReturnStatement 。這一步的目的是僅匹配位元組碼生成需要的資料,而不是直接從 antlr 生成的物件中取資料,這樣會讓程式碼看起來很醜陋。

public class StatementVisitor extends EnkelBaseVisitor<Statement> {

    //other stuff
    
    @Override
    public Statement visitRETURNVOID(@NotNull EnkelParser.RETURNVOIDContext ctx) {
        return new ReturnStatement(new EmptyExpression(BultInType.VOID));
    }
    
    @Override
    public Statement visitRETURNWITHVALUE(@NotNull EnkelParser.RETURNWITHVALUEContext ctx) {
        Expression expression = ctx.expression().accept(expressionVisitor); 
        return new ReturnStatement(expression);
    }   
}
複製程式碼

3. 檢測隱士空返回

假設方法中包含有隱士返回,在解析階段是不會生成返回語句的,這就是為什麼我們需要檢測這種情景,並且在位元組碼生成階段手動新增返回語句。

public class MethodGenerator {
    //other stuff
    private void appendReturnIfNotExists(Function function, Block block,StatementGenerator statementScopeGenrator) {
        Statement lastStatement = block.getStatements().get(block.getStatements().size() - 1);
        boolean isLastStatementReturn = lastStatement instanceof ReturnStatement;
        if(!isLastStatementReturn) {
            EmptyExpression emptyExpression = new EmptyExpression(function.getReturnType());
            ReturnStatement returnStatement = new ReturnStatement(emptyExpression);
            returnStatement.accept(statementScopeGenrator);
        }
    }
}

複製程式碼

上述方法檢測方法最後的語句是不是返回語句,如果不是就新增返回指令。

4. 生成位元組碼

public class StatementGenerator {
    //oher stuff
    public void generate(ReturnStatement returnStatement) {
        Expression expression = returnStatement.getExpression();
        Type type = expression.getType();
        expression.accept(expressionGenrator); //generate bytecode for expression itself (puts the value of expression onto the stack)
        if(type == BultInType.VOID) {
            methodVisitor.visitInsn(Opcodes.RETURN);
        } else if (type == BultInType.INT) {
            methodVisitor.visitInsn(Opcodes.IRETURN);
        }
    }
}
複製程式碼

因此,return 5 會經過如下階段:

  • 從返回語句中獲得表示式(這裡是5,型別是值)
  • 生成 5 對應的位元組碼。(expression.accept(expressionGenerator) 呼叫 ExpressionGenerator.generate(Value value))
  • 位元組碼生成階段,會生成一個新的值 5 並壓入運算元棧
  • IRETURN 指令將運算元棧棧頂資料出棧,並返回

位元組碼錶示:

 bipush        5
 ireturn
複製程式碼

5. 示例

假設我們又如下 Enkel 程式碼:

SumCalculator {

    void main(string[] args) {
        print sum(5,2)
    }

    int sum (int x ,int y) {
        x+y
    }
}
複製程式碼

生成的位元組碼如下:

$ javap -c  SumCalculator
public class SumCalculator {
  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #12                 //get static field java/lang/System.out:Ljava/io/PrintStream;
       3: bipush        5
       5: bipush        2
       7: invokestatic  #16                 // call method sum (with the values on operand stack 5,2)
      10: invokevirtual #21                 // call method println (with the value on stack - the result of method sum)
      13: return                           //return

  public static int sum(int, int);
    Code:
       0: iload_0
       1: iload_1
       2: iadd
       3: ireturn //return the value from operand stack (result of iadd)
}
複製程式碼

相關文章