上一节中完成了对虚拟机的实现和初始化,还有指令集的定义,接下来就是对源文件的词法分析部分
词法分析
什么是词法分析
源文件的内容对于我们的解释器而言,本质上就是一系列ASCII字符串,是毫无意义的,我们需要通过语法分析将其解析为简单的指令,然后再执行。可是这样一来,这就使得语法分析的部分十分庞大。所以编译器一般会对整个过程进行前后端的分离。前端负责对源文件进行预处理,将其解析为一系列标识符流,如下:
1 2 3
| 2 + 3 * (4 - 5) => (Number, 2) Add (Number, 3) Multiply Left-Bracket (Number, 4) Subtract (Number, 5) Right-Bracket
|
然后后端就可以根据直接对标识符流进行处理,就避免了中间处理过程臃肿的情况。
词法分析和编译器
词法分析从某种意义上来讲,本质上也是一种编译器,它以源代码作为输入流,然后输出标识符流:
1 2 3
| +-------+ +--------+ -- source code --> | lexer | --> token stream --> | parser | --> assembly +-------+ +--------+
|
直接从源代码编译成汇编代码是很困难的,因为输入的字符串比较难处理。所以我们先编写一个较为简单的编译器(词法分析器)来将字符串转换成标记流,而标记流对于语法分析器而言就容易处理得多了。
词法分析器的实现
这里我们就不用现有的词法分析器了,我们手动的实现这一部分。当然我们需要注意,词法分析并不是一次性的将所有的源码转换成标记流。因为在代码中的大多数操作符,和字符关键字都是和代码有上下文关系的。
所以实际的做法是,提供一个next()函数,每次调用这个函数就返回下一个标识。
支持的标识
我们的编辑器支持以下标识符:
1 2 3 4 5
| enum { Num = 128, Fun, Sys, Glo, Loc, Id, Char, Else, Enum, If, Int, Return, Sizeof, While, Assign, Cond, Lor, Lan, Or, Xor, And, Eq, Ne, Lt, Gt, Le, Ge, Shl, Shr, Add, Sub, Mul, Div, Mod, Inc, Dec, Brak };
|
这里之所以让标识符从128开始,是为了避免和前面设置的指令枚举变量发生冲突。
上面的运算符的枚举变量是有顺序的,实际上他们是按优先级进行排列的,之后的语法分析过程中会用到。现在我们就要进行token的匹配了。例如将==匹配成Eq,将=匹配成Assign。在下面的实现中,我们会分析这些匹配是怎么实现的。
当然这里还需要注意一些字符,他们自己就构成了标记,如右方括号]和波浪号~等,我们不需要对他们做额外的处理,因为他们不属于多字符的共同标记,也不参与运算(没有优先级关系)
框架
我们的next()的主体框架如下:
1 2 3 4 5 6 7
| void next(){ char *last_pos; while(*src){ ++src; } }
|
这里我们使用whlie来跳过我们分析过程中的遇到的空白字符和未知的符号。下面我们先把几个比较特殊的标识符进行一下处理:
换行符号
换行符也和空格类似,只不过遇到换行符号,我们需要将当前的行号+1:
1
| if(token == '\n') {++line;}
|
宏定义
我们的解释器不支持宏定义所以直接跳过#后面部分的内容:
1 2 3
| else if(token == '#'){ while (*src == 0 || *src == ' ') {++src;} }
|
标识符和符号表
标识符可以理解为变量名。对于语法分析而言,我们并不关心一个变量叫什么,而是在意这个变量的标识符,我们需要从它唯一的标识符中获取这个变量的信息。
基于这个理由,我们让词法分析器将扫描到的标识符保存在一张表中,每当我们遇到标识符,就去表中查找,如果标识符存在就返回它的唯一标识符,不存在则创建有一个新的标识符存放在表中。
我们用结构体来存放标识符的信息:
1 2 3 4 5 6 7 8 9 10 11 12
| typedef struct _id{ int token; int hash; char *name; int class; int type; int value; int Bclass; int Btype; int Bvalue; } ID;
|
这里解释一下各个字段的具体含义:
- token:该标识符返回的标记,这里主要是存储变量的信息,所以标记类型注意要是
Id
- hash: 这个标识符的hash值,用于快速比较标识
- name:存放标识符的变量名
- class:
存放标识符的类型,例如全局、局部、数字等
- type:
标识符的类型,如果是个变量,可能是指针、int或者char
- value:
存放这个标识符的值,如果标识符是函数的,则存放函数地址
- BXXX:
C语言的标识符中可能是全局的也是局部的,当我们局部标识符和全局标识符冲突时,这个字段用于保存全局变量的信息。
上面这些信息用于我们在语法分析部分进行解析,更加方便快捷:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| int token_val; ID *Symbols; ID *current_id;
... else if(token>='a'&&token<='z' || token>='A'&&token<='Z' || (*src >= '0' && *src <= '9') || token=='_'){ last_pos = src - 1; while(*src>='a'&&*src<='z' || *src>='A'&&*src<='Z' || *src>='0'&&*src<='9' || *src=='_') hash = hash * 147 + *(src++); current_id = Symbols; while(current_id->token){ if(current_id->hash == hash && !memcmp(current_id->name, last_pos, src - last_pos)){ token = current_id->token; return; } current_id++; } token = current_id->token = Id; current_id->hash = hash; current_id->name = (int)last_pos; return; } ...
|
我们通过以上形式实现对标识符的识别。
数字
当遇到数字时我们也需要,进行对应的解析,这一部分的难点在于不同进制的识别。然后将数字转换为对应的数据并求值,我们把它的值保存在token_value中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| else if(token>='0'&&token<='9'){ token_val = token - '0'; if(token_val > 0){ while(*src>='0'&&*src<='9') token_val = token_val *10 + *(src++) - '0'; }else{ if(*src == 'x' || *src == 'X'){ token = *(++src); while((token>='0'&&token<='9') || (token>='a'&&token<='f') || (token>='A'&&token<='F')){ token_val = token_val *16 + (token&15) + (token>='A' ?9:0); token = *(++src); } }else{ while(*src>='0'&&*src<='7') token_val = token_val *8 + *(src++) - '0'; } } token = Num; return; }
|
字符串
在实际的分析过程中,当我们遇到字符串,我们需要将它的内容保存在data段中,然后返回它data段中的地址,之后我们对字符串的使用都指向这个地址。另一个特殊的地方在于,我们需要支持部分转义字符的实现,这里我们只对\n做一个支持,也支持\x表示字符x的语法。
在分析时,我们将单个字符'a'按Num的形式保存并返回,我们将"a string"按字符串的形式进行处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| else if(token=='"'||token=='\''){ last_pos = data; while(*src!=token && *src!=0){ token_val = *src++; if(token_val=='\\'){ token_val = *src++; if(token_val=='n') token_val = 10; else if(token_val=='t') token_val = 9; else if(token_val=='r') token_val = 13; else if(token_val=='\\') token_val = '\\'; } if(token == '"') *data++ = token_val; } src++; if(token == '"') { *data++ = 0; token_val = (int)last_pos; }else {token = Num;} return; }
|
这一部分可能比较难理解,首先就是判断这个是单字符还是多字符的字符串。然后分别做不同的处理即可。
注释
在我们的C中,只支持//类型的注释,不支持多行/**/的注释方法。处理方式和前面对#的方式差不多,需要注意的是,这里要前瞻的观察下一位是什么/可能是除法,也可能是//注释
1 2 3 4 5
| else if(token=='/'){ if(*src=='/') while(*src!=0 && *src!='\n') ++src; else {token = Div; return;} }
|
这里我们利用src比token快一个字符的特点,实现了对多个字符的观察,我们称这个概念为前瞻(lookahead)
其他
其他的大多数是一些简单的操作符,我们简单的附上代码即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| else if(token=='='){ if(*src=='='){++src; token = Eq;} else {token = Assign;} return; }else if(token=='+'){ if(*src=='+'){++src; token = Inc;} else {token = Add;} return; }else if(token=='-'){ if(*src=='-'){++src; token = Dec;} else {token = Sub;} return; }else if(token=='!'){ if(*src=='='){++src; token = Ne;} return; }else if(token=='<'){ if(*src=='='){++src; token = Le;} else if(*src=='<'){++src; token = Shl;} else {token = Lt;} return; }else if(token=='>'){ if(*src=='='){++src; token = Ge;} else if(*src=='>'){++src; token = Shr;} else {token = Gt;} return; }else if(token=='|'){ if(*src=='|'){++src; token = Lor;} else {token = Or;} return; }else if(token=='&'){ if(*src=='&'){++src; token = Lan;} else {token = And;} return; }else if(token=='^'){ token = Xor; return; }else if(token=='%'){ token = Mod; return; }else if(token=='*'){ token = Mul; return; }else if(token=='['){ token = Brak; return; }else if(token=='?'){ token = Cond; return; }else if(token=='~' || token==';' || token=='{' || token=='}' || token=='(' || token==')' || token==']' || token==',' || token==':') return;
|
基本的思想就是利用前瞻确定标记。
关键字和内置函数
完成了上述的词法分析之后,我们需要在正式开始词法分析之前,预处理我们的关键字和内置的函数。我们将它们加入符号表,并提前为他们赋予必要的信息。
我们在main函数中对其进行初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| { Symbols = malloc(poolsize); memset(Symbols, 0, poolsize); src = "char else enum if int return sizeof while " "open read close printf malloc memset memcmp exit void main"; i=Char; while(i<=While){ next(); current_id->token = i++; } i = OPEN; while(i<=EXIT){ next(); current_id->value = i++; current_id->class = Sys; current_id->type = INT; } next(); current_id->token = Char; next(); idmain = current_id; }
|
至此我们就完成了对源代码的词法分析,程序会通过词法分析将我们的源代码转换成标记流。