MyC編譯器採用自頂向下的方法進行語法解析,這種語法解析方式,通常是從最左邊的Token開始,而後自頂向下看哪一條語法規則可能包含這個Token,若是包含這個Token,則自左向右根據這條語法規則逐一匹配後面的Token。自頂向下的語法解析我會在其餘文章中說明,在前文咱們已經列出了MyC的語法規則:git
program ::= ( outer_decl | func_decl ); outer_decl ::= [ class ] type ident { "," ident } ";"; func_decl ::= [ class ] type ident "(" params ")" outer_block; outer_block ::= "{" { inner_decl } { stmt } "}"; inner_decl ::= [ class ] type ident { "," ident } ";"; inner_block ::= "{" { stmt } "}"; ident ::= name | function_call; function_call ::= name "(" [expr {, expr}] ")"; params ::= type ident { , type ident }; stmt ::= ( if_stmt | while_stmt | for_stmt | break_stmt | cont_stmt | ret_stmt | assign_stmt ); if_stmt ::= "if" "(" expr ")" stmt_block [ "else" inner_block ]; while_stmt ::= "while" "(" expr ")" inner_block; for_stmt ::= "for" "(" assign ";" expr ";" assign ")" inner_block break_stmt ::= "break" ";"; cont_stmt ::= "continue" ";"; ret_stmt ::= "return" expr ";"; assign_stmt ::= assign ";" ; assign = ident "=" expr; class ::= "extern" | "static" | "auto"; type ::= "int" | "void"; factor ::= (ident | integer | "(" expr ")" ); unary_factor ::= ["+"|"-"] factor; term1 ::= ["*"|"/"] factor; term0 ::= factor { term1 }; first_term ::= unary_factor term1; math_expr ::= first_term { ["+"|"-"] term0 } rel_expr ::= math_expr ("=="|"!="|"<"|">"|">="|"<=") math_expr; not_factor ::= ["!"] rel_expr; term_bool ::= not_factor { ("&" | "&&") not_factor }; bool_expr ::= term_bool { ("|" | "^") term_bool }; expr ::= bool_expr; name ::= letter { letter | digit }; integer ::= digit { digit }; letter ::= "A-Za-z"; digit ::= "0-9";
咱們用幾個例子來講明自頂向下的解析過程,好比下面這個全局變量的定義:ruby
int a;
採用自頂向下的過程以下:ide
outer_decl ::= [ class ] type ident { "," ident } ";";
而組成outer_decl規則的class, type, ident都是語法規則,不是詞法規則 – 即Token,這是由於在MyC的語法裏,咱們能找到class, type和ident的語法定義。其中 class 被方括號包圍,說明其是可選的。函數
type ::= "int" | "void";
ident ::= name | function_call; name ::= letter { letter | digit }; letter ::= "A-Za-z"; digit ::= "0-9";
編譯器繼續向右消化Token,此時源碼中還有一個分號‘;’還沒有匹配,而outer_decl裏還有下面這些規則:oop
{ "," ident } ";";
咱們再舉一個例子,來演示編譯器是如何處理函數定義的:this
void main() { int c; int d; }
func_decl ::= [ class ] type ident "(" params ")" outer_block;
與處理outer_decl的過程相似,編譯器從左往右處理掉void和main兩個Token,繼續向右處理的時候,此時源碼裏的Token – ‘(’跟func_decl下一個規則 」(」 是匹配的,所以編譯器能夠繼續自頂向下,從左往右根據func_decl的規則消化源碼裏剩下的Token。spa
剩下的語句解析部分,我就不在這裏多說了,請有興趣的網友本身找幾條C語句對着上面的語法自頂向下過一遍。對象
下面咱們開始分析MyC的語法解析源碼,這些功能都由parse類完成。Parse類的構造函數接受兩個參數:Io對象和Tok對象,這裏Io對象主要就是兩個用處,一個是在語法解析過程當中記錄當前源碼的位置,另外一個是輸出些錯誤消息,所以這裏咱們就不放什麼篇幅說明Io對象了。blog
public Parse(Io i, Tok t) { io = i; tok = t; // 初始化靜態變量列表 staticvar = new VarList(); }
Parse對象建立以後,實際的解析過程是由program函數處理的 – 即在myc.cs的Main函數裏調用,大體瀏覽下源碼,你應該就能夠發現函數命名跟前面的語法規則名很是重合,如program、outerDecl等函數,由於語法解析的函數思路都差很少,這裏我挑幾個關鍵的函數說明下:遞歸
public void program() { // 準備要生成的模塊信息 prolog(); // 循環消耗源碼裏的Token流 while (tok.NotEOF()) { // 雖然名字叫outerDecl,實際上包含了兩條語法規則,即program // 規則裏的outer_decl和func_decl outerDecl(); } // 錯誤處理 if (Io.genexe && !mainseen) io.Abort("Generating executable with no main entrypoint"); // 結束代碼生成 epilog(); }
MyC的語法很簡單,所以在program函數裏就一併將語法解析和代碼生成作掉了。因爲C語言是沒有類的概念的,而.NET的IL卻又是一個面向對象的中間語言,因此在program函數的一開始調用prolog函數,一方面是爲正在編譯的C程序生成一個默認的對象,另外一方面,因爲.NET的可執行文件Assembly其實是能夠由多個模塊 – Module組成的,因此在prolog函數裏也順便生成了一個默認模塊。下面是prolog函數的源碼:
void prolog() { // 建立代碼生成對象 emit = new Emit(io); // 準備最終包含C程序的.NET模塊 emit.BeginModule(); // need assembly module // 生成一個默認的類 emit.BeginClass(); }
outerDecl函數裏負責處理program的兩個語法規則outer_decl和func_decl:
void outerDecl() { // 保存當前解析的C語句中變量的信息,如變量名 // 函數名、變量類型等信息 Var e = new Var(); #if DEBUG Console.WriteLine("outerDecl token=["+tok+"]\n"); #endif // 記錄當前源碼位置,以便在結果IL文件(若是要生成IL的話) // 中保存位置信息 CommentHolder(); /* mark the position in insn stream */ // 處理 outer_decl 和 func_decl 規則共有的 [class] 規則 dataClass(e); // 處理 outer_decl 和 func_decl 規則共有的 type 規則 dataType(e); // 判斷下一個字符是不是左括號,若是是的話,則按 // func_decl規則處理 if (io.getNextChar() == '(') declFunc(e); // 不然按outer_decl規則處理 else declOuter(e); } // 解析outer_decl語法規則的剩餘部分 void declOuter(Var e) { #if DEBUG Console.WriteLine("declOuter1 token=["+tok+"]\n"); #endif // 前面在outerDecl函數裏已經處理過 [class] 和 type 規則了 // 所以目前Token流的第一個Token時ident,也就是變量名 // 這裏將變量名賦值給e - 即由outerDecl建立的變量名 e.setName(tok.getValue()); /* use value as the variable name */ // 將這個變量保存到全局變量列表裏,以備後面語義分析時使用 addOuter(e); /* add this variable */ // 在結果語法樹裏建立一個變量聲明節點 emit.FieldDef(e); /* issue the declaration */ // 若是當前語句有匹配 [class] 規則的部分,即語句的前面有static, // extern 這些關鍵字,把這個信息也保存到變量聲明節點裏,以便 // 後面生成代碼時參考 if (e.getClassId() == Tok.T_DEFCLASS) e.setClassId(Tok.T_STATIC); /* make sure it knows its storage class */ // 處理 outer_decl 規則裏 { "," ident } 這個多變量聲明部分 // 即處理相似:int a, b, c; 這樣的變量聲明語句 // 這個過程是經過判斷後面的Token是不是 ',' 來完成的 /* * loop while there are additional variable names */ while (io.getNextChar() == ',') { // 後面跟着 ',',那麼先消化掉這個字符 tok.scan(); if (tok.getFirstChar() != ',') io.Abort("Expected ','"); // 向右掃描 tok.scan(); // 嘗試找到一個匹配 ident 規則的Token if (tok.getId() != Tok.T_IDENT) io.Abort("Expected identifier"); // 找到一個變量名 - 即匹配 ident 規則的Token e.setName(tok.getValue()); /* use value as the variable name */ // 將這個新的全局變量添加到全局變量表裏 addOuter(e); /* add this variable */ // 固然也要在結果語法樹裏保存這個變量聲明節點 emit.FieldDef(e); /* issue the declaration */ if (e.getClassId() == Tok.T_DEFCLASS) e.setClassId(Tok.T_STATIC); /* make sure it knows its storage class */ } // 消化完前面的變量定義,看看後面跟着的字符是否是分號 - ';' /* * move beyond end of statement indicator */ tok.scan(); if (tok.getFirstChar() != ';') io.Abort("Expected ';'"); // 順利解析完一條語句,掃尾處理 CommentFill(); tok.scan(); #if DEBUG Console.WriteLine("declOuter2 token=["+tok+"]\n"); #endif }