1. 程式人生 > >java實現C語言編譯器:實現有引數的函式呼叫

java實現C語言編譯器:實現有引數的函式呼叫

更詳細的講解和程式碼除錯演示過程,請參看視訊
用java開發C語言編譯器

上一節,我們實現了沒有引數傳遞的函式呼叫,本節,我們看看如何實現有引數傳遞的函式呼叫。

有引數的函式呼叫要比無引數的函式呼叫複雜的多,一個難題在於,我們需要確定引數變數的作用域,例如下面的程式碼:

int a;

void f(int a, int b) {
    int c;
    c = a + b;
}

在程式碼裡,有兩個同名變數都成為a, 顯然,這兩個變數作用範圍不同,他們的存在並不矛盾。當兩個變數存在符號表中時,由於名字相同,要實現引數傳遞時,必須確定引數的值傳遞給正確的變數a, 如果傳遞出錯了,那麼整個程式執行的邏輯就混亂了。

因此,要實現有引數的函式呼叫,首要問題是確保把數值傳遞給對應左右域的相應變數。那麼,如何確定變數的作用域呢。根據我們的程式碼實現,每個變數都對應一個Symbol物件,我們在該物件裡新增一個字串變數,用來表明該物件對應變數的作用域,如果變數是全域性變數,那麼它的作用域字串內容為”global”,如果變數是某個函式的引數,或是定義在函式體內,那麼該變數的作用域字串就可以用該函式的名字來定義。

以上面程式碼為例,第一個變數a,作用域字串是”global”, 第二個變數a,作用域範圍是”f”, 也就是它對應的函式名字。我們看看程式碼如何實現這個功能。

一條變數定義的程式碼語句,例如:
int a;
對應的語法表示式為:
EXT_DEF -> OPT_SPECIFIERS EXT_DECL_LIST SEMI

因此,當語法解析器讀入一條語句,然後解析成上面的語句時,我們就知道,當前程式碼正在定義一個變數,此時就可以設定該變數的作用域字串了,程式碼如下:

private void takeActionForReduce(int productNum) {
...
case CGrammarInitializer.OptSpecifier_ExtDeclList_Semi_TO_ExtDef:
        case CGrammarInitializer.TypeNT_VarDecl_TO_ParamDeclaration:
        case CGrammarInitializer.Specifiers_DeclList_Semi_TO_Def:
            Symbol symbol = (Symbol)attributeForParentNode;
            TypeLink specifier = (TypeLink)(valueStack.get(valueStack.size() - 3
)); typeSystem.addSpecifierToDeclaration(specifier, symbol); typeSystem.addSymbolsToTable(symbol, symbolScope); ... }

我們早在語法解析是講解過上面程式碼,當C語言定義一個變數時,上面對應的case程式碼會被執行,進而為生成的變數對應的Symbol物件,在把Symbol物件加入符號表時,需要多新增一個變數,就是symbolScope,這是一個字串全域性變數,用來代表當前變數所在的作用域,它的初始化方式如下:

public class LRStateTableParser {
    private Lexer lexer;
    int    lexerInput = 0;
    int    nestingLevel = 0;
    int    enumVal = 0;
    String text = "";
    public static final String GLOBAL_SCOPE = "global";
    public String symbolScope = GLOBAL_SCOPE;
    ...
    }

它一開始時就被初始化為”global”,因此,我們的解析器在遇到變數宣告時,先把當前變數設定為global的,如果在後面的解析中,發現該變數並不是全域性變數,那麼在後面再修改該變數的作用範圍。例如函式呼叫:

void f(int a, int b) {
    int c;
    c = a + b;
}

兩個引數a,b,在第一次解析時,根據前面的case程式碼,會先為這兩個變數的作用域字串設定為”global”,然後繼續解析,等到解析器解析完整個函式頭,也就是當直譯器根據以下表達式進行遞迴時:

FUNCT_DECL -> NEW_NAME LP VAR_LIST RP
FUNCT_DECL -> NEW_NAME LP RP

此時,我們可以得知,當前解析器解析的程式碼是函式頭部定義,這個時候,我們將改變symbolScope的內容,把它變成當前解析函式的函式名,這樣,接下來遇到變數宣告時,它對應的作用域字串就會變成當前的函式名。

前面我們說過,引數定義時,解析器首先會把它的作用域設定為global,即使它是函式的引數,現在我們解析到函式頭定義了,這時候是把原來引數的作用域範圍更改為對應函式的正確時機,因此程式碼如下:

