从零写一个编译器(十):编译前传之直接解释执行
項目的完整代碼在 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的作用就是對節點設置屬性,把值往上傳遞
函數定義 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;函數調用
函數調用也是屬于一元操作,對于函數調用有兩種情況:一種是自定義的函數,還有一種是解釋器提供的函數
邏輯語句處理
邏輯語句處理無非就是根據節點值判斷該執行哪些節點
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 bodyexecuteChild(root, 3);//execute EndOptExprexecuteChild(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的遍歷是關鍵,主要在于遍歷到該執行的子節點部分,然后處理邏輯,再把信息通過子節點傳遞到父節點部分。
轉載于:https://www.cnblogs.com/secoding/p/11382017.html
總結
以上是生活随笔為你收集整理的从零写一个编译器(十):编译前传之直接解释执行的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从零写一个编译器(九):语义分析之构造抽
- 下一篇: 从零写一个编译器(十一):代码生成之Ja