詞法解析
詞法解析的工作都由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è)試源碼文件:
效果如下圖: