词法分析的代码就是个大型的if / else语句,不同的起始字符表示不同类型的词汇,根据起始字符选择对应的if / else分支就可以了。
在每个不同的if / else分支里,就是这类词汇的处理函数。
例如0x开始的是16进制,0开始的是8进制,0b开始是2进制,1-9开始的是10进制,那么就可以这么写:
FILE* fp = fopen("source.c", "r"); // 打开源码文件
char c0 = fgetc(fp); // 读取第1个字符,也就是起始符
if ('0' == c0) { // 数字分支,16进制、8进制、2进制,或0.开头的浮点数
char c1 = fgetc(fp); // 读取第2个字符
if ('x' == c1)
return number_base16();
else if ('b' == c1)
return number_base2();
else if ('.' == c1) // 浮点数,一律按double类型处理
return number_double();
return number_base8();
} else if ('1' <= c0 && '9' >= c0) // 10进制,可能是整数,也可能是浮点数
return number_base10(); // 如果发现小数点的话,在这个函数里处理
else if ('.' == c0) // 小数点开头的浮点数
return number_double();
如果要是支持以小数点开头的浮点数(前导0省略),加一行else if就行,如上。
词法分析的if / else分支,都是根据起始字符来划分的,处理函数的结束都是根据终止字符来的。
当进行16进制的分析时,后续的字符必须都是0-9或a-f或A-F(也可以是UL之类的后缀),除了这些之外的其他字符都是终止字符。
当发现终止符的时候,因为终止符并不包含在当前数字内,所以要把它放回输入缓冲区。
这种情况在词法分析时是很常见的,不光是分析数字,也可以是分析运算符、标志符。
所以把输入缓冲区做成一个双向链表是有必要的(也可以用其他数据结构)。
总之,在从源码文件里读字符到缓冲区时,输入缓冲区是个先进先出(FIFO)的队列。
在词法分析器从缓冲区读取字符,或者把字符放回到缓冲区时,输入缓冲区是个栈。
放回字符,一定是把字符压回到栈顶(push)。
读取字符,一定是把字符从栈顶pop出栈(pop)。
我在scf里使用的双向链表,双向链表处理取出和放回时比较无脑[捂脸]
16进制整数的词法分析
上图就是scf_lex.c里的一个片段,16进制整数的分析,但我都没支持后缀的ULL。
标志符的词法分析是以下划线_或字母a-z和A-Z开头的,之后可以包含数字。
char c0 = fgetc(fp);
if ('_' == c0 || ('a' <= c0 && 'z' >= c0) || ('A' <= c0 && 'Z' >= c0))
return lex_identity(); // 标志符的分析
关键字是包含在标志符里的,它们都是一样的词法规则的字符序列,无非就是关键字被编译器保留做特殊用途,不能当作普通的标志符。
在词法阶段,是没法确定标志符到底是类型、变量、常量、还是函数的,只能确定它是不是关键字。
scf的标志符的词法分析
NULL,__func__, __LINE__ 都在这里处理好了,如上图。
从数学上来说,这类字符串处理程序都是一个有限的自动机。
它根据读入的字符变化来更新自己的状态,把字符读一遍就完成了词法分析。
词法分析的状态机
运算符的分析也是类似的,+和+=是一个分支,*和*=是一个分支,etc。
*=肯定是乘等,但*并不能确定是乘法还是指针,所以只能叫它星号(star)。
符号/的分析稍微复杂点,因为//和/**/的注释也是以/开头的,如下图。
符号/的状态机
词法分析并不难写,只需要一点耐心就可以写出来。
它是编译器里最简单的模块,有兴趣的可以参考我的scf_lex.c文件。
| 留言与评论(共有 0 条评论) “” |