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

KevinOfNeu發表於2018-09-06

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

原始碼

Github

1. 方法

到目前為止,我們可以在 Enkel 中宣告類和變數,但是他們都處於同一個全域性作用域中。下一步,我們需要支援方法。

我們的目標是可以處理如下程式碼:

First {
    void main (string[] args) {
        var x = 25
        metoda(x)
    }

    void metoda (int param) {
        print param
    }
}
複製程式碼

2. 作用域

為了可以訪問其他的函式或者變數,他們需要在同一個作用域下:

public class Scope {
    private List<Identifier> identifiers; //think of it as a variables for now
    private List<FunctionSignature> functionSignatures;
    private final MetaData metaData;  //currently stores only class name

    public Scope(MetaData metaData) {
        identifiers = new ArrayList<>();
        functionSignatures = new ArrayList<>();
        this.metaData = metaData;
    }

    public Scope(Scope scope) {
        metaData = scope.metaData;
        identifiers = Lists.newArrayList(scope.identifiers);
        functionSignatures = Lists.newArrayList(scope.functionSignatures);
    }
    
    //some other methods that expose data to the outside
}         
複製程式碼

物件 scope 是在類建立的時候被建立的,然後傳遞給下一層級(方法)。下一層級拷貝並且新增其他的選項。

3. 簽名

函式呼叫的時候,需要提供函式的一些額外資訊。假設有如下的虛擬碼:

f1() {
    f2()
}

f2(){
}
複製程式碼

解析後如下圖所示:

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

節點的訪問順序如下:

  • Root
  • 函式 f1
  • 對函式 f2 的呼叫//錯誤,此時 f2 還沒有定義
  • 函式 f2

因此,當函式呼叫發生時,函式的定義可能沒有訪問到,f1 解析的時候並沒有 f2 的資訊。 為了解決這個問題,我們必須訪問所有函式的定義並且把函式的簽名儲存到作用域中。

public class ClassVisitor extends EnkelBaseVisitor<ClassDeclaration> {

 private Scope scope;

 @Override
 public ClassDeclaration visitClassDeclaration(@NotNull EnkelParser.ClassDeclarationContext ctx) {
     String name = ctx.className().getText();
     FunctionSignatureVisitor functionSignatureVisitor = new FunctionSignatureVisitor();
     List<EnkelParser.FunctionContext> methodsCtx = ctx.classBody().function();
     MetaData metaData = new MetaData(ctx.className().getText());
     scope = new Scope(metaData);
     //First find all signatures
     List<FunctionSignature> signatures = methodsCtx.stream()
             .map(method -> method.functionDeclaration().accept(functionSignatureVisitor))
             .peek(scope::addSignature)
             .collect(Collectors.toList());
     //Once the signatures are found start parsing methods
     List<Function> methods = methodsCtx.stream()
             .map(method -> method.accept(new FunctionVisitor(scope)))
             .collect(Collectors.toList());
     return new ClassDeclaration(name, methods);
 }
}
複製程式碼

4. Invokestatic

當所有相關的資訊都被正確解析後,接下來需要生成位元組碼了。當前 Enkele 還沒有實現物件的建立,因此方法的呼叫先使用 static 的方式來呼叫。

int access = Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC;

靜態方法的呼叫對應的位元組碼指令是 invokestatic, 需要兩個引數:

運算元棧中的會執行出棧操作,並傳遞給方法呼叫(型別和個數必須和方法描述一致)。

public class MethodGenerator {
    private final ClassWriter classWriter;

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

    public void generate(Function function) {
        Scope scope = function.getScope();
        String name = function.getName();
        String description = DescriptorFactory.getMethodDescriptor(function);
        Collection<Statement> instructions = function.getStatements();
        int access = Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC;
        MethodVisitor mv = classWriter.visitMethod(access, name, description, null, null);
        mv.visitCode();
        StatementGenerator statementScopeGenrator = new StatementGenerator(mv);
        instructions.forEach(instr -> statementScopeGenrator.generate(instr,scope));
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(-1,-1); //asm autmatically calculate those but the call is required
        mv.visitEnd();
    }
}
複製程式碼

5. 效果

如下 Enkel 程式碼:

First {
    void main (string[] args) {
        var x = 25
        metoda(x)
    }

    void metoda (int param) {
        print param
    }
}
複製程式碼

會被編譯成如下所示的位元組碼:

$ javap -c First
public class First {
  public static void main(java.lang.String[]);
    Code:
       0: bipush        25 //push value 25 onto the stack
       2: istore_0         //store value from stack into variable at index 0
       3: iload_0          //load variable at index onto the stack
       5: invokestatic  #10 //call metod Method metoda:(I)V  
       8: return

  public static void metoda(int);
    Code:
       0: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: iload_0
       4: invokevirtual #20                 // Method "Ljava/io/PrintStream;".println:(I)V
       7: return
}
複製程式碼

相關文章