MYC編譯器源碼之詞法分析

詞法解析

詞法解析的工作都由Tok類處理,其構(gòu)造函數(shù)接受一個(gè)Io對(duì)象做文件處理,下面是Tok構(gòu)造函數(shù)的源碼:

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

構(gòu)造函數(shù)中第一個(gè)函數(shù)調(diào)用InitHash的目的是將關(guān)鍵字和操作符解析成更容易識(shí)別的字符類型識(shí)別號(hào) - Token,這樣做的目的是為了便于語(yǔ)法解析器parser處理。例如,對(duì)于下面這條C語(yǔ)句:

int foo(int a)

與其讓語(yǔ)法解析器去逐個(gè)處理單個(gè)字符,詞法解析器的作用是將去上面一行語(yǔ)句歸類成類似下面的格式:

T_INT T_IDENT ‘(‘ T_INT T_IDENT ‘)’

因?yàn)門_INT,T_IDENT都是一個(gè)整數(shù)型常量,而’(‘這樣的單個(gè)字符也可以當(dāng)作整數(shù)型常量對(duì)待,這樣語(yǔ)法解析器在分析語(yǔ)法的時(shí)候工作會(huì)更輕松些。所以在InitHash函數(shù)里,其把編程語(yǔ)言里所有的關(guān)鍵字和多字符操作符(如左移賦值操作符 <<=)都設(shè)置了類型標(biāo)識(shí)號(hào)(Token),在Tok對(duì)象的scan()函數(shù)掃描源文件時(shí),會(huì)逐一在這個(gè)字典里查詢關(guān)鍵字的標(biāo)識(shí)號(hào):

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

而對(duì)應(yīng)的每個(gè)標(biāo)識(shí)號(hào)(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;

字符類型識(shí)別號(hào)對(duì)照表初始化完畢后,語(yǔ)法分析器就可以調(diào)用Tok對(duì)象的scan函數(shù)進(jìn)行語(yǔ)法處理了,scan函數(shù)每次只處理并返回一個(gè)字符類型:

public void scan()
{
    // 跳過(guò)注釋、換行符、空格等字符
    skipWhite();
    // 先判斷當(dāng)前讀取的字符是不是一個(gè)字母
    // 如果是字母開(kāi)頭的話,要么是關(guān)鍵字,
    // 要么就是變量名
    if (Char.IsLetter(io.getNextChar()))
      // 逐個(gè)掃描后面的字符,直到識(shí)別出關(guān)鍵字
      // 或者變量名為止才退出
      LoadName();
    // 如果當(dāng)前的字符是 0 - 9的數(shù)字
    else if (Char.IsDigit(io.getNextChar()))
      // 掃描完后面的數(shù)字并歸類
      LoadNum();
    // 如果是操作符,掃描完后面的操作符字符串
    else if (isOp(io.getNextChar()))
      LoadOp();
    // 如果文件已經(jīng)讀取完畢了
    else if (io.EOF())
      {
      // 返回特殊的識(shí)別符 T_EOF,表示文件讀取完畢
      value = null;
      token_id = T_EOF;
      }
    else
      {
      // 這個(gè)字符不是一個(gè)合法的字符,歸類成T_UNKNOWN
      // T_UNKNOWN沒(méi)有被任何語(yǔ)法引用
      // 如果語(yǔ)法分析器在掃描語(yǔ)法的過(guò)程中
      // 看到這個(gè)識(shí)別符,很有可能是源碼里有語(yǔ)法錯(cuò)誤
      value = new StringBuilder(MyC.MAXSTR);
      value.Append(io.getNextChar());
      token_id = T_UNKNOWN;
      io.ReadChar();
      }
    skipWhite();
    // 條件編譯,如果是myc.exe是調(diào)試版本,則在命令行里
    // 打印出當(dāng)前識(shí)別的字符類型,便于myc.exe的開(kāi)發(fā)者排錯(cuò)
#if DEBUG
    Console.WriteLine("[tok.scan tok=["+this+"]");
#endif
}

scan函數(shù)是Tok對(duì)象里最核心的函數(shù),它實(shí)際上是完成前面myc語(yǔ)法里這些詞法規(guī)則(還有隱含的關(guān)鍵字和操作符識(shí)別):

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

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

我們?cè)偻ㄟ^(guò)說(shuō)明LoadName函數(shù)來(lái)解釋詞法分析的細(xì)節(jié):

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

上面基本上就是詞法分析的關(guān)鍵代碼了,不過(guò)在說(shuō)明的時(shí)候,我特意跳過(guò)了構(gòu)造函數(shù)的 io.ReadChar()這個(gè)函數(shù),這個(gè)函數(shù)從字面意義上看是讀取一個(gè)字符,但實(shí)際上從源文件一個(gè)字符一個(gè)字符的讀取效率實(shí)在是太低了,因此一般都是從源文件里讀取一大段字符并緩存在內(nèi)存里,提高效率:

// Io.cs – ReadChar函數(shù)

public void ReadChar()
{
  // 判斷是不是讀到文件末尾了
  if (_eof)         // if already eof, nothing to do here
    return;
  // 如果緩存還沒(méi)有實(shí)例化,或者緩存里的字符
  // 已經(jīng)處理完畢了,創(chuàng)建一個(gè)新的緩存
  // 對(duì)于老的緩存數(shù)組,丟給垃圾回收機(jī)制處理
  if (ibuf == null || ibufidx >= MyC.MAXBUF)
    {
    ibuf = new char[MyC.MAXBUF];
    _eof = false;
    // 從源文件里讀取一大塊內(nèi)容到緩存里
    ibufread = rfile.Read(ibuf, 0, MyC.MAXBUF);
    ibufidx = 0;
    if (buf == null)
      buf = new StringBuilder(MyC.MAXSTR);
    }
  // 從緩存里讀取下一個(gè)字符
  look = ibuf[ibufidx++];
  // 判斷這次讀取時(shí),是否已經(jīng)到源文件末尾了
  if (ibufread < MyC.MAXBUF && ibufidx > ibufread)
    _eof = true;

  /*
   * track the read characters
   */
  // 保存當(dāng)前讀取的字符,以便在生成IL源文件的時(shí)候
  // 可以把C源碼跟生成的IL源碼對(duì)應(yīng)起來(lái)
  buf.Append(look);
  // 如果碰到換行,更新行號(hào),行號(hào)在報(bào)告語(yǔ)法錯(cuò)誤
  // 的時(shí)候會(huì)用到,告知具體語(yǔ)法出錯(cuò)的行號(hào)便于
  // 程序員找到錯(cuò)誤
  if (look == '\n')
    bufline++;
}

在Io.ReadChar函數(shù)里,會(huì)保存讀取的C源碼,當(dāng)要生成IL源文件的時(shí)候,這個(gè)信息用來(lái)保存C語(yǔ)句跟IL語(yǔ)句的對(duì)應(yīng)關(guān)系,如用下面的命令編譯myc里自帶的測(cè)試源碼文件:

myc-list-cmd.png

效果如下圖:

myc-list-result.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 本文涉及的javac編譯器來(lái)自openjdk. javac的目錄地址為: 解壓目錄/langtools/src/s...
    whthomas閱讀 1,400評(píng)論 3 3
  • 車無(wú)疑是夢(mèng)的翅膀,自由更是夢(mèng)的天堂! 我的車?yán)餂](méi)有美女,只有酒!我酒量不大,里邊的酒絕大部分是給別人準(zhǔn)備的。我喜歡...
    英馳商貿(mào)閱讀 424評(píng)論 0 1
  • 目錄 葉姍姍說(shuō)過(guò)讓我不要去看她,可是這幾天我越想越覺(jué)得愧疚,拋開(kāi)別的不說(shuō),全靠她心甘情愿把事情經(jīng)過(guò)說(shuō)出來(lái),并且把布...
    疏牧風(fēng)閱讀 433評(píng)論 0 1
  • 風(fēng)和日麗的一天,冬娜打扮了三個(gè)小時(shí)才出門,出門前照著鏡子看了很久,像被什么勾了魂兒似的。出門后她沒(méi)有按照計(jì)劃的方向...
    彼岸久伴閱讀 220評(píng)論 0 0
  • github 新賬號(hào): ziran655@126.com 密碼: ziran4082227 第二個(gè)郵箱: olif...
    olifer閱讀 179評(píng)論 0 0