☰
Flex+Bison课设实战:Cminus词法分析与语法分析完整实现
2026/10/3 2:54:58 网站建设 项目流程

简介:这是一份编译原理课程设计完整方案,使用Flex与Bison工具对Cminus语言进行词法分析和语法分析,附带全部源代码、实验报告及说明文档;项目适合计算机科学与技术、人工智能、软件工程等专业的在校学生完成大作业、课程设计或毕设初期演示,对刚接触编译器构造的初学者也很友好。压缩包为zip格式,共14个文件,整体约328KB,主要包含6个C源文件、2个头文件、以及词法规则文件、语法规则文件、实验报告和README文档,目录结构清晰,易于按模块查阅。目前已有106人学习浏览,所有代码均在测试通过、可稳定运行后才打包上传,作者答辩评审平均分达到96分,质量有保障。下载后既能参照源码与实验报告理解词法分析、语法分析和语法树的完整实现,也便于在此基础上二次修改、扩展新功能;若运行环境不熟,还可私聊作者获取远程教学支持。

1. 这份Flex+Bison课设包:Cminus词法分析与语法分析,拿到手就能跑

当编译原理课设落到“用Flex和Bison给Cminus写词法分析和语法分析”这个题目上,多数人第一道坎其实不是文法本身,而是工具链:flex生成的文件怎么跟bison生成的语法分析器拼起来,主函数该放哪,语法树节点怎么定义才能被语义动作引用。这份资源把整条链路打包好了:lexical.l、syntax.y、main.c、tree.h四个核心源文件都在,生成的lex.yy.c、syntax.tab.c也一并附上,还有input1.c、input2.c、input3.c三个Cminus测试用例和实验报告。你拿到手可以先跑通一次再回头看原理,适合课设要交差的学生、想快速上手Flex+Bison入门组合的初学者,也适合拿来做二次修改的底子。核心价值是别在工具链上卡死,把精力留给文法设计。

2. 词法分析:从lexical.l的token设计到lex.yy.c生成

2.1 Cminus的token集:先定词法边界再写正则

Cminus是一套照着C语言砍出来的迷你文法,来自主流编译教材,用来做课程设计恰好能把工作量控制在一个学期能完成的范围。它保留了int、void、if、else、while、return这几个关键字,支持全局变量声明、函数定义、数组下标、赋值和算术表达式,去掉了C里头结构体、指针、switch这类复杂语法。这意味着词法层面需要处理的token种类很有限,一张表能列完。

token类别具体tokenFlex里的写法
关键字if、else、int、return、void、while直接写字符串"if"并return对应token宏
标识符由字母组成的ID[a-zA-Z]+,需要把yytext复制进语义值
数字非负整数NUM[0-9]+,用atoi转成int
运算符+ - * / < <= > >= == != =每个运算符一条规则
界符; , ( ) [ ] { }每个符号一条规则
注释//开头的行注释"//"[^\n]*,匹配后不return

这个表同时也是写lexical.l的清单。词法规则的书写顺序在Flex里是有讲究的:当多个规则都能匹配同一段输入时,Flex选择“匹配长度最长”的那条,如果长度相同才选择最早出现的那条。所以关键字必须写在标识符规则之前,否则if会被[a-zA-Z]+吃掉;而>=这类双字符运算符也天然比>优先,因为更长。

lexical.l开头是典型的Flex文件结构:

