写这个编译器的目的,是为了完成编译原理课上老师布置的大作业,实际上该大作业并不是真的实现一个编译器,而我选择硬刚,是为了完成我的小愿望--手写内核,编译器和CPU。我花了整个上半学期,写完了WeiOS,为了让它支持更多的用户态程序,甚至是基本的程序开发,必须给它量身打造一个编译器。于是这个编译器被提上日程。
因为我要复习考研和专业课过多,我打消了手写词法分析和语法分析的念头,转而使用FLEX和YACC,等到有时间再完成手工的版本--我认为也不是很难,如果用递归下降的话。
全部代码都在该处 https://github.com/mynamevhinf/CMinusCompiler
词法分析
因为是精简的C语言,所以只支持基本的符号(Token),像”++”, “--”和位操作都不予考虑。另外,只支持int和void类型。于是便构成了以下符号集:
number [1-9][0-9]*
letter [a-zA-Z]
Identifier {letter}({letter}|{number})*
Newline \n
whitespace [ \t\r]+
保留字:
If else while int void return
运算和界限符号:
<= >= != == + - * / < > ; , ( ) { } [ ]
主体函数是getToken(),这个函数封装了FLEX原生的yylex函数。而yyparser也将直接调用该函数。它的主要工作是在开始第一次词的检索前,初始化相关变量。然后在每次被调用的时候,返回符号的类型给yyparser,并且把构成符号的字符串临时保存在tokenString字符数组中。所以这函数相当于什么事情都没有干。
另外注意的是注释的两个符号,我直接在词法分析处理注释了。行注释是”//”,利用FLEX自带的input()函数(如果有的话,没有就写一个)一直读到’\n’出现。然后就是段注释符”/*”和”*/”,相似的做法。
语法分析
以下是BNF格式的语法规则:
Program -> declaration_list
declaration_list -> declaration_list declaration | declaration
declaration -> var_declaration | func_declaration
type_specifier -> INT | VOID
var_declaration -> type_specifier VARIABLE ; | type_specifier VARIABLE [ NUM ] ;
func_declaration -> type_specifier VARIABLE ( params ) compound_stmt
Params -> params_list | VOID
params_list -> params_list , param | param
Param -> type_specifier VARIABLE | type_specifier VARIABLE [ ]
compound_stmt -> { local_declarations stmt_list }
local_declarations -> local_declarations var_declaration | /* empty */
stmt_list ->stmt_list stmt | stmt
Stmt -> expr_stmt | if_stmt | return_stmt | compound_stmt | iteration_stmt
expr_stmt -> expr ; | ;
if_stmt ->IF ( expr ) stmt | IF ( expr ) stmt ELSE stmt
iteration_stmt -> WHILE ( expr ) stmt
return_stmt ->RET ; | RET expr ;
Expr -> var = expr | simple_expr
Var -> VARIABLE | VARIABLE [ NUM ]
Call -> VARIABLE ( args )
Args -> arg_list | /* empty */
arg_list -> arg_list , expr | expr
simple_expr -> NUM | var | call | ( expr ) | - simple_expr
| simple_expr + simple_expr
| simple_expr - simple_expr
| simple_expr * simple_expr
| simple_expr / simple_expr
| simple_expr < simple_expr
| simple_expr > simple_expr
| simple_expr >=simple_expr
| simple_expr <= simple_expr
| simple_expr != simple_expr
| simple_expr == simple_expr
我用globl.h中的TreeNode结构来保存语法树中的每一个节点。而一些为空的转换,我打算还是用一个该结构来表示,但是类型标记为None(也许有点浪费内存).
我实现的C-还算是个比较完整的程序语言,所以很有必要生成AST(抽象语法树),那么语法树中共有几种类型的节点呢?按理说应每种语法规则对应一种类型,例如参数列表,声明语句,普通语句和表达式等都对应一个节点类型,详细可以参见NodeType枚举类型。Parser.c文件是处理与语法树相关的函数,目前来说当中几个函数还没写清楚,TreeNode需要大改一下我估计,过几天也许就明了了。--2018/05/29
2018/05/30
没想到只过了一天不到,就完成了语法分析部分,总体上来说还是很简单的。
有些语法规则导出两种相似的子规则,用专业术语来讲就是就是要做左因子消除,但好像yacc已经代替我们做了这个工作--我猜测它在底下优化了我们混乱的语法规则,包括左递归也解决了。据来说就是if_stmt导出的两种情况,我并不打算在StmtType枚举中添加新的枚举类型来处理,而是利用原有的结构,用nkids来辨别是哪种情况。而在处理var_declaration第二种导出的规则时,原有的结构不够用了,因为我要存储VARIABLE和NUM,很显然一个attr联合体不够用,所以我引入了第二个联合体分别来存储两个值。分别叫attrA和attrB。有些时候这样做也无法解决结构上的问题,才不得不用两个枚举类型来解决。现在我也不确定这样做是否多余,毕竟我也是第一次写编译器,但它确实解决了当下的问题。
我删除了封装yylex()的getToken()函数,并解决了注释代码的问题,现在可以支持/**/的段注释和//行注释了。另外,我不想让我代码变得冗余,所以我把构建符号表(Symbol table)的任务放在了语义分析,直接放在语法分析中虽然节约了时间,但实在是难以维护。
最后根目录下source*.c都成功地进行了语法分析,可能也只是表面上...
语义分析还没学,翻书去了...