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

KevinOfNeu發表於2018-09-06

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

原始碼

Github

語法

語法規則和 Java 非常類似,但是更加簡單,沒有複雜的修飾符(比如 static, volatile, transient)。

Fields {

    int field

    start {
        field = 5
        print field
    }
}
複製程式碼

語法規則更改

在本篇之前我們只能在類中定義方法,現在我們開啟定義欄位的大門吧:

classBody :  field* function* ;
field : type name;
複製程式碼

賦值語句:

assignment : name EQUALS expression;
複製程式碼

為何這麼久都沒實現賦值語句

欄位用來賦值,但是這麼久以來我們一直沒有實現賦值語句來給宣告的變數賦值,我這麼做是因為有以下考量。

我希望變數是不可變的,賦值意味著改變狀態,這會導致許多問題,比如同步,副作用,還有記憶體洩漏。

比如有如下的 Java 程式碼:

Stuff trustMeIWontModifyYourArg(SomeObject arg) {
    ... 999 lines of code 
    arg = null; //or some other nasty hidden stuff
    ...another 999 lines of code
}
複製程式碼

通過方法簽名,我們可能理所當然的想,方法會修改引數嗎,他沒有 final 修飾,但是大多數 Java 程式設計師會忽略。僅僅通過名字判斷出來方法不會修改變數,那我們就用他吧。

過了兩個小時後,出現了 NullPointerException,方法還是修改了引數。

如果方法沒有副作用,那麼可以很方便的實現併發而不用擔心同步的問題,這種方法沒有狀態,也沒有副作用,實現避免副作用方法最簡單的辦法就是儘可能的使用常量。

生成位元組碼

宣告欄位

使用 ASM 的 visitField 來宣告欄位。它新增欄位到 fields[],fields_count 會自動增加計數器。

public class FieldGenerator {

    private final ClassWriter classWriter;

    public FieldGenerator(ClassWriter classWriter) {
        this.classWriter = classWriter;
    }

    public void generate(Field field) {
        String name = field.getName();
        String descriptor = field.getType().getDescriptor();
        FieldVisitor fieldVisitor = classWriter.visitField(Opcodes.ACC_PUBLIC, name,descriptor, null, null);
        fieldVisitor.visitEnd();
    }
}
複製程式碼

讀取欄位

讀取欄位,你需要:

  • 欄位名
  • 欄位型別修飾符
  • 持有者的全限定名
public class ReferenceExpressionGenerator {

     //constructor and fields

    public void generate(FieldReference fieldReference) {
        String varName = fieldReference.geName();
        Type type = fieldReference.getType();
        String ownerInternalName = fieldReference.getOwnerInternalName();
        String descriptor = type.getDescriptor();
        methodVisitor.visitVarInsn(Opcodes.ALOAD,0);
        methodVisitor.visitFieldInsn(Opcodes.GETFIELD, ownerInternalName,varName,descriptor);
    }
}
複製程式碼
  • ALOAD, 0 獲得 this ,也就是區域性變數索引值為 0 的值。在 非 static 語境中,this 預設都會在索引為 0 的位置。
  • GETFIELD 讀取變數的指令

賦值

public class AssignmentStatementGenerator {

    //constructor and fields
    
    public void generate(Assignment assignment) {
        String varName = assignment.getVarName();
        Expression expression = assignment.getExpression();
        Type type = expression.getType();
        if(scope.isLocalVariableExists(varName)) {
            int index = scope.getLocalVariableIndex(varName);
            methodVisitor.visitVarInsn(type.getStoreVariableOpcode(), index);
            return;
        }
        Field field = scope.getField(varName);
        String descriptor = field.getType().getDescriptor();
        methodVisitor.visitVarInsn(Opcodes.ALOAD,0);
        expression.accept(expressionGenerator);
        methodVisitor.visitFieldInsn(Opcodes.PUTFIELD,field.getOwnerInternalName(),field.getName(),descriptor);
    }
複製程式碼

如果區域性變數和欄位名字衝突了,那麼區域性變數有更高的優先順序。

PUTFIELD 和 GETFIELD 相似,但是會出棧頂資料,表示式的值會被賦值到變數

示例

如下 Enkel 檔案

Fields {

    int field

    start {
        field = 5
        print field
    }
}
複製程式碼

生成位元組碼如下:

public class Fields {
  public int field;

  public void start();
    Code:           
       0: aload_0               //get "this"
       1: ldc           #9      // load constant "5" from constant pool 
       3: putfield      #11     // Field field:I - pop 5 off the stack and write to field
       6: getstatic     #17     // Field java/lang/System.out:Ljava/io/PrintStream; 
       9: aload_0               //get "this" reference
      10: getfield      #11     // Field field:I
      13: invokevirtual #22     // Method "Ljava/io/PrintStream;".println:(I)V
      16: return

 //autogenerated constructor and main method
}
複製程式碼

相關文章