前面講過語法的解析以後,代碼生成方面就簡單不少了。雖然myc是一個簡單的示例編譯器,可是它仍是在解析的過程當中生成了一個小的語法樹,這個語法樹將會用在生成exe可執行文件和il源碼的過程當中。數組
編譯器在解析時,使用emit類來產生中間的語法樹,語法樹的數據結構和操做方法在iasm這個類型裏完成,源程序的語法解析完畢後,Exe和Asm兩個類分別遍歷生成的語法樹產生最終的代碼。數據結構
咱們來看幾個代碼的例子,下表的函數 Parser.program 裏,在函數開始和結束的地方分別調用了 prolog 和 epilog 兩個函數,這兩個函數的目的就是在語法解析的先後執行一些準備和掃尾工做。如在編譯過程的開始階段,根據.net assembly的要求建立好模塊(module)和類(class),雖然c語言是一個面向過程的語言,可是在.net是一個面向對象的環境,全部的代碼都應該保存在一個類裏。app
public void program() { prolog(); while (tok.NotEOF()) { outerDecl(); } if (Io.genexe && !mainseen) io.Abort("Generating executable with no main entrypoint"); epilog(); } void prolog() { emit = new Emit(io); emit.BeginModule(); // need assembly module emit.BeginClass(); }
而在emit裏,BeginModule和BeginClass這兩個函數的代碼以下:dom
public void BeginModule() { // 委託給Exe類來建立這個module,雖然在.net裏,一個assembly能夠由 // 多個module組成,可是在C程序裏,只要一個module就足夠了,所以 // 下面的代碼並無在生成IL源碼的時候產生module。 exe.BeginModule(io.GetInputFilename()); } public void BeginClass() { // 委託給Exe類來建立class,再進一步跟蹤代碼的時候,會發現它其實 // 是根據反射技術來建立類型的。 exe.BeginClass(Io.GetClassname(), TypeAttributes.Public); // 若是在執行程序的命令行裏,啓用了生成源碼的開關,那麼 // 將會輸出IL class的源碼定義。 if (Io.genlist) io.Out(".class " + Io.GetClassname() + "{\r\n"); }
.NET裏,能夠使用反射技術來生成assembly、類型和函數,下表就是Exe類的BeginModule函數的源碼:函數
public void BeginModule(string ifile) { // .net的動態assembly建立功能,要求跟appdomain綁定 appdomain = System.Threading.Thread.GetDomain(); appname = getAssemblyName(filename); // 調用AppDomain.DefineDynamiceAssembly建立一個Assembly,以這個爲 // 起點,能夠建立類型,建立函數並執行。實際上,.net上的IronPython等 // 動態語言的實現就很是依賴這個技術。 appbuild = appdomain.DefineDynamicAssembly(appname, AssemblyBuilderAccess.Save, Io.genpath); // 在.net裏,全部的代碼實際上都應該保存在一個module裏。 emodule = appbuild.DefineDynamicModule( filename+"_module", Io.GetOutputFilename(), Io.gendebug); Guid g = System.Guid.Empty; if (Io.gendebug) srcdoc = emodule.DefineDocument(ifile, g, g, g); }
準備工做作好了之後,就能夠生成語法樹了,編譯器在解析語法的過程中,不停的往語法樹裏添加元素,如在編譯函數的過程當中,以處理while循環爲例(其中一個調用路徑是:program -> outerDecl -> declFunc -> blockOuter -> fcWhile)oop
void fcWhile() { // 在通常的il或者彙編語言裏,循環和判斷語句通常都是在不一樣路徑的入口 // 出定義好標籤(label),再經過判斷條件的方式跳轉到指定的label實現的 String label1 = newLabel(); String label2 = newLabel(); // 記錄當前源碼的位置,以便生成IL源碼的時候能夠把源代碼和IL代碼對照生成 CommentHolder(); /* mark the position in insn stream */ // 通常來講,循環語句至少有兩個分支代碼塊,一個是繼續循環的代碼塊, // 一個是跳出循環的代碼塊,看後面的代碼,這個label是循環中執行的代碼塊 // 開始的地方,以便知足條件的時候跳到開頭繼續執行 emit.Label(label1); tok.scan(); // 作一些錯誤判斷 if (tok.getFirstChar() != '(') io.Abort("Expected '('"); // 處理循環條件的判斷語句相關代碼 boolExpr(); CommentFillPreTok(); // 跳出循環的label emit.Branch("brfalse", label2); // 循環內部的代碼塊,進入blockInner進行循環裏面的編譯 blockInner(label2, label1); /* outer label, top of loop */ // 若是知足循環條件,跳轉到代碼塊開頭繼續執行 emit.Branch("br", label1); // 循環結束,跳出循環的地方 emit.Label(label2); }
而在emit類型裏,各個方法只是將解析出來的語法元素添加到語法樹裏,語法樹的節點、數據結構和操做方法都在IAsm這個類裏定義,以下表是 Branch 的源碼:ui
public void Branch(String s, String lname) { // this is the branch source NextInsn(1); // 往語法樹裏添加一個類別爲 Branch 的元素 icur.setIType(IAsm.I_BRANCH); // 指令名稱 icur.setInsn(s); // 指令參數 icur.setLabel(lname); }
當程序編譯完成後,Exe類和Asm類則分別遍歷語法樹生成最終的結果,在myc編譯器的源碼裏,Parser.declFunc函數經過調用Emit.IL函數來完成程序的生成:this
// 由於C程序大部分都是由函數組成的,並且函數使用到的變量或者其餘函數, // 都必須在函數以前定義,因此只須要在解析函數的時候實時生成代碼便可 void declFunc(Var e) { #if DEBUG Console.WriteLine("declFunc token=["+tok+"]\n"); #endif CommentHolder(); // start new comment // 記錄解析出來的函數名 e.setName(tok.getValue()); /* value is the function name */ // 若是函數名是main,則設置一個標識位 - mainseen爲true // 在外層的函數裏,會經過判斷這個標誌來肯定程序是否有語義錯誤 if (e.getName().Equals("main")) { if (Io.gendll) io.Abort("Using main entrypoint when generating a DLL"); mainseen = true; } // 函數名也是一個全局變量,放到全局變量表裏,以便作語義分析 // 例如要調用的函數以前沒有定義,則應該報錯,在後文咱們將 // 看到語義方面的處理 staticvar.add(e); /* add function name to static VarList */ paramvar = paramList(); // track current param list e.setParams(paramvar); // and set it in func var // 記錄函數裏面定義的局部變量 localvar = new VarList(); // track new local parameters CommentFillPreTok(); // 開始生成函數的prolog,例如參數傳遞,this對象等 emit.FuncBegin(e); if (tok.getFirstChar() != '{') io.Abort("Expected ‘{'"); // 遞歸分析函數裏面的源碼 blockOuter(null, null); emit.FuncEnd(); // 解析完整個函數後,執行代碼生成操做 emit.IL(); // 若是須要生成IL源碼,則調用LIST函數生成IL源碼 if (Io.genlist) emit.LIST(); emit.Finish(); }
而emit.IL函數就是用Exe類型遍歷整個語法樹,生成結果程序:spa
public void IL() { IAsm a = iroot; IAsm p; // 循環遍歷整個語法樹 while (a != null) { // 根據語法樹裏各個節點的類型來執行對應的操做 switch (a.getIType()) { case IAsm.I_INSN: exe.Insn(a); break; case IAsm.I_LABEL: exe.Label(a); break; case IAsm.I_BRANCH: exe.Branch(a); break; // 省略一些代碼 default: io.Abort("Unhandled instruction type " + a.getIType()); break; } p = a; a = a.getNext(); } }
而Exe類型執行真正的代碼生成,如前面IL函數,在碰到I_BRANCH類型的節點時,調用Exe.Branch函數在動態Assembly (DynamicAssemby) 裏生成代碼:.net
public void Branch(IAsm a) { Object o = opcodehash[a.getInsn()]; if (o == null) Io.ICE("Instruction branch opcode (" + a.getInsn() + ") not found in hash」); // 使用 ILGenerator 類生成跳轉IL指令。 il.Emit((OpCode) o, (Label) getILLabel(a)); }
而Asm類也採用相似的方法生成IL源碼。
最後,myc編譯器裏也有一些語義方面的處理,如前面講到的函數調用時,若是被調用的函數沒有定義的話,應該拋出異常的狀況,在Parser.statement(即編譯實際的C語句的函數)中就有所體現
void statement() { Var e; String vname = tok.getValue(); CommentHolder(); /* mark the position in insn stream */ switch (io.getNextChar()) { case '(': /* this is a function call */ // 省略一些語法處理方面的代碼 tok.scan(); /* move to next token */ // 下面這一行即在生成函數調用代碼以前,在全局變量列表裏 // 查找要調用的函數是否已經定義了,若是沒有定義,則應該報告此錯誤 e = staticvar.FindByName(vname); /* find the symbol (e cannot be null) */ emit.Call(e); // 省略後面的代碼 } if (tok.getFirstChar() != ';') io.Abort("Expected ';'"); tok.scan(); }