%{ #include <stdio.h> #include "syntax.tab.h" #include "tree.h" void yyerror(const char* msg); %} %option noyywrap %% "if" { return IF; } "else" { return ELSE; } "int" { return INT; } "return" { return RETURN; } "void" { return VOID; } "while" { return WHILE; } "+" { return PLUS; } "-" { return MINUS; } "*" { return TIMES; } "/" { return OVER; } "<" { return LT; } "<=" { return LTE; } ">" { return GT; } ">=" { return GTE; } "==" { return EQ; } "!=" { return NEQ; } "=" { return ASSIN; } ";" { return SEMI; } "," { return COMMA; } "(" { return LPAREN; } ")" { return RPAREN; } "[" { return LBRACKET; } "]" { return RBRACKET; } "{" { return LBRACE; } "}" { return RBRACE; } [0-9]+ { yylval.type_int = atoi(yytext); return NUM; } [a-zA-Z]+ { yylval.id_name = strdup(yytext); return ID; } [ \t\n\r]+ { /* 空白不产生token */ } "//"[^\n]* { /* 行注释直接忽略 */ } . { yyerror("词法错误:非法字符"); } %%

代码说明:%{ %}里的内容会被原样复制到生成的lex.yy.c顶部,这里必须包含syntax.tab.h,因为Bison生成的头文件里定义了IF、ELSE等所有token的宏常量;%option noyywrap表示不需要额外的yywrap函数,省去链接libfl的麻烦。规则区每条规则由正则表达式和花括号里的动作组成,动作里return XXX会把token编号交给语法分析器;对于NUM和ID,还要把具体值塞进全局变量yylval,Bison语法栈拿到的就是这份值。

有个细节容易被忽略:标识符的语义值如果用strdup(yytext),会分配一块新内存,这样词法缓冲区的复用不会影响它;如果图省事直接把yytext指针存进yylval,后面读到的标识符会全是同一个内容。这个坑在后面的避坑章还会再展开。

2.2 Flex三段式结构:定义区、规则区、用户代码区

一个完整的Flex文件用%%分成三个区域。第一段是定义区和%{ %}里的C头文件、外部声明;第二段是规则区,也就是上一节展示的一大串“正则 + 动作”;第三段是用户代码区,通常放着辅助函数,比如yyerror的简单实现,或者一个给课设演示用的打印入口。

%% // 用户代码区例子 void yyerror(const char* msg) { fprintf(stderr, "Error: %s\n", msg); }

Flex生成lex.yy.c之后,yylex()函数会按顺序尝试规则:先看有没有更长的可行匹配,有就选最长;没有就在同长度规则里选最靠前的。这就是上一节说关键字必须排在ID前面的根本原因。理解了这套优先级,你就能解释为什么++如果没定义单独规则,会被当成两个+,而>=永远不会误拆成>和=。

Cminus没有自增运算符,所以这个场景不会踩到,但如果是照着C扩展自己的文法,记得给++、--、&&、||都单独列规则。词法分析器的工作在这里就结束了,它只负责把字符流切成token流,不关心token之间是否符合语法。

2.3 从lexical.l到lex.yy.c:生成命令与独立调试

词法文件改好后,生成C代码只需要一条命令:

flex lexical.l

默认会产出lex.yy.c。如果你用的不是这份资源里的文件,而是自己新建的lexer,建议顺手加个-d开关变成flex -d lexical.l,这会打开Flex的调试模式,运行时把每一步匹配的token都打印到终端,排词法问题非常直观。

这时候可以先把bison那一半放一边,只验证词法分析器本身。最省事的办法是写个临时main函数,让yylex循环输出token编号和文本:

extern FILE* yyin; extern int yylex(); int main(int argc, char** argv) { if (argc > 1) { yyin = fopen(argv[1], "r"); if (!yyin) { perror("open"); return 1; } } int t; while ((t = yylex()) != 0) { printf("token=%d text=%s\n", t, yytext); } return 0; }

逻辑说明:yylex()每次调用返回一个token,返回0表示文件结束。main里把yyin指向输入文件,然后循环读出token流。如果你看到if被识别成ID而不是IF,就可以立刻去检查lexical.l里的规则顺序;如果看到>=被拆成了GT和ASSIN,那就检查是不是把>=这条规则写漏了。

这段临时代码编译时,只需要lex.yy.c和这个临时main一起编,不需要涉足Bison生成的任何文件:

gcc -o lexer_only lex.yy.c temp_main.c

注意temp_main.c里不要include tree.h,因为这一阶段还没有构建语法树的需求,只需要一个干净的词法输出。这样拆开调试的好处是定位问题快:词法阶段出问题不会混杂语法阶段的干扰。

3. 语法分析:syntax.y中的产生式、优先级与建树动作

3.1 syntax.y三件套:%union、%token与%type

Bison文件同样分三段,但第一段比Flex复杂得多。词法分析器往yylval里塞值,语法分析器从$$、$1、$3这些位置取值,两边对接靠的是%union里声明的字段。这份资源里的syntax.y典型写法是这样:

%{ #include <stdio.h> #include <stdlib.h> #include <string.h> #include "tree.h" void yyerror(const char* msg); extern int yylex(); extern FILE* yyin; %} %union { int type_int; char id_name[32]; struct TreeNode* type_tnode; } %token <type_int> NUM %token <id_name> ID %token IF ELSE INT RETURN VOID WHILE %token PLUS MINUS TIMES OVER LT LTE GT GTE EQ NEQ ASSIN %token SEMI COMMA LPAREN RPAREN LBRACKET RBRACKET LBRACE RBRACE %type <type_tnode> program declaration-list declaration var-declaration %type <type_tnode> fun-declaration params param-list param compound-stmt %type <type_tnode> local-declarations statement-list statement expression-stmt %type <type_tnode> if-stmt while-stmt return-stmt expression var simple-expression %type <type_tnode> additive-expression term factor call args arg-list relop addop mulop

逻辑说明:%union定义了语义值的联合体,type_int用来存NUM的整数值,id_name存标识符名字,type_tnode存语法树节点指针。%token <type_int> NUM把token和联合体字段绑定,这样Flex里yylval.type_int = atoi(yytext)就和语法栈能对上;%type <type_tnode> ...则声明哪些非终结符的语义值是一棵子树指针。

最容易出问题的地方是id_name这个字段的类型。如果写成char*就要在Flex动作里strdup,写成定长数组char[32]则要在动作里用snprintf拷进去。资源里如果用的是定长数组,那个strcpy的越界风险是存在的,比如标识符长度超过31就会覆盖相邻内存。课设阶段没人会写32个字母以上的变量名,但这个边界意识建议保留:后续要加语法检查识别长标识符,字段就要扩容或改指针。

3.2 Cminus产生式集合与算符优先级处理

核心文法沿用了Cminus的标准定义,产生式集合如下:

program → declaration-list declaration-list → declaration-list declaration | declaration declaration → var-declaration | fun-declaration var-declaration → INT ID ; | INT ID [ NUM ] ; fun-declaration → INT ID ( params ) compound-stmt | VOID ID ( params ) compound-stmt params → param-list | VOID param-list → param-list , param | param param → INT ID | INT ID [ ] compound-stmt → { local-declarations statement-list } local-declarations → local-declarations var-declaration | ε statement-list → statement-list statement | ε statement → expression-stmt | if-stmt | while-stmt | return-stmt | compound-stmt if-stmt → IF ( expression ) statement | IF ( expression ) statement ELSE statement while-stmt → WHILE ( expression ) statement return-stmt → RETURN ; | RETURN expression ; expression-stmt → expression ; | ; expression → var ASSIN expression | simple-expression var → ID | ID [ expression ] simple-expression → additive-expression relop additive-expression | additive-expression relop → LTE | LT | GT | GTE | EQ | NEQ additive-expression → additive-expression addop term | term addop → PLUS | MINUS term → term mulop factor | factor mulop → TIMES | OVER factor → LPAREN expression RPAREN | var | call | NUM call → ID ( args ) args → arg-list | ε arg-list → arg-list , expression | expression

这里面有两个经典歧义点。第一是if-stmt的悬垂else,也就是if (a) if (b) s1; else s2;里的else到底该绑定内层if还是外层if;Bison默认是移进优先,所以else自动归最近的未配对的if,这正好符合C语言语义,不需要特殊处理。第二是expression → var ASSIN expression,赋值运算符天然右结合,而且优先级最低;如果只写这一条产生式,Bison会报告shift/reduce冲突。

最常见的处理方式是在syntax.y的产生式之前声明优先级:

%left ASSIN %left LT LTE GT GTE EQ NEQ %left PLUS MINUS %left TIMES OVER

优先级声明从下往上递增,越靠后优先级越高,%left表示左结合,%right表示右结合。这里ASSIN写在最下面一行,所以它优先级最低;ASSIN如果用%right声明,a = b = c就会归约成a = (b = c),符合赋值右结合的要求。把这几条声明加进去,再配合上面那套分层产生式,Bison就能算出无冲突的分析表。当然也可以把表达式合并成一条产生式,用优先级声明自动处理所有运算符,但Cminus课设通常保留分层写法,因为语法树节点能直观体现运算层次。

配合文件里bison -d syntax.y生成syntax.tab.c和syntax.tab.h时,加一个-v参数可以额外产出syntax.output,这个文件里能看到每一条状态、每一个活前缀和冲突点。如果编译时有冲突警告,打开syntax.output搜索“conflicts”就能定位到是哪个产生式和哪个移进动作打架。

3.3 归约动作:语义动作与语法树构建

Bison每归约一个产生式,就执行一次花括号里的动作,语法树就靠这些动作一点一点搭起来。tree.h里定义好节点结构后,syntax.y里会先放一个创建节点的辅助函数:

TreeNode* newTreeNode(NodeKind kind, char* name, int lineno) { TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode)); node->kind = kind; node->lineno = lineno; strcpy(node->name, name); for (int i = 0; i < MAX_CHILDREN; i++) { node->child[i] = NULL; } node->sibling = NULL; return node; }

