前文 .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裏自帶的測試源碼文件:
效果以下圖: