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

KevinOfNeu發表於2018-09-06

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

原始碼

Github

語法

Enkel 的構造器宣告和呼叫的語法和 Java 保持一致。 宣告例項:

Cat ( String name ) {
}
複製程式碼

呼叫例項:

new Cat ( "Molly" ) 
複製程式碼

語法規則更改

Java 中構造器的宣告是一個沒有返回值的函式。Enkel 中也是一樣。

對於構造器的呼叫呢?解析器如何區別方法呼叫和構造器呼叫呢?因此,Enkel 引入了關鍵字 new:

//other rules
expression : //other rules alternatives
           | 'new' className '('argument? (',' argument)* ')' #constructorCall
複製程式碼

匹配 Antlr 上下文物件

新的語法規則 constructCall 帶來一個新的解析回撥:

@Override
public Expression visitConstructorCall(@NotNull EnkelParser.ConstructorCallContext ctx) {
    String className = ctx.className().getText();
    List<EnkelParser.ArgumentContext> argumentsCtx = ctx.argument();
    List<Expression> arguments = getArgumentsForCall(argumentsCtx, className);
    return new ConstructorCall(className, arguments);
}
複製程式碼

方法呼叫要求名字,返回值,以及引數和持有者的資訊。構造器的呼叫僅僅需要類名和引數。

  • 構造器需要型別嗎? 不需要。因為返回值型別都是固定的,就是類本身
  • 構造器需要持有者資訊嗎?不需要。因為構造器的呼叫都是通過 new 關鍵字,someObject.new SomeObject() 這種呼叫時沒有任何意義的

對於方法宣告,我們又該如何區分呢? 有一種簡單的辦法就是對比方法的名字和型別是否一致。這也就是意味著普通方法的命名不能跟類名重複。

@Override
    public Function visitFunction(@NotNull EnkelParser.FunctionContext ctx) {
        List<Type> parameterTypes = ctx.functionDeclaration().functionParameter().stream()
                .map(p -> TypeResolver.getFromTypeName(p.type())).collect(toList());
        FunctionSignature signature = scope.getMethodCallSignature(ctx.functionDeclaration().functionName().getText(),parameterTypes);
        scope.addLocalVariable(new LocalVariable("this",scope.getClassType()));
        addParametersAsLocalVariables(signature);
        Statement block = getBlock(ctx);
        //Check if method is not actually a constructor
        if(signature.getName().equals(scope.getClassName())) {
            return new Constructor(signature,block);
        }
        return new Function(signature, block);
    }
複製程式碼

預設的構造器

如果你沒有手動建立構造器,Enkel 會建立預設的構造器:

@Override
public ClassDeclaration visitClassDeclaration(@NotNull EnkelParser.ClassDeclarationContext ctx) {
    //some other stuff
    boolean defaultConstructorExists = scope.parameterLessSignatureExists(className);
    addDefaultConstructorSignatureToScope(name, defaultConstructorExists);
    //other stuff
    if(!defaultConstructorExists) {
        methods.add(getDefaultConstructor());
    }
}
        
private void addDefaultConstructorSignatureToScope(String name, boolean defaultConstructorExists) {
    if(!defaultConstructorExists) {
        FunctionSignature constructorSignature = new FunctionSignature(name, Collections.emptyList(), BultInType.VOID);
        scope.addSignature(constructorSignature);
    }
}

private Constructor getDefaultConstructor() {
    FunctionSignature signature = scope.getMethodCallSignatureWithoutParameters(scope.getClassName());
    Constructor constructor = new Constructor(signature, Block.empty(scope));
    return constructor;
}
複製程式碼

你或許好奇為何構造器返回 void。簡單來說就是 JVM 把物件的建立分為兩個步驟:首先分配記憶體空間,然後才是呼叫構造器(構造器主要職責是做初始化,因此我們可以在建構函式內呼叫 this 變數)。

生成位元組碼

到目前為止,我們已經可以解析建構函式的宣告以及呼叫了。接下來就是如何生成位元組碼了。