参数说明:kind表示节点类型,是声明节点、表达式节点还是语句节点;name用来标识具体的运算符或关键字;lineno记录代码行号,调试时能精确指出问题位置。MAX_CHILDREN在tree.h里定义,通常取3,能容纳三目表达式这类最多3个孩子的情况,Cminus里最复杂的三叉节点是if语句。

有了造节点的工具,产生式动作就可以写成这样:

expression : var ASSIN expression { TreeNode* node = newTreeNode(EXP_STMT, "assign", @$); node->child[0] = $1; node->child[1] = $3; $$ = node; } ; additive-expression : additive-expression PLUS term { TreeNode* node = newTreeNode(EXP_STMT, "add", @$); node->child[0] = $1; node->child[1] = $3; $$ = node; } ; term : term TIMES factor { TreeNode* node = newTreeNode(EXP_STMT, "mul", @$); node->child[0] = $1; node->child[1] = $3; $$ = node; } ;

逻辑说明:$1、$3是右部符号的语义值,在这里是子树指针;$$是左部非终结符的返回值。第一次出现$$ = node时,就把新建节点挂到了上层;后面上层产生式再引用这个非终结符,拿到的是整棵子树。运算符节点两个孩子的顺序左子树在前、右子树在后,打印语法树时天然就是清晰的运算顺序。

