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引數數值設定正確後,函式體就能正確執行了
函式體的執行在上一節我們曾經討論過,再次不再討論。請通過參看視訊獲得更詳細的程式碼講解和除錯演示過程,以便增加理解。
更多技術資訊,包括作業系統,編譯器,面試演算法,機器學習,人工智慧,請關照我的公眾號: