從零寫一個編譯器(十):編譯前傳之直接解釋執行
專案的完整程式碼在 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;
函式呼叫
函式呼叫也是屬於一元操作,對於函式呼叫有兩種情況:一種是自定義的函式,還有一種是直譯器提供的函式
- 如果是自定義函式,就找到這個函式的頭節點,從這個頭節點開始執行
- 如果是直譯器提供的函式,就交由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的遍歷是關鍵,主要在於遍歷到該執行的子節點部分,然後處理邏輯,再把資訊通過子節點傳遞到父節點部分。