语句节点的构建也类似,区别在于孩子数目和语义不同。比如while循环节点有两个孩子,第一个是条件表达式,第二个是循环体语句;if语句最多三个孩子,条件、then分支、else分支,else分支可以为NULL。

while-stmt : WHILE LPAREN expression RPAREN statement { TreeNode* node = newTreeNode(STMT, "while", @$); node->child[0] = $3; node->child[1] = $5; $$ = node; } ;

这里$3是括号里expression的子树,$5是statement的子树,注意产生式右部共有5个符号,下标按顺序数。写动作时漏掉某个$引用位置不对,Bison编译不会报错,但运行时语法树就会少一块或多一块,这是最隐蔽的一类错误。我一般会在建完节点后立刻在动作里加一句printf("create node: %s\n", node->name),开着这段日志把三个input跑一遍,树的结构对不对一目了然。

4. main.c与tree.h:把词法、语法、树输出串成完整闭环

4.1 main.c的职责:读文件、调yyparse、触发树遍历

词法和语法都就位后,main.c决定了程序从哪里开始、怎么结束。这份资源里的main.c结构很标准,职责有三块:解析命令行参数找输入文件、把yyin指向目标文件、调用yyparse()并处理返回结果。

#include <stdio.h> #include <stdlib.h> #include "tree.h" extern FILE* yyin; extern int yyparse(); extern TreeNode* root; int main(int argc, char** argv) { if (argc != 2) { fprintf(stderr, "用法: %s <输入文件>\n", argv[0]); return 1; } yyin = fopen(argv[1], "r"); if (yyin == NULL) { perror("打开输入文件失败"); return 1; } if (yyparse() == 0) { printf("语法分析通过,生成语法树:\n"); printTree(root, 0); } else { fprintf(stderr, "语法分析失败\n"); } fclose(yyin); return 0; }

逻辑说明:argc != 2强制要求命令行给出一个输入文件,这是课设答辩时最常见的调用方式./compiler input1.c;yyin是Flex里的全局输入流指针,直接指向FILE*就能让词法分析器从指定文件读,这是不修改lexical.l就实现任意文件输入的关键。yyparse()返回0表示语法分析成功,main.c再调用printTree把语法树按缩进打出来。

printTree定义在tree.h或者单独的tree.c里,递归遍历节点时遵循“先孩子后兄弟”的顺序。

4.2 tree.h里的TreeNode:孩子数组加兄弟链

语法树节点结构是整份资源的底座,syntax.y所有语义动作都在往这个结构里填东西。典型定义如下:

#define MAX_CHILDREN 3 typedef enum { DECL, EXP_STMT, STMT, OPERATOR } NodeKind; typedef struct TreeNode { NodeKind kind; // 节点类型 int lineno; // 对应源代码行号 char name[32]; // 节点名,如 add、while、assign struct TreeNode* child[MAX_CHILDREN]; // 孩子指针数组 struct TreeNode* sibling; // 兄弟链 } TreeNode;

