承接上一篇博客,这篇博客主要是开始分析词法分析部分了。
来回顾一下代码逻辑模块是如何进入到词法分析部分的,在主函数中调用了输入的函数Read_Input_Files,它又通过循环调用read_file挨个读入文件,read_file经read_text把文件转成token后进行存储,read_text经过text模块中转,调用到stream模块进行词法分析器的初始化和打开。
为了搞清楚token模块具体是实现了什么功能,我把Dick爷爷提供的一篇“非正式”paper又读了一遍,其中提到关于词法的工作是这样的:
把文本压缩成一个8-bits的“基本单词”,更加具体一点的方式如图中我的注释所说。但是实际上我在token中发现的映射方式是用4个16进制数,也就是总共16-bits来表示一个Token,所以token中是用16-bits来表示的,README中,对于如何扩展其他语言检测方法的叙述也证实了这一点:
To add another language L, write a file Llang.l along the lines of clang.l or the other *lang.l files, extend the Makefile and recompile.
All knowledge about a given language L is located in Llang.l; the rest of the program expects each token to be a 16-bit character.
共有16-bits,所以可以表示token的数值范围是0x0000 ~ 0xFFFF。我试图具体分析映射的方法:
#include
/* Macros for the composition of tokens */ /* range (gaps unused)*/
#define No_Token int2Token(0) /* 0x0000 */
/* 8-bit bytes */ /* 0x0001-0x00FF */
#define CTRL(ch) int2Token(0x100|((ch)&0x01F)) /* 0x0101-0x011E */
#define NORM(ch) int2Token(0x100|((ch)&0x07F)) /* 0x0121-0x017E */
#define IDF int2Token(0x17F) /* 0x017F */
#define STR int2Token(0x180) /* 0x0180 */
#define MTCT(ch) int2Token(0x180|((ch)&0x01F)) /* 0x0181-0x019E */
#define META(ch) int2Token(0x180|((ch)&0x07F)) /* 0x01A1-0x01FE */
/* tokens from idf_hashed() */ /* 0x0200-0xFFFE */
#define End_Of_Line int2Token(0xFFFF) /* 0xFFFF */
注释里有这样的解释:
2. macros for defining summary tokens (with ranges of their parameters):
CTRL(ch) ch in 'A'-'~'
NORM(ch) ch in '!'-'~'
MTCT(ch) ch in 'A'-'~'
META(ch) ch in '!'-'~'
稍微分析一下它的来头,只看算式中(ch)&***的部分,会发现CTRL(ch)和MTCT(ch)相同,运算是&0x01F,也就是只选取ch二进制位的后5位。从注释中可以推断,ch的后五位范围在0x01到0x1E。这是什么意思呢?翻出ASCII码表,关注英文字母,可以看到:
关注高位为0100(二进制)到0111这四列,可以看到A和a的最后5个二进制位是相同的,不同字母之间的最后五位又可以区分出来,但是对大小写不敏感。
类似地可以推断出,NORM(ch) META(ch)是在对所有ASCII打印字符进行映射。这个当然是大小写敏感了。
那么除了这四个定义的方法,还有两个区间,分别是8-bit bytes和idf_hashed()。暂时没有找到8-bits bytes是如何产生的,但是对于idf_hashed,看过了它的函数体之后,它的功能就是传入一个字符串,传出一个范围在0X200到0XFFE之间的16bit数,过程中的哈希函数看起来没有规律:
#define HASH(h,ch) (((h) * 8209) + (ch)*613)
像是随便哈希了一下。
看到这个.l文件中,开头大几十行定义了两个idf数组,这里即使不看idf的数据结构,只看内容,也知道这是关于字符串和映射之间关系的数组。比如{“define”, META(‘d’)},就是把define映射成了一个数字。
在它的决策部分可以看到,引号中间的字符串和字符全部视作一样的:
\"{StrChar}*\" { /* strings */
return_ch(STR);
}
\'{ChrChar}+\' { /* characters */
return_ch('\'');
}
由#开头的行,如果是include则忽略,否则匹配出的token丢进ppcmd里面查找是哪个预编译指令,找不到就按#映射了。
对于在左括号之前的idf,也就是函数名,有一个设置是F,如果设定了这个F,就把这个函数名进行hash,否则就只当做token统一的IDF进行映射。关于这一点,可以看到manual中对于F的解释:
-F The names of routines in calls are required to match exactly (not in sim text)
BTW,原来routine可以用来表示“函数”的意思,看源码之前先看manual的时候一脸懵逼。
普通的idf就不进行hash了,全部当做一样的IDF来看待。
分号,如果设定了f就增加一个分号的ch,否则不去管。f的作用是保证run有balancing parentheses,来分离潜在的函数体,要探究怎么实现
的话需要看后面匹配的部分。
换行符需要记录。
ASCII95为[\040-\176],匹配的是95个ASCII能显示的字符,如果上面的这些匹配都没有匹配到的话,就由ASCII95来收场,结果直接保留在token中。
经历以上过程还没有匹配到的,属于非ASCII字符,单独计数,不计token。