1. 程式人生 > >從零寫一個編譯器(十):編譯前傳之直接解釋執行

從零寫一個編譯器(十):編譯前傳之直接解釋執行

專案的完整程式碼在 C2j-Compiler

前言

這一篇不看也不會影響後面程式碼生成部分

現在經過詞法分析語法分析語義分析,終於可以進入最核心的部分了。前面那部分可以稱作編譯器的前端,程式碼生成程式碼優化都是屬於編譯器後端,如今有關編譯器的工作崗位主要都是對後端的研究。當然現在寫的這個編譯器因為水平有限,並沒有優化部分。

在進行程式碼生成部分之前,我們先來根據AST來直接解釋執行,其實就是對AST的遍歷。現代直譯器一般都是生成一個比較低階的指令然後跑在虛擬機器上,但是簡單起見我們就直接根據AST解釋執行的直譯器。(原本這部分是不想寫的,是可以直接寫程式碼生成的)

這次的檔案在interpreter包裡,這次涉及到的檔案比較多,就不列舉了

一個小問題

在開始說直譯器的部分前我們看一下,認真觀察之前在構造符號表對賦初值的推導式的處理是有問題的,但是問題不大,只要稍微改動一下

在github原始碼的部分已經改了,改動如下:

case SyntaxProductionInit.VarDecl_Equal_Initializer_TO_Decl:
    attributeForParentNode = (Symbol) valueStack.get(valueStack.size() - 3);
    ((Symbol) attributeForParentNode).value = initialValue;
break;

case SyntaxProductionInit.Expr_TO_Initializer:
    initialValue = (Integer) valueStack.get(valueStack.size() - 1);
    System.out.println(initialValue);
    break;

其實就是一個拿到賦的初值放到Symbol的value裡

示例

先看一下這篇完成之後解釋執行的效果

