词法分析(lexical analysis)是编译器的第一阶段,主要是将代码的字符序列转换为token的过程。简单地来说,就是对代码进行切块的一个过程,并将每一块添加上其所属的类别标签。比如说int asd=897;
,其词法分析的结果即为
-
int
:< TYPE , ‘int’ > -
asd
:< ID , ‘ast’ > -
=
:< OPERATOR , ‘=’ > -
897
:< DEC_CONST , 897 > -
;
:< SEMICN , ‘;’ >
token的一般形式为<type , literal>,其中type为该词所属类型,literal则为其表值。词法分析的理论部分主要是自动机的构造以及转换,并不是太难,这里不做赘述。本篇文章主要讲述如何借助Flex工具来生成SysY语言(C语言的一个子集,具体语言定义见这)的词法分析。
Flex的前身是Lex。Lex是1975年由Mike Lesk和当时还在AT&T做暑期实习的Eric Schmidt,共同完成的一款基 于Unix环境的词法分析程序生成工具。虽然Lex很出名并被广泛 使用,但它的低效和诸多问题也使其颇受诟病。后 来伯克利实验室的Vern Paxson使用C语言 重写Lex,并将这个新的程序命名为Flex(意为Fast Lexical Analyzer Generator)。无论在效率上还是在稳定性上,Flex都远远好于它的前辈Lex。我们在Linux下使用的是Flex在GNU License下的版本,称作GNU Flex。
Flex是一个帮助生成词法分析代码的工具,你只需告诉它目标语言中各个词法的正则表达式,Flex就会帮你生成一个lex.yy.c的C代码文件。那么首先你需要提交给Flex一个.l
文件来告诉它词法规则,该文件的格式如下:
%option xxx
%{
Declarations
%}
Definitions
%%
Rules
%%
User Subroutines
第一行的option是来开启Flex自带的一些辅助功能。比如说Flex提供行号记录变量yylineno
,我们可以直接通过该变量读取当前行号而且无需维护,但在默认状态下该变量不开放给用户,如需使用就要在开头加入%option yylineno
。
第二部分%{…%}中的Declarations呢,是会直接加入生成代码的靠前部分,一般是引用头文件或者是定义初始变量。
第三部分%}…%%中的Definitions则是定义段,这里一般会为比较长的正则表达式命名以方便下一模块的描述,类似于C语言中的宏定义。正则表达式具体的语法可以参考Flex 核心规范。
第四部分%%…%%的Rules则是该文件的核心部分——规则段。规则段就是定义需要被识别出来的词法类型(可以用第三部分中的定义来组合),以及识别到该类型时需要进行的操作。每一次Flex都会使用尽可能长的字符串进行匹配,即选择最长字符串可以匹配到的类型。但当同一个长度的字符串匹配了多个类型时,默认选择第一个匹配到的类型,因此Rules的排列也有讲究。
第五部分User Subroutines则是用户自己的代码,会直接复制到生成代码的最后。前面词法识别规则会直接被Flex翻译为一个yylex()
函数,该函数即为匹配词法类型的函数。那么如果用户需要做一些错误处理(比如识别到该语言中不存在的字符)以及部分类型识别到后需要单独处理时,就可以将处理代码写在这。
另外,Flex还提供了两个全局变量 yytext
和 yyleng
,分别表示刚刚匹配到类型的字符串与该字符串的长度。
❦ SysY.l
%option yylineno
%{
#include "translator.h"
%}
TYPE int|void
KEYWORD if|else|while|break|continue|return
OPERATOR "+"|"-"|"!"|"*"|"%"|"/"|"="
COMPARISON "=="|"!="|">"|"<"|">="|"<="
NONZERO [1-9]
DIGIT [0-9]
LETTER [A-Za-z]
OCTAL_DIGIT [0-7]
OCTAL_CONST 0{OCTAL_DIGIT}*
ILLEGAL_OCTAL_CONST 0[0-9a-wy-zA-WY-Z]({LETTER}|{DIGIT})*
HEX_PREFIX 0x|0X
HEX_DIGIT [0-9a-fA-F]
HEX_CONST {HEX_PREFIX}{HEX_DIGIT}+
ILLEGAL_HEX_CONST {HEX_PREFIX}({LETTER}|{DIGIT})*
NONDIGIT {LETTER}|"_"
ID {NONDIGIT}({DIGIT}|{NONDIGIT})*
DEC_CONST {NONZERO}{DIGIT}*
COMMENT1 "/*"[^*]*"*"+([^*/][^*]*"*"+)*"/"
COMMENT2 "//".*
%%
{TYPE} { printf("\033[1;32mTYPE\033[0m\t\t%s\n",yytext);return TYPE; }
{OCTAL_CONST} { printf("\033[1;32mOCTAL_CONST\033[0m\t");return OCTAL_CONST; }
{ILLEGAL_OCTAL_CONST} { return ILLEGAL_OCTAL_CONST; }
{HEX_CONST} { printf("\033[1;32mHEX_CONST\033[0m\t");return HEX_CONST; }
{ILLEGAL_HEX_CONST} { return ILLEGAL_HEX_CONST; }
{DEC_CONST} { printf("\033[1;32mDEC_CONST\033[0m\t%s\n",yytext);return DEC_CONST; }
{KEYWORD} { printf("\033[1;32mKEYWORD\033[0m\t\t%s\n",yytext);return KEYWORD; }
{ID} { printf("\033[1;32mID\033[0m\t\t%s\n",yytext);return ID; }
{OPERATOR} { printf("\033[1;32mOPERATOR\033[0m\t%s\n",yytext);return OPERATOR; }
{COMPARISON} { printf("\033[1;32mCOMPARISON\033[0m\t%s\n",yytext);return COMPARISON; }
"(" { printf("\033[1;32mLPARENT\033[0m\t\t%s\n",yytext);return LPARENT; }
")" { printf("\033[1;32mRPARENT\033[0m\t\t%s\n",yytext);return RPARENT; }
"[" { printf("\033[1;32mLBRACKET\033[0m\t%s\n",yytext);return LBRACKET; }
"]" { printf("\033[1;32mRBRACKET\033[0m\t%s\n",yytext);return RBRACKET; }
"{" { printf("\033[1;32mLBRACE\033[0m\t\t%s\n",yytext);return LBRACE; }
"}" { printf("\033[1;32mRBRACE\033[0m\t\t%s\n",yytext);return RBRACE; }
";" { printf("\033[1;32mSEMICN\033[0m\t\t%s\n",yytext);return SEMICN; }
"," { printf("\033[1;32mCOMMA\033[0m\t\t%s\n",yytext);return COMMA; }
"&&" { printf("\033[1;32mAND\033[0m\t\t%s\n",yytext);return AND; }
"||" { printf("\033[1;32mOR\033[0m\t\t%s\n",yytext);return OR; }
{COMMENT1}|{COMMENT2} { }
[ \t\n] { }
. { return UNEXPECTED; }
%%
int main()
{
int token_type;
while (token_type = yylex())
{
if (token_type == UNEXPECTED)
printf("\033[1;31mError type A at Line %d: Invalid character \"%s\"\033[0m\n", yylineno, yytext);
else if (token_type == OCTAL_CONST)
{
int sum = 0;
for (int i = 1; i < yyleng; ++i)
sum = sum * 8 + (yytext[i] - '0');
printf("%d\n", sum);
}
else if (token_type == HEX_CONST)
{
int sum = 0;
for (int i = 2; i < yyleng; ++i)
{
if (yytext[i] <= '9' && yytext[i] >= '0')
sum = sum * 16 + (yytext[i] - '0');
else
{
switch (yytext[i])
{
case 'a':
case 'A':
sum = sum * 16 + 10;
break;
case 'b':
case 'B':
sum = sum * 16 + 11;
break;
case 'c':
case 'C':
sum = sum * 16 + 12;
break;
case 'd':
case 'D':
sum = sum * 16 + 13;
break;
case 'e':
case 'E':
sum = sum * 16 + 14;
break;
case 'f':
case 'F':
sum = sum * 16 + 15;
break;
}
}
}
printf("%d\n", sum);
}
else if (token_type == ILLEGAL_OCTAL_CONST)
printf("\033[1;31mError type B at line %d: Illegal octal number \'%s\'\033[0m\n", yylineno, yytext);
else if (token_type == ILLEGAL_HEX_CONST)
printf("\033[1;31mError type B at line %d: Illegal hex number \'%s\'\033[0m\n", yylineno, yytext);
}
}
☕︎ 一些说明
- “translator.h”头文件内对需要识别的词法类型(TYPE,OCTAL_CONST,…)进行了enum枚举,本质上就是整数。这里需要注意enum枚举默认从0开始,而yylex()读取完文件也返回0会造成误解,因此需要将枚举的第一项类型赋值为1,该文件内容我放在评论区了。
- 第四部分中引用第三部分定义的名字时需要注意在引用名字外面加上{大括号},否则会变成识别一段字符串
printf
中的【\033[1;32m…\033[0m】是将中间文字上色🎨- 正则表达式中对于注释的识别是一个比较棘手的问题。
- 对于单行注释,我们知道开头是“//”,后面需要将该行剩余所有字符都匹配掉,注意到点可以匹配非换行符的任意字符(
.==[^\n]
),那么点的Kleene闭包.*
即可匹配该行剩余所有字符了 - 对于多行注释,我们知道开头是“/*”,中间部分只要不出现“*/”即可。那么我们可以考虑将中间部分(包括结尾“*/”中的“*”)分为多段以“**···*”()结尾的字符串,每一段的结尾前面都不能出现“*”。那么每一小段都可以先写为
[^*]*"*"+
(⚠️“*”仅表示字符,*表示前面部分的Kleene闭包)。但是每一段连起来的时候不能出现“*/”,因此每一小段的开头都不能是“/”,这时则可以写为[^/*][^*]*"*"+
。然而第一小段以“/”开头却是没关系的(/*/
不是一个完整的注释),因此第一小段单独写为[^*]*"*"+
。最终多行注释的正则表达式即为"/*"[^*]*"*"+([^/*][^*]*"*"+)*"/"
。
- 对于单行注释,我们知道开头是“//”,后面需要将该行剩余所有字符都匹配掉,注意到点可以匹配非换行符的任意字符(
- 第五部分主要实现了八进制与十六进制的转换,以及一些错误处理,包括识别非法八进制、十六进制字符和非法字符的报错
- 八进制与十六进制的非法字符识别是另外定义了两个类型ILLEGAL_OCTAL_CONST与ILLEGAL_HEX_CONST,分别置于第四部分中OCTAL_CONST和HEX_CONST的后面。这样一来如果更长的字符串匹配到了ILLEGAL类型就会返回ILLEGAL,而正确的格式只会匹配前者。
yylex()
函数每次匹配一段字符串,若匹配成功则执行该类型后面的操作,操作中如无return
语句则继续识别,如有return
语句则返回main
函数
Test.c
int main()
{
int abc = 0123, bb = 0x12d23p, c = 0987, di = 0x45Fc1;
if (abc >= bb) // hello
while (di == 1)
return 8;
/*This is a test
This is a test
*/
$
}
Cast Magic 🪄
flex SysY.l
clang lex.yy.c -ly -ll
cat test.c|./a.out
Output
∂ 一个小坑
第四部分中我一开始将{KEYWORD}
放在{ID}
的后面,执行Flex的时候一直报错
SysY.l:36: warning, rule cannot be matched
很迷,当时改来改去这个warning就一直跟着{KEYWORD}
跑,只有行号在变化。
这条warning的意思应该是无法匹配{KEYWORD}
这条规则。
仔细想想,{KEYWORD}
匹配的都是关键字,那么为什么关键字会成为关键字呢?因为在一开始的时候就被{KEYWORD}
匹配到了,从而用户不能用关键字来定义变量名。这时候一看{ID}
果然放在{KEYWORD}
前面,显然{KEYWORD}
是{ID}
的一个子集,因此{ID}
匹配完后{KEYWORD}
就是光杆司令一个了。