设计思路是“每个节点最多三个孩子,平级节点用sibling串成链表”。Cminus语法的确有少数产生式右部超过3个符号,比如param-list → param-list , param每次只新增一个子节点,所以用链式追加而不是直接塞进数组;而if-stmt的三叉结构用child[0]、child[1]、child[2]刚好装下条件、then分支、else分支。这种孩子兄弟表示法在分析树里很常见,打印时先递归打印所有孩子,再沿sibling走。

因为节点是新建的,语义动作里就需要触达root。syntax.y的起始产生式是program,它的归约动作会把整棵树的第一层节点交给全局变量root,main.c里的extern TreeNode* root才能拿到。

4.3 一条命令编到底:Makefile与手动构建步骤

由于资源里已经附带lex.yy.c和syntax.tab.c两个生成文件,你其实可以直接跳过flex和bison,只做gcc编译。完整的手动构建过程是这样:

# 如果lexical.l或syntax.y有改动,先重新生成 flex lexical.l bison -d syntax.y # 然后编译所有源文件 gcc -o compiler lex.yy.c syntax.tab.c main.c tree.c -lm

参数说明:flex不加额外参数生成lex.yy.c;bison必须带-d,否则不会产出syntax.tab.h,lexical.l里include的头文件就找不到。最后的-lm不是Cminus需要的,只有用到数学库函数才加,课设的四个C文件没有引用任何libm函数,可以去掉。整个命令生成一个名为compiler的可执行文件。

如果不想每次敲这一长串,在资源根目录放一个Makefile是更稳的做法:

CC = gcc CFLAGS = -g -Wall all: compiler compiler: lex.yy.c syntax.tab.c main.c tree.c $(CC) $(CFLAGS) -o compiler lex.yy.c syntax.tab.c main.c tree.c lex.yy.c: lexical.l syntax.tab.h flex lexical.l syntax.tab.c syntax.tab.h: syntax.y bison -d syntax.y clean: rm -f compiler lex.yy.c syntax.tab.c syntax.tab.h .PHONY: all clean

Makefile把文件依赖关系写清楚了:syntax.tab.h是lexical.l和syntax.y共同的上游,syntax.y改动要连带触发bison和flex两次生成;lex.yy.c本身又被主程序依赖,所以只要任一源文件有改动,make会自动重跑生成链。-g装调试信息,-Wall把潜在警告全亮出来,课设阶段宁可让警告多刷屏,也不要静默编译。

这套依赖关系里最容易搞乱的是bison命令同时产出syntax.tab.c和syntax.tab.h两个文件,Makefile里把两个文件写在一个规则的冒号右边是没有问题的,但别拆成两条规则,否则会让make误以为它们可以单独更新。

5. 避坑与排查:Flex/Bison课设里那些翻车现场

5.1 链接阶段报undefined reference toyywrap

现象:gcc编译全部通过,最后链接时报错“undefined reference toyywrap”。

原因:lexical.l里没有声明%option noyywrap。Flex生成的代码默认要调用yywrap()判断是否还有下一个输入文件,这个函数如果不自己定义,就需要链接libfl库,否则链接器找不到符号。

解决:最省事的做法是在lexical.l定义区加上%option noyywrap,然后重新跑flex生成lex.yy.c;如果不想改文件,链接命令末尾追加-lfl也能过。我自己的习惯是前者,因为吃掉了对libfl的依赖,换机器部署时少一个坑。

5.2 Bison冲突警告:conflicts: 4 shift/reduce

现象:跑bison时出现“conflicts: 4 shift/reduce”,语法分析器仍然能编译,但某些输入的处理结果可能不符合预期。

原因:悬垂else和赋值优先级没有通过优先级声明消除。Cminus产生式里有if-stmt → IF ( expression ) statement ELSE statement这种二义性结构,不做声明时Bison会生成一个默认移进或归约,选错就语法行为不一致。

解决:把%left ASSIN和四行关系运算、加减、乘除优先级声明补上,再跑一次bison确认警告清零。排查冲突位置用bison -v syntax.y生成syntax.output,打开文件搜索conflicts,能看到具体状态和冲突动作。加了声明后大多数课设文法能把冲突压到零,如果还有残余,就要回去看产生式哪里写重了。

5.3 关键字被识别成标识符

