MYC編譯器源碼之詞法分析

前文  .NET框架源碼解讀之MYC編譯器 和 MYC編譯器源碼分析之程序入口 分別講解了 SSCLI 裏示例編譯器的架構和程序入口,本文接着分析它的詞法分析部分的代碼。html

詞法解析的工做都由Tok類處理,其構造函數接受一個Io對象作文件處理,下面是Tok構造函數的源碼:git

public Tok(Io ihandle)
{
    io = ihandle;
    // 初始化Token(字符歸類)字典
    InitHash();            // initialize the tokens hashtable
    // 讀入文件的第一個字符
    io.ReadChar();
    // 逐個掃描文件裏的字符,獲取
    // 第一個字符歸類(Token)
    scan();
}

 

構造函數中第一個函數調用InitHash的目的是將關鍵字和操做符解析成更容易識別的字符類型識別號 - Token,這樣作的目的是爲了便於語法解析器parser處理。例如,對於下面這條C語句: 程序員

int foo(int a)

 

與其讓語法解析器去逐個處理單個字符,詞法解析器的做用是將去上面一行語句歸類成相似下面的格式: 編程

T_INT T_IDENT ‘(‘ T_INT T_IDENT ‘)’

 

由於T_INT,T_IDENT都是一個整數型常量,而’(‘這樣的單個字符也能夠看成整數型常量對待,這樣語法解析器在分析語法的時候工做會更輕鬆些。因此在InitHash函數裏,其把編程語言裏全部的關鍵字和多字符操做符(如左移賦值操做符 <<=)都設置了類型標識號(Token),在Tok對象的scan()函數掃描源文件時,會逐一在這個字典裏查詢關鍵字的標識號:數組

public void InitHash()
{
    // 爲字符類型識別號對照表 – tokens分配空間
    tokens = new Hashtable();
    AddTok(T_LEFT_ASSIGN,    "<<=");     
    // ... ...
    AddTok(T_IF,        "if");
    // ... ...
    AddTok(T_STATIC,        "static");
    AddTok(T_INT,        "int");
    // ... ...
}

 

而對應的每一個標識號(Token)的定義,則能夠在Tok.cs源文件的最上面找到: 緩存

public const int T_LEFT_ASSIGN    = 10001;
// ... ...
public const int T_IF            = 20001;
// ... ...
public const int T_STATIC        = 30002;
// ... ...
public const int T_INT        = 40003;
// ... ...
public const int T_IDENT         = 50001;
public const int T_DIGITS         = 50002;
public const int T_UNKNOWN        = 99999;
public const int T_EOF         = -1;

 

字符類型識別號對照表初始化完畢後,語法分析器就能夠調用Tok對象的scan函數進行語法處理了,scan函數每次只處理並返回一個字符類型: 架構

public void scan()
{
    // 跳過註釋、換行符、空格等字符
    skipWhite();
    // 先判斷當前讀取的字符是否是一個字母
    // 若是是字母開頭的話,要麼是關鍵字,
    // 要麼就是變量名
    if (Char.IsLetter(io.getNextChar()))
      // 逐個掃描後面的字符,直到識別出關鍵字
      // 或者變量名爲止才退出
      LoadName();
    // 若是當前的字符是 0 - 9的數字
    else if (Char.IsDigit(io.getNextChar()))
      // 掃描完後面的數字並歸類
      LoadNum();
    // 若是是操做符,掃描完後面的操做符字符串
    else if (isOp(io.getNextChar()))
      LoadOp();
    // 若是文件已經讀取完畢了
    else if (io.EOF())
      {
      // 返回特殊的識別符 T_EOF,表示文件讀取完畢
      value = null;
      token_id = T_EOF;
      }
    else
      {
      // 這個字符不是一個合法的字符,歸類成T_UNKNOWN
      // T_UNKNOWN沒有被任何語法引用
      // 若是語法分析器在掃描語法的過程當中
      // 看到這個識別符,頗有多是源碼裏有語法錯誤
      value = new StringBuilder(MyC.MAXSTR);
      value.Append(io.getNextChar());
      token_id = T_UNKNOWN;
      io.ReadChar();
      }
    skipWhite();
    // 條件編譯,若是是myc.exe是調試版本,則在命令行裏
    // 打印出當前識別的字符類型,便於myc.exe的開發者排錯
#if DEBUG
    Console.WriteLine("[tok.scan tok=["+this+"]");
#endif
}

 

scan函數是Tok對象裏最核心的函數,它其實是完成前面myc語法裏這些詞法規則(還有隱含的關鍵字和操做符識別):框架

letter ::= "A-Za-z";
digit ::= "0-9";

name ::= letter { letter | digit };
integer ::= digit { digit };

 

咱們再經過說明LoadName函數來解釋詞法分析的細節: 編程語言

void LoadName()
{
  // 緩存讀取到的字符
  value = new StringBuilder(MyC.MAXSTR);
  skipWhite();  
  // 錯誤驗證 - 確保第一個字符是字母
  if (!Char.IsLetter(io.getNextChar()))
    throw new ApplicationException("?Expected Name");
  // 後面跟着的字符只能是數字或者字母
  while (Char.IsLetterOrDigit(io.getNextChar()))
    {
    // 緩存字符,以便判斷是變量名,仍是關鍵字
    value.Append(io.getNextChar());
    // 從源文件裏讀取下一個字符
    io.ReadChar();
    }
  // 在字符類型識別表裏查詢讀取到的詞組是否是關鍵字
  token_id = lookup_id();
  // 不是關鍵字的話,那麼就是變量名(或函數名)
  if (token_id <= 0)
    token_id = T_IDENT;
  skipWhite();
}

 

上面基本上就是詞法分析的關鍵代碼了,不過在說明的時候,我特地跳過了構造函數的 io.ReadChar()這個函數,這個函數從字面意義上看是讀取一個字符,但實際上從源文件一個字符一個字符的讀取效率實在是過低了,所以通常都是從源文件裏讀取一大段字符並緩存在內存裏,提升效率:函數

// Io.cs – ReadChar函數

public void ReadChar()
{
  // 判斷是否是讀到文件末尾了
  if (_eof)            // if already eof, nothing to do here
    return;
  // 若是緩存尚未實例化,或者緩存裏的字符
  // 已經處理完畢了,建立一個新的緩存
  // 對於老的緩存數組,丟給垃圾回收機制處理
  if (ibuf == null || ibufidx >= MyC.MAXBUF)
    {
    ibuf = new char[MyC.MAXBUF];
    _eof = false;
    // 從源文件裏讀取一大塊內容到緩存裏
    ibufread = rfile.Read(ibuf, 0, MyC.MAXBUF);
    ibufidx = 0;
    if (buf == null)
      buf = new StringBuilder(MyC.MAXSTR);
    }
  // 從緩存裏讀取下一個字符
  look = ibuf[ibufidx++];
  // 判斷此次讀取時,是否已經到源文件末尾了
  if (ibufread < MyC.MAXBUF && ibufidx > ibufread)
    _eof = true;

  /*
   * track the read characters
   */
  // 保存當前讀取的字符,以便在生成IL源文件的時候
  // 能夠把C源碼跟生成的IL源碼對應起來
  buf.Append(look);
  // 若是碰到換行,更新行號,行號在報告語法錯誤
  // 的時候會用到,告知具體語法出錯的行號便於
  // 程序員找到錯誤
  if (look == '\n')
    bufline++;
}

 

在Io.ReadChar函數裏,會保存讀取的C源碼,當要生成IL源文件的時候,這個信息用來保存C語句跟IL語句的對應關係,如用下面的命令編譯myc裏自帶的測試源碼文件:

效果以下圖:

相關文章
相關標籤/搜索