前面講過語法的解析之后辆沦,代碼生成方面就簡單很多了玖像。雖然myc是一個簡單的示例編譯器,但是它還是在解析的過程中生成了一個小的語法樹蕾羊,這個語法樹將會用在生成exe可執(zhí)行文件和il源碼的過程中侮叮。
編譯器在解析時避矢,使用emit類來產生中間的語法樹,語法樹的數據結構和操作方法在iasm這個類型里完成,源程序的語法解析完畢后审胸,Exe和Asm兩個類分別遍歷生成的語法樹產生最終的代碼亥宿。
我們來看幾個代碼的例子,下表的函數 Parser.program 里砂沛,在函數開始和結束的地方分別調用了 prolog 和 epilog 兩個函數烫扼,這兩個函數的目的就是在語法解析的前后執(zhí)行一些準備和掃尾工作。如在編譯過程的開始階段碍庵,根據.net assembly的要求創(chuàng)建好模塊(module)和類(class)映企,雖然c語言是一個面向過程的語言,但是在.net是一個面向對象的環(huán)境静浴,所有的代碼都應該保存在一個類里堰氓。
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這兩個函數的代碼如下:
public void BeginModule()
{
// 委托給Exe類來創(chuàng)建這個module苹享,雖然在.net里双絮,一個assembly可以由
// 多個module組成,但是在C程序里得问,只要一個module就足夠了掷邦,因此
// 下面的代碼并沒有在生成IL源碼的時候產生module。
exe.BeginModule(io.GetInputFilename());
}
public void BeginClass()
{
// 委托給Exe類來創(chuàng)建class椭赋,再進一步跟蹤代碼的時候,會發(fā)現它其實
// 是根據反射技術來創(chuàng)建類型的或杠。
exe.BeginClass(Io.GetClassname(), TypeAttributes.Public);
// 如果在執(zhí)行程序的命令行里哪怔,啟用了生成源碼的開關,那么
// 將會輸出IL class的源碼定義向抢。
if (Io.genlist)
io.Out(".class " + Io.GetClassname() + "{\r\n");
}
.NET里认境,可以使用反射技術來生成assembly、類型和函數挟鸠,下表就是Exe類的BeginModule函數的源碼:
public void BeginModule(string ifile)
{
// .net的動態(tài)assembly創(chuàng)建功能叉信,要求跟appdomain綁定
appdomain = System.Threading.Thread.GetDomain();
appname = getAssemblyName(filename);
// 調用AppDomain.DefineDynamiceAssembly創(chuàng)建一個Assembly,以這個為
// 起點艘希,可以創(chuàng)建類型硼身,創(chuàng)建函數并執(zhí)行。實際上覆享,.net上的IronPython等
// 動態(tài)語言的實現就非常依賴這個技術佳遂。
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循環(huán)為例(其中一個調用路徑是:program -> outerDecl -> declFunc -> blockOuter -> fcWhile
)
void fcWhile()
{
// 在一般的il或者匯編語言里,循環(huán)和判斷語句一般都是在不同路徑的入口
// 出定義好標簽(label)煤搜,再通過判斷條件的方式跳轉到指定的label實現的
String label1 = newLabel();
String label2 = newLabel();
// 記錄當前源碼的位置免绿,以便生成IL源碼的時候可以把源代碼和IL代碼對照生成
CommentHolder(); /* mark the position in insn stream */
// 一般來說,循環(huán)語句至少有兩個分支代碼塊宅楞,一個是繼續(xù)循環(huán)的代碼塊针姿,
// 一個是跳出循環(huán)的代碼塊,看后面的代碼厌衙,這個label是循環(huán)中執(zhí)行的代碼塊
// 開始的地方距淫,以便滿足條件的時候跳到開頭繼續(xù)執(zhí)行
emit.Label(label1);
tok.scan();
// 做一些錯誤判斷
if (tok.getFirstChar() != '(')
io.Abort("Expected '('");
// 處理循環(huán)條件的判斷語句相關代碼
boolExpr();
CommentFillPreTok();
// 跳出循環(huán)的label
emit.Branch("brfalse", label2);
// 循環(huán)內部的代碼塊,進入blockInner進行循環(huán)里面的編譯
blockInner(label2, label1); /* outer label, top of loop */
// 如果滿足循環(huán)條件婶希,跳轉到代碼塊開頭繼續(xù)執(zhí)行
emit.Branch("br", label1);
// 循環(huán)結束榕暇,跳出循環(huán)的地方
emit.Label(label2);
}
而在emit類型里,各個方法只是將解析出來的語法元素添加到語法樹里喻杈,語法樹的節(jié)點彤枢、數據結構和操作方法都在IAsm這個類里定義,如下表是 Branch 的源碼:
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函數來完成程序的生成:
// 因為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();
// 解析完整個函數后,執(zhí)行代碼生成操作
emit.IL();
// 如果需要生成IL源碼焕檬,則調用LIST函數生成IL源碼
if (Io.genlist)
emit.LIST();
emit.Finish();
}
而emit.IL函數就是用Exe類型遍歷整個語法樹禽作,生成結果程序:
public void IL()
{
IAsm a = iroot;
IAsm p;
// 循環(huán)遍歷整個語法樹
while (a != null)
{
// 根據語法樹里各個節(jié)點的類型來執(zhí)行對應的操作
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類型執(zhí)行真正的代碼生成,如前面IL函數揩页,在碰到I_BRANCH類型的節(jié)點時旷偿,調用Exe.Branch函數在動態(tài)Assembly (DynamicAssemby) 里生成代碼:
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();
}