MYC編譯器源碼之代碼生成

前面講過語法的解析以後,代碼生成方面就簡單不少了。雖然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();
  }
相關文章
相關標籤/搜索