现象:输入文件里有if、else、while,词法分析输出却把它们打成了ID,语法分析随即直接报错。

原因:lexical.l里常量字符串规则写在[a-zA-Z]+之后。Flex匹配时先比较匹配长度,这条规则“if”匹配长度2,区间规则“if”匹配长度也是2,同一长度时选先出现的规则;如果ID规则在前,那if就永远轮不到关键字规则。

解决:把关键字、双字符运算符这类优先级高的规则一律放在宽泛的正则前面。检查标准也很简单,单独编译词法驱动的临时main跑一下输入文件,看看输出token流里if是不是IF。以后新增关键字也要按这个顺序插。

5.4 语法树打印出来只有一个根节点

现象:./compiler input1.c返回0,但打印的树只有program或declaration一个节点,下面什么都没有。

原因:语义动作里只把节点赋值给了$$,但上层产生式引用它时用的是$n,两者对不上;或者建树时只设置了child指针,sibling链没接,打印函数只递归孩子不递归兄弟,导致树中间断掉。

解决:把syntax.y里每个归约动作跟产生式右部逐个符号对一遍,确认$1、$3的位置没有错位;如果用的是孩子兄弟表示法,打印函数要同时递归child数组和sibling指针。最稳的办法是在建树动作里临时加printf输出节点名,三个input跑一遍,对照源码行号看树结构缺在哪。

5.5 节点里存了yytext指针,运行期全成了同一串字符

现象:语法树里所有标识符节点的name打印出来都是最后一个变量名,或者是一段乱码。

原因:写lexical.l时图省事,动作直接yylval.id_ptr = yytext而没有复制。yytext是Flex内部缓冲区,每次匹配都会覆盖,语法分析器归约动作执行时,yytext早就指向了最新匹配的内容。

解决:在Flex动作里用strdup分配新内存,比如yylval.id_name = strdup(yytext);如果syntax.y里id_name声明为定长数组,则用strncpy(..., 31)。改了之后再跑这三个输入,标识符节点的名字应该和源码一一对应。如果遇到strdup分配的内存没有释放,那是课题之外的另一课,先把功能跑通再补valgrind。

6. 把这份课设用活:加调试开关、跑完三个input再扩展

拿到资源先别急着改文法,我建议先做三件事:一是用调试开关确认整个链路真的通了,二是用三个input文件建立回归基线,三是才谈扩展。

调试开关有两个值得记的。Bison给了-t参数,生成的语法分析器会带调试代码;但更常用的做法是在main.c里设置yydebug变量:

extern int yydebug; int main(int argc, char** argv) { #ifdef YYDEBUG yydebug = 1; #endif // 原有逻辑 }

编译时加上-DYYDEBUG=1,运行后每个移进、归约动作都会打印一条状态转换记录,能看到语法分析器睁开眼睛后的完整路径。Flex那边则用-d参数生成调试版词法分析器,两种开关一起开,就能把词法和语法一步步全看清。调试完记得把开关撤掉,否则答辩演示时终端会刷上千行状态记录。

第二个习惯是让三个input文件当回归用例。input1.c、input2.c、input3.c是资源里配好的测试程序,分别覆盖不同的语法特性,有的侧重变量声明和表达式,有的侧重if/else嵌套和函数调用。拿它们顺序跑一遍:

./compiler input1.c > out1.txt ./compiler input2.c > out2.txt ./compiler input3.c > out3.txt

把三份输出保存下来,之后每改一处文法或者动作,就再跑一遍对比,输出结构没变才算没改坏。这就是最朴素的回归测试。批处理可以用shell里一层for循环搞定,不需要引入额外工具。

最后才是扩展。课设改完验收后,如果你想在这里继续加东西,有两个低成本且加分的方向。一个是做符号表:在var-declaration归约动作里往一个哈希表注册变量名,语法分析完成后打印符号表,展示声明检查的能力。另一个是做错误恢复:现在yyerror一碰到语法错误就终止,改成在statement产生式里捕获错误后跳过一定数量的token继续解析,能让一个输入文件同时暴露多处错误。这两块改动都不会破坏原有文法,只是在已有的归约动作里加逻辑。

我自己的固定流程是:改任何一处syntax.y或lexical.l之后,强制跑一遍bison -d -v syntax.y && flex lexical.l && make clean && make,然后三个input各跑一次,diff输出。资源里给的那些文件正好当这套流程的演练对象。这个习惯后来帮我避免过至少三次把好端端的课设改崩的糗事,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询