MYC編譯器源碼之語法分析

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

  1. 首先編譯器從最上面的語法規則 – program開始向下解析;
  2. program包含兩個規則,outer_decl和func_decl,由於outer_decl是第一個規則,因此編譯器開始嘗是當前的關鍵字是否匹配outer_decl的語法規則;
    outer_decl ::= [ class ] type ident { "," ident } ";";
    

     

  3. 而組成outer_decl規則的class, type, ident都是語法規則,不是詞法規則 – 即Token,這是由於在MyC的語法裏,咱們能找到class, type和ident的語法定義。其中 class 被方括號包圍,說明其是可選的。函數

  4. 編譯器向右再嘗試看當前的token是否跟type匹配,發現第一個單詞 int 跟type的語法規則匹配成功:
    type ::= "int" | "void";
    

      

  5. 到這一步,編譯器順利消化掉第一個Token – int,再繼續向右在outer_decl中匹配下一個Token,outer_decl規則裏下一個要求的是ident。所以編譯器判斷下一個Token是否是匹配ident:
    ident ::= name | function_call;
    name ::= letter { letter | digit };
    letter ::= "A-Za-z";
    digit ::= "0-9";
    

     

  6. 此時詞法分析器給出的下一個Token是a,匹配ident裏的name規則(向下匹配路徑是:ident -> name -> letter)。
  7. 編譯器繼續向右消化Token,此時源碼中還有一個分號‘;’還沒有匹配,而outer_decl裏還有下面這些規則:oop

    { "," ident } ";";
    

      

  8. 然而outer_decl中,大括號裏的規則是零到多個的可選項,所以源碼裏最後剩下的分號和outer_decl的最後一個分號匹配,編譯器到這時順利完成了一條C語句的分析,進而得知這條語句是一個全局變量的定義語句。

咱們再舉一個例子,來演示編譯器是如何處理函數定義的:this

void main()
{
  int c;
  int d;
}

  

  1. 與前面分析全局變量的方式同樣,編譯器自頂向下解析,首先嚐試outer_decl這個語法規則,而且從左向右根據 [class] type ident這些規則消化掉void main這兩個Token,然而在處理括號‘(’編譯器遇到麻煩,outer_decl裏沒有處理括號的規則。
  2. 編譯器只好往左回溯,將已經處理掉的void和main兩個token放回Token流,嘗試program的第二個規則func_decl。注意:這裏說的往左回溯,在人工編寫的語法分析器裏不多會這麼作 – 由於這樣的效率過低,在後面說明具體的源碼的時候你會看到myc編譯器是如何處理這種狀況的。
    func_decl ::= [ class ] type ident "(" params ")" outer_block;
    

      

  3. 與處理outer_decl的過程相似,編譯器從左往右處理掉void和main兩個Token,繼續向右處理的時候,此時源碼裏的Token – ‘(’跟func_decl下一個規則 」(」 是匹配的,所以編譯器能夠繼續自頂向下,從左往右根據func_decl的規則消化源碼裏剩下的Token。spa

  4. 在func_decl裏有一個規則頗有意思,inner_decl的定義和outer_decl的定義看起來是相似的,可是正是這樣的區分使得編譯器可以正確識別,同是 int a; 這樣的語句,是全局變量定義仍是函數內的局部變量定義 – 由於inner_decl只能從func_decl裏遞歸解析到。

剩下的語句解析部分,我就不在這裏多說了,請有興趣的網友本身找幾條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
  }
相關文章
相關標籤/搜索