物件的建立的位元組碼有兩個指令:

  • NEW 在堆中分類記憶體,初始化成員變數為預設值
  • INVOKESPECIAL 呼叫構造器

Java 中你無需在構造器中手動呼叫 super() 。實際上這是必須的,否則無法建立物件,但是 Java 編譯器幫我們做了這一步。

呼叫 super 會用到 INVOKESPECIAL 指令,Enkel 編譯器跟 Java 編譯器保持一致,也會自動處理呼叫。

構造器呼叫的位元組碼生成

public void generate(ConstructorCall constructorCall) {
        String ownerDescriptor = scope.getClassInternalName(); //example : java/lang/String
        methodVisitor.visitTypeInsn(Opcodes.NEW, ownerDescriptor); //NEW instruction takes object decriptor as an input
        methodVisitor.visitInsn(Opcodes.DUP); //Duplicate (we do not want invokespecial to "eat" our brand new object
        FunctionSignature methodCallSignature = scope.getMethodCallSignature(constructorCall.getIdentifier(),constructorCall.getArguments());
        String methodDescriptor = DescriptorFactory.getMethodDescriptor(methodCallSignature);
        generateArguments(constructorCall);
        methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, ownerDescriptor, "<init>", methodDescriptor, false);
    }
複製程式碼

你可能會好奇為什麼用到了 DUP 指令。在 NEW 指令執行後,棧中儲存了新建創的物件。INVOKESPECIAL 指令會從棧頂取資料,然後初始化。如果我們不賦值物件,這樣會導致新建立的物件被構造器指令出棧,然後物件會丟失在堆中等待 GC 去做垃圾回收。

如下的語句: new Cat().meow()

會生成如下的位元組碼:

0: new           #2                  // class Cat
3: dup
4: invokespecial #23                 // Method "<init>":()V
7: invokevirtual #26                 // Method meow:()V
複製程式碼

構造器宣告的位元組碼生成

public void generate(Constructor constructor) {
    Block block = (Block) constructor.getRootStatement();
    Scope scope = block.getScope();
    int access = Opcodes.ACC_PUBLIC;
    String description = DescriptorFactory.getMethodDescriptor(constructor);
    MethodVisitor mv = classWriter.visitMethod(access, "<init>", description, null, null);
    mv.visitCode();
    StatementGenerator statementScopeGenrator = new StatementGenerator(mv,scope);
    new SuperCall().accept(statementScopeGenrator); //CALL SUPER IMPLICITILY BEFORE BODY ITSELF
    block.accept(statementScopeGenrator); //CALL THE BODY DEFINED BY PROGRAMMER
    appendReturnIfNotExists(constructor, block,statementScopeGenrator);
    mv.visitMaxs(-1,-1);
    mv.visitEnd();
}
複製程式碼

前面我們提到,構造器中的 super 呼叫時必須的,Java 中我們沒有手動呼叫(除非父類沒有無參構造器)。這樣做不是非必須的而是 Java 編譯器幫我們做了自動生成。Enkel 也要有這麼炫酷的功能。

new SuperCall().accept(statementScopeGenrator);

觸發:

public void generate(SuperCall superCall) {
    methodVisitor.visitVarInsn(Opcodes.ALOAD,0); //LOAD "this" object
    generateArguments(superCall);
    String ownerDescriptor = scope.getSuperClassInternalName();
    methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, ownerDescriptor, "<init>", "()V" , false);
}
複製程式碼

每個方法(甚至是構造器)把引數當做幀中的區域性變數來對待。如果方法 int add(int x,int y) 在靜態上下文中被呼叫,他的初始 frame 中存在兩個變數(x, y)。如果在非靜態上下文中,this(被呼叫者)也存在區域性變數中。因此,如果 add 方法是在非靜態上下文中被呼叫,那麼有三個區域性變數(this, x, y)。

Cat 類的構造器(構造器內沒有內容)生成的位元組碼如下:

0: aload_0      //load "this"
1: invokespecial #8                  // Method java/lang/Object."<init>":()V - call super on "this" (the Cat dervies from Object)
12: return
複製程式碼

相關文章