void swap(int arr[10], int i, int j) {
    int temp;
    temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

void quickSort(int a[10], int p, int r) {
    int x;
    int i;
    i = p - 1;
    int j;
    int t;
    int v;
    v = r - 1;
    if (p < r) {
        x = a[r];
        for (j = p; j <= v; j++) {
            if (a[j] <= x) {
                i++;
                swap(a, i, j);
            }
        }
        v = i + 1;
        swap(a, v, r);
        t = v - 1;
        quickSort(a, p, t);
        t = v + 1;
        quickSort(a, t, r);
    }
}


void main () {
    int a[10];
    int i;
    int t;

    printf("Array before quicksort:");
    for(i = 0; i < 10; i++) {
        t = (10 - i);
        a[i] = t;
        printf("value of a[%d] is %d", i, a[i]);
    }

    quickSort(a, 0, 9);

    printf("Array after quicksort:");
    for (i = 0; i < 10; i++) {
        printf("value of a[%d] is %d", i, a[i]);
    }
}

Executor介面

所有能夠執行結點的類都要實現這個介面,所以以此來達到遍歷AST來執行程式碼

直譯器的啟動在Interpreter類裡,它也實現了Executor介面

Interpreter類的execute傳入的引數就是整棵抽象語法樹的頭節點了,ExecutorFactory的getExecutor則是根據當前結點的TokenType返回一個可以解釋當前節點的類,而其它執行節點的類都繼承了BaseExecutor

@Override
public Object execute(AstNode root) {
    if (root == null) {
        return null;
    }

    ExecutorFactory factory = ExecutorFactory.getInstance();
    Executor executor = factory.getExecutor(root);
    executor.execute(root);

    return root;
}

BaseExecutor的兩個主要方法就是執行它的子節點,並且可以指定執行哪個子節點。可以先忽略Brocaster,這些是用來實現執行節點類之前的通訊的,現在還沒有用。reverseChildren是用來對節點的反轉,因為在建立的AST的過程由於堆疊的原因,所以節點順序的相反的。continueExecute是標誌位,後面可能會執行到設定它的節點來結束執行

protected void executeChildren(AstNode root) {
    ExecutorFactory factory = ExecutorFactory.getInstance();
    root.reverseChildren();

    int i = 0;
    while (i < root.getChildren().size()) {
        if (!continueExecute) {
            break;
        }

        AstNode child = root.getChildren().get(i);
        executorBrocaster.brocastBeforeExecution(child);
        Executor executor = factory.getExecutor(child);
        if (executor != null) {
            executor.execute(child);
        } else {
            System.err.println("Not suitable Generate found, node is: " + child.toString());
        }

        executorBrocaster.brocastAfterExecution(child);

        i++;
    }
}

protected AstNode executeChild(AstNode root, int childIdx) {
    root.reverseChildren();
    AstNode child;
    ExecutorFactory factory = ExecutorFactory.getInstance();
    child = (AstNode)root.getChildren().get(childIdx);
    Executor executor = factory.getExecutor(child);
    AstNode res = (AstNode)executor.execute(child);

    return res;
}

解釋執行

我們可以知道一個C語言的原始檔一般都是一些函式定義和一個main的函式來啟動,所以在AstBuilder裡返回給Interpreter的節點就是從main開始的

public AstNode getSyntaxTreeRoot() {
    AstNode mainNode = funcMap.get("main");
    return mainNode;
}

執行函式ExtDefExecutor

用來執行函式的Executor是ExtDefExecutor

  • 在進入execute會先執行FunctDecl節點,再執行CompoundStmt節點
  • saveArgs和restoreArgs屬於保護當前的環境,就是進入其它作用域的時候保證這個符號不變修改,不比如當作引數傳遞的時候
  • returnVal也是屬於由其它節點設定的屬性
  • root.setAttribute的作用就是對節點設定屬性,把值往上傳遞
@Override
public Object execute(AstNode root) {
    this.root = root;
    int production = (Integer) root.getAttribute(NodeKey.PRODUCTION);
    switch (production) {
        case SyntaxProductionInit.OptSpecifiers_FunctDecl_CompoundStmt_TO_ExtDef:
            AstNode child = root.getChildren().get(0);
            funcName = (String) child.getAttribute(NodeKey.TEXT);
            root.setAttribute(NodeKey.TEXT, funcName);
            saveArgs();
            executeChild(root, 0);

            executeChild(root, 1);
            Object returnVal = getReturnObj();
            clearReturnObj();

            if (returnVal != null) {
                root.setAttribute(NodeKey.VALUE, returnVal);
            }
            isContinueExecution(true);
            restoreArgs();
            break;

        default:
            break;
    }
    return root;
}

函式定義 FunctDeclExecutor

執行函式會先執行它的括號的前部分也就是識別符號和引數那部分,對引數進行初始化,函式的傳遞的引數用單獨一個類FunctionArgumentList來表示

@Override
public Object execute(AstNode root) {
    int production = (Integer) root.getAttribute(NodeKey.PRODUCTION);
    Symbol symbol;
    currentNode = root;

    switch (production) {
        case SyntaxProductionInit.NewName_LP_RP_TO_FunctDecl:
            root.reverseChildren();
            copyChild(root, root.getChildren().get(0));
            break;

        case SyntaxProductionInit.NewName_LP_VarList_RP_TO_FunctDecl:
            symbol = (Symbol) root.getAttribute(NodeKey.SYMBOL);

            Symbol args = symbol.getArgList();
            initArgumentList(args);

            if (args == null || argsList == null || argsList.isEmpty()) {
                System.err.println("generate function with arg list but arg list is null");
                System.exit(1);
            }
            break;

        default:
            break;
    }

    return root;
}

執行語句部分 CompoundStmtExecutor

執行語句的部分就開始對樹的遍歷執行,但是我們來看一下這個節點的推導式

COMPOUND_STMT-> LC LOCAL_DEFS STMT_LIST RC

在構建AST的時候我們並沒有構建LOCAL_DEFS,並且在之前符號表也沒有進行處理,所以我們直接執行第0個節點就可以了

@Override
public Object execute(AstNode root) {
    return executeChild(root, 0);
}

一元操作

下面看UnaryNodeExecutor,UnaryNodeExecutor應該是所有Executor最複雜的之一了,其實對於節點執行,先執行子節點,並且向上傳遞執行結果的值。

只說其中的幾個

  • 指標

這個就是對指標的操作了,本質是對記憶體分配的一個模擬,再設定實現ValueSetter的DirectMemValueSetter,讓它的父節點可以通過這個節點的setter對指標指向進行賦值

ValueSetter是一個可以對變數進行賦值的介面,陣列、指標、簡單的變數都有各自的valueSetter

case SyntaxProductionInit.Start_Unary_TO_Unary:
    child = root.getChildren().get(0);
    int addr = (Integer) child.getAttribute(NodeKey.VALUE);
    symbol = (Symbol) child.getAttribute(NodeKey.SYMBOL);

    MemoryHeap memHeap = MemoryHeap.getInstance();
    Map.Entry<Integer, byte[]> entry = memHeap.getMem(addr);
    int offset = addr - entry.getKey();
    if (entry != null) {
        byte[] memByte = entry.getValue();
        root.setAttribute(NodeKey.VALUE, memByte[offset]);
    }

    DirectMemValueSetter directMemSetter = new DirectMemValueSetter(addr);
    root.setAttribute(NodeKey.SYMBOL, directMemSetter);
    break;
  • 指標和陣列操作:

這是執行陣列或者是指標的操作,對於陣列和指標的操作會在節點中的Symbol裡設定一個可以進行賦值的介面:ArrayValueSetter、PointerValueSetter,邏輯都不是很複雜。對於指標的操作其實是對於記憶體地址分配的一個模擬。

case SyntaxProductionInit.Unary_LB_Expr_RB_TO_Unary:
    child = root.getChildren().get(0);
    symbol = (Symbol) child.getAttribute(NodeKey.SYMBOL);

    child = root.getChildren().get(1);
    int index = (Integer) child.getAttribute(NodeKey.VALUE);

    try {
        Declarator declarator = symbol.getDeclarator(Declarator.ARRAY);
        if (declarator != null) {
            Object val = declarator.getElement(index);
            root.setAttribute(NodeKey.VALUE, val);
            ArrayValueSetter setter = new ArrayValueSetter(symbol, index);
            root.setAttribute(NodeKey.SYMBOL, setter);
            root.setAttribute(NodeKey.TEXT, symbol.getName());
        }
        Declarator pointer = symbol.getDeclarator(Declarator.POINTER);
        if (pointer != null) {
            setPointerValue(root, symbol, index);

            PointerValueSetter pv = new PointerValueSetter(symbol, index);
            root.setAttribute(NodeKey.SYMBOL, pv);
            root.setAttribute(NodeKey.TEXT, symbol.getName());
        }

    } catch (Exception e) {
        System.err.println(e.getMessage());
        e.printStackTrace();
        System.exit(1);
    }
    break;
  • 函式呼叫

函式呼叫也是屬於一元操作,對於函式呼叫有兩種情況:一種是自定義的函式,還有一種是直譯器提供的函式

  1. 如果是自定義函式,就找到這個函式的頭節點,從這個頭節點開始執行
  2. 如果是直譯器提供的函式,就交由ClibCall處理,比如printf就是屬於庫函式
case SyntaxProductionInit.Unary_LP_RP_TO_Unary:
case SyntaxProductionInit.Unary_LP_ARGS_RP_TO_Unary:
    String funcName = (String) root.getChildren().get(0).getAttribute(NodeKey.TEXT);
    if (production == SyntaxProductionInit.Unary_LP_ARGS_RP_TO_Unary) {
        AstNode argsNode = root.getChildren().get(1);
        ArrayList<Object> argList = (ArrayList<Object>) argsNode.getAttribute(NodeKey.VALUE);
        ArrayList<Object> symList = (ArrayList<Object>) argsNode.getAttribute(NodeKey.SYMBOL);
        FunctionArgumentList.getInstance().setFuncArgList(argList);
        FunctionArgumentList.getInstance().setFuncArgSymbolList(symList);
    }

    AstNode func = AstBuilder.getInstance().getFunctionNodeByName(funcName);
    if (func != null) {
        Executor executor = ExecutorFactory.getInstance().getExecutor(func);
        executor.execute(func);
        Object returnVal = func.getAttribute(NodeKey.VALUE);
        if (returnVal != null) {
            ConsoleDebugColor.outlnPurple("function call with name " + funcName + " has return value that is " + returnVal.toString());
            root.setAttribute(NodeKey.VALUE, returnVal);
        }
    } else {
        ClibCall libCall = ClibCall.getInstance();
        if (libCall.isApiCall(funcName)) {
            Object obj = libCall.invokeApi(funcName);
            root.setAttribute(NodeKey.VALUE, obj);
        }
    }
    break;

邏輯語句處理

邏輯語句處理無非就是根據節點值判斷該執行哪些節點

  • FOR、WHILE語句

程式碼邏輯和語句的邏輯是一樣,比如對於

for(i = 0; i < 5; i++){}

就會先執行i = 0部分,在執行{}和i++部分,然後再判斷條件是否符合

case SyntaxProductionInit.FOR_OptExpr_Test_EndOptExpr_Statement_TO_Statement:
executeChild(root, 0);

while (isLoopContinute(root, LoopType.FOR)) {
    //execute statement in for body
    executeChild(root, 3);
    //execute EndOptExpr
    executeChild(root, 2);
}
break;

case SyntaxProductionInit.While_LP_Test_Rp_TO_Statement:
while (isLoopContinute(root, LoopType.WHILE)) {
    executeChild(root, 1);
}
break;
  • IF語句

if語句就是先執行判斷部分,再根據判斷的結果來決定是否執行{}塊

@Override
public Object execute(AstNode root) {

    AstNode res = executeChild(root, 0);
    Integer val = (Integer)res.getAttribute(NodeKey.VALUE);
    copyChild(root, res);

    if (val != null && val != 0) {
        executeChild(root, 1);
    }

    return root;
}

小結

這一篇寫的很亂,一是直譯器部分還是蠻大的,想在一篇之內寫完比較難。所以省略了很多東西。但其實對於直譯器實現部分對於AST的遍歷才比較涉及編譯原理部分,其它的主要是邏輯實現

對於直譯器部分,因為沒有采用虛擬機器那樣的實現,而是直接對AST的遍歷。所以對AST的遍歷是關鍵,主要在於遍歷到該執行的子節點部分,然後處理邏輯,再把資訊通過子節點傳遞到父節點部分。