private void takeActionForReduce(int productNum) {
   ....
case CGrammarInitializer.NewName_LP_VarList_RP_TO_FunctDecl:
            setFunctionSymbol(true);
            Symbol argList = (Symbol)valueStack.get(valueStack.size() - 2);
            ((Symbol)attributeForParentNode).args = argList;
            typeSystem.addSymbolsToTable((Symbol)attributeForParentNode, symbolScope);
            //遇到函式定義,變數的scope名稱要改為函式名,並把函式引數的scope改為函式名
            symbolScope = ((Symbol)attributeForParentNode).getName();
            Symbol sym = argList;
            while (sym != null) {
                sym.addScope(symbolScope);
                sym = sym.getNextSymbol();
            }
            break;

        case CGrammarInitializer.NewName_LP_RP_TO_FunctDecl:
            setFunctionSymbol(false);
            typeSystem.addSymbolsToTable((Symbol)attributeForParentNode, symbolScope);
            //遇到函式定義,變數的scope名稱要改為函式名
            symbolScope = ((Symbol)attributeForParentNode).getName();

            break;
   ....
}

從上面程式碼我們可以看到,在進入到函式頭的解析時,直譯器會把symbolScope設定為函式名,如果當前的case 等於CGrammarInitializer.NewName_LP_VarList_RP_TO_FunctDecl時,通過argList變數獲得函式引數對應的輸入引數變數連結串列,這個連結串列的建立,我們在前面章節中已經詳細解釋過。然後通過引數連結串列變數每個引數,把引數的作用域字串改為對應的函式名。

等到函式全部解析完畢後,變數的作用域就得重新轉變為global, 根據前一節內容,我們知道,函式定義解析完畢對應的語法表示式為:
EXT_DEF -> OPT_SPECIFIERS FUNCT_DECL COMPOUND_STMT

當直譯器根據上面的表示式進行遞迴時,我們知道,當前狀態是函式解析結束,因此,我們在這時就需要把symbolScope的內容,重新改為global.程式碼如下:

 private void takeActionForReduce(int productNum) {
        switch(productNum) {
        ...
        case CGrammarInitializer.OptSpecifiers_FunctDecl_CompoundStmt_TO_ExtDef:
            symbol = (Symbol)valueStack.get(valueStack.size() - 2);
            specifier = (TypeLink)(valueStack.get(valueStack.size() - 3));
            typeSystem.addSpecifierToDeclaration(specifier, symbol);

            //函式定義結束後,接下來的變數作用範圍應該改為global
            symbolScope = GLOBAL_SCOPE;
            break;
        ...
        }

請大家通過視訊檢視程式碼的講解和除錯過程,以便獲得更詳細的理解。

接下來,我們看看函式是如何傳遞的。上一節,我們知道,沒有引數輸入的函式呼叫對應的語法表示式是:

UNARY -> UNARY LP RP

那麼如果,函式呼叫有引數的話,其對應的語法表示式是:

UNARY -> UNARY LP ARGS RP
ARGS -> NO_COMMA_EXPR
ARGS -> NO_COMMA_EXPR COMMA ARGS

由此,我們需要構造一個ARGS對應的節點,程式碼如下:

public ICodeNode buildCodeTree(int production, String text) {
        ICodeNode node = null;
        Symbol symbol = null;

        switch (production) {
        ...
        case CGrammarInitializer.NoCommaExpr_TO_Args:
            node = ICodeFactory.createICodeNode(CTokenType.ARGS);
            node.addChild(codeNodeStack.pop());
            break;

        case CGrammarInitializer.NoCommaExpr_Comma_Args_TO_Args:
            node = ICodeFactory.createICodeNode(CTokenType.ARGS);
            node.addChild(codeNodeStack.pop());
            node.addChild(codeNodeStack.pop());
            break;
        ...
        }

由此,我們還需要構造一個ARGS節點對應的Executor物件,以便實現引數解析,程式碼如下:

package backend;

import java.util.ArrayList;

import frontend.CGrammarInitializer;

public class ArgsExecutor extends BaseExecutor {

    @Override
    public Object Execute(ICodeNode root) {
        int production = (Integer)root.getAttribute(ICodeKey.PRODUCTION);
        ArrayList<Object> argList = new ArrayList<Object>();
        ICodeNode child ;
        switch (production) {
        case CGrammarInitializer.NoCommaExpr_TO_Args:
            child = (ICodeNode)executeChild(root, 0);
            int val = (Integer)child.getAttribute(ICodeKey.VALUE);
            argList.add(val);
            break;

        case CGrammarInitializer.NoCommaExpr_Comma_Args_TO_Args:
            child = executeChild(root, 0);
            val = (Integer)child.getAttribute(ICodeKey.VALUE);
            argList.add(val);

            child = (ICodeNode)executeChild(root, 1);
            ArrayList<Object> list = (ArrayList<Object>)child.getAttribute(ICodeKey.VALUE);
            argList.addAll(list);
            break;
        }

        root.setAttribute(ICodeKey.VALUE, argList);
        return root;
    }

}

對應函式呼叫,例如f(1,2,3) ,ArgsExecutor 的作用是構造一個引數佇列:
3 -> 2 -> 1, 然後把這個佇列交給函式的執行物件,也就是ExtDefExecutor.
ArgsExecutor 先通過子執行子節點,把數字字串讀取成對應的數值,然後再把這些數值加入一個佇列中返回。

同時UnaryExecutor也要做相應變化,它需要讓ArgsExecutor執行後,獲取引數的數值列表,以便傳遞給ExtDefExecutor,對應的程式碼改動如下:

public class UnaryNodeExecutor extends BaseExecutor{

    @Override
    public Object Execute(ICodeNode root) {
        executeChildren(root);

        int production = (Integer)root.getAttribute(ICodeKey.PRODUCTION); 
        String text ;
        Symbol symbol;
        Object value;
        ICodeNode child;

        switch (production) {
        ...
        case CGrammarInitializer.Unary_LP_RP_TO_Unary:
        case CGrammarInitializer.Unary_LP_ARGS_RP_TO_Unary:
            //先獲得函式名
            String funcName = (String)root.getChildren().get(0).getAttribute(ICodeKey.TEXT);
            if (production == CGrammarInitializer.Unary_LP_ARGS_RP_TO_Unary) {
                ICodeNode argsNode = root.getChildren().get(1);
                ArrayList<Object> argList = (ArrayList<Object>)argsNode.getAttribute(ICodeKey.VALUE);
                FunctionArgumentList.getFunctionArgumentList().setFuncArgList(argList); 
            }

            //找到函式執行樹頭節點
            ICodeNode func = CodeTreeBuilder.getCodeTreeBuilder().getFunctionNodeByName(funcName);
            if (func != null) {
                Executor executor = ExecutorFactory.getExecutorFactory().getExecutor(func);

                executor.Execute(func);
            }
            break;
        ...
        }

executeChildren(root);首先讓孩子節點先執行,由於ARGS是UNARY的孩子節點,所以executeChildren會讓ArgsExecutor先執行,這樣就可以獲取引數數值列表。然後通過ARGS節點得到引數列表,也就是執行下面語句得到引數列表:

ICodeNode argsNode = root.getChildren().get(1);
                ArrayList<Object> argList = (ArrayList<Object>)argsNode.getAttribute(ICodeKey.VALUE);

有了列表之後,怎麼把列表傳遞給函式執行體呢,是通過一個單子物件儲存的:

package backend;

import java.util.ArrayList;

public class FunctionArgumentList {
    private static FunctionArgumentList argumentList = null;
    private ArrayList<Object> funcArgList = new ArrayList<Object>();

    public static FunctionArgumentList getFunctionArgumentList() {
        if (argumentList == null) {
            argumentList = new FunctionArgumentList();
        }

        return argumentList;
    }

    public void setFuncArgList(ArrayList<Object> list) {
        funcArgList = list;
    }

    public ArrayList<Object> getFuncArgList() {
        return funcArgList;
    }

    private FunctionArgumentList() {}
}

UnaryExecutor將獲得的列表放入FunctionArgumentList物件,然後從根據要呼叫的函式名,從函式雜湊表中找到函式執行樹的頭結點,接著再通過ExtDefExecutor去執行函式體內的語句。

有了引數列表,接下來要做的是把引數列表對應的數值傳遞給引數,這樣函式執行時才能獲得輸入的數值,數值傳遞是由FunctDeclExecutor實現的,程式碼如下:

public class FunctDeclExecutor extends BaseExecutor {
    private ArrayList<Object> argsList = null;
    private ICodeNode currentNode;
    @Override
    public Object Execute(ICodeNode root) {
    switch (production) {
    ...
    case  CGrammarInitializer.NewName_LP_VarList_RP_TO_FunctDecl:
            symbol = (Symbol)root.getAttribute(ICodeKey.SYMBOL);
            //獲得引數列表
            Symbol args = symbol.getArgList();
            initArgumentList(args);

            if (args == null || argsList == null || argsList.isEmpty()) {
                //如果引數為空,那就是解析錯誤
                System.err.println("Execute function with arg list but arg list is null");
                System.exit(1);
            }

            break;
    ...
    }
private void initArgumentList(Symbol args) {
        if (args == null) {
            return;
        }


        argsList = FunctionArgumentList.getFunctionArgumentList().getFuncArgList();
        Collections.reverse(argsList);
        Symbol eachSym = args;
        int count = 0;
        while (eachSym != null) {
            IValueSetter setter = (IValueSetter)eachSym;
            try {
                /*
                 * 將每個輸入引數設定為對應值並加入符號表
                 */
                setter.setValue(argsList.get(count));
                count++;
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            eachSym = eachSym.getNextSymbol();
        }
    }

}

它先通過FunctionArgumentList獲得引數數值列表,然後找到對應的引數symbol物件列表,逐個把傳入數值設定到引數對應的symbol中,當symbol引數數值設定正確後,函式體就能正確執行了

函式體的執行在上一節我們曾經討論過,再次不再討論。請通過參看視訊獲得更詳細的程式碼講解和除錯演示過程,以便增加理解。

更多技術資訊,包括作業系統,編譯器,面試演算法,機器學習,人工智慧,請關照我的公眾號:
這裡寫圖片描述