ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

四川大学编译原理实验:手写词法分析器与语法解析器实战

四川大学编译原理实验:手写词法分析器与语法解析器实战 简介本资源是四川大学《编译原理》课程配套实验教学材料面向计算机专业本科生及编译技术初学者系统支撑词法分析、语法分析、语义分析与代码生成四大核心阶段的动手实践。压缩包共53个文件涵盖C语言源码.c/.c-、头文件.h、测试用例.txt/.tny、Makefile构建脚本、实验说明文档.docx、课程设计PPT.pptx及README.md项目指南2.24MB体积轻量实用便于本地编译运行与结构化学习。已有157人下载学习适合课堂同步实践、课程设计参考或自学复现编译器前端组件。资源按周次组织Week 6–12每阶段均含可运行代码、测试样例与图文说明尤其突出tiny语言语法分析、DFA图解.gv、三地址码生成等关键实现细节辅以多版本对比文件如c1-sample.txt/c1.txt帮助理解修改逻辑是理论落地与工程能力培养的优质实操载体。1. 四川大学编译原理实验课不是抄报告是亲手把“hello world”喂给词法分析器嚼碎再吐出语法树你手头这个.zip文件表面看是“四川大学编译原理课程-实验课相关代码与实验报告”但实际它是一套可运行、可调试、可打断点的微型编译器骨架——从hello.c这样的极简 C 子集源码开始经词法扫描Lexical Scanner、语法分析Parser、中间代码生成IR Generation最终输出类汇编三地址码。它不依赖 LLVM 或 GCC 后端所有核心逻辑用 C/C 手写Makefile 控制全流程.y和.l文件直连 Flex/Bison 工具链。这不是教学演示动画而是学生真正在实验室里一行行改、一次次make clean make、在 GDB 里看着yytext怎么被切分、yylval如何传进语法栈的实操包。适合刚学完《编译原理》清华大学出版社第三版前四章、正卡在“理论懂了但写不出 scanner”阶段的本科生也适合想补全编译器工程闭环的嵌入式/系统方向开发者——毕竟搞懂makefile怎么把.l编译成lex.yy.c比背诵 LL(1) 文法判定条件更能让你在秋招笔试里多抢 30 秒。2. 从解压到跑通用最小命令链验证词法扫描器是否真正“看见”了关键字2.1 解压后第一眼该盯住哪三个文件别急着打开 PDF 实验报告。先unzip 四川大学编译原理课程-实验课相关代码与实验报告-内含源码和说明书.zip进入主目录后立刻执行ls -la你会看到类似这样的结构├── Makefile ├── src/ │ ├── lexer.l # Flex 词法规则定义核心 │ ├── parser.y # Bison 语法规则定义 │ ├── main.c # 主程序入口调用 yyparse() │ └── utils.h/.c # 错误处理、符号表基础函数 ├── test/ │ └── hello.c # 测试用例最简 C 子集代码 └── doc/ └── 实验指导书.pdf提示lexer.l是整个实验的起点。它定义了int,if,while等关键字如何被识别数字、标识符、运算符怎么切分。parser.y负责把 lexer 输出的 token 序列组装成语法树。二者通过#include parser.tab.h和yylval联动——这是新手最容易断联的“黑匣子”。2.2 用flexbison生成扫描器和解析器源码本实验不提供预编译的lex.yy.c或parser.tab.c必须本地生成。确保已安装 Flex 和 BisonUbuntu/Debian 下sudo apt install flex bisonmacOS 用brew install flex bisoncd src flex lexer.l # 生成 lex.yy.c词法分析器 C 源码 bison -d parser.y # 生成 parser.tab.c 和 parser.tab.h-d 表示生成头文件执行后检查lex.yy.c是否存在且大小 5KB太小说明 flex 未成功读取规则parser.tab.c和parser.tab.h是否成对出现缺.h会导致main.c编译报unknown type name YYSTYPE参数说明bison -d是关键。若漏掉-dBison 只生成.c不生成.h而lexer.l中#include parser.tab.h就会失败。很多同学卡在这一步以为是环境问题其实是命令少了一个字母。2.3 编译链接Makefile 里藏着三处必须手动校验的路径回到项目根目录查看Makefile内容不要直接make先读CC gcc CFLAGS -Wall -g -I./src SRC_DIR ./src OBJ_DIR ./build TARGET compiler $(TARGET): $(OBJ_DIR)/main.o $(OBJ_DIR)/lex.yy.o $(OBJ_DIR)/parser.tab.o $(CC) $^ -o $ -lfl $(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR) $(CC) $(CFLAGS) -c $ -o $ $(OBJ_DIR)/lex.yy.o: $(SRC_DIR)/lex.yy.c $(CC) $(CFLAGS) -c $ -o $ $(OBJ_DIR)/parser.tab.o: $(SRC_DIR)/parser.tab.c $(CC) $(CFLAGS) -c $ -o $ $(OBJ_DIR): mkdir -p $ .PHONY: clean clean: rm -rf $(OBJ_DIR) $(TARGET)重点校验三处CFLAGS中-I./src确保#include utils.h能被找到$(OBJ_DIR)/lex.yy.o的依赖项是$(SRC_DIR)/lex.yy.c而非lexer.l—— 这意味着flex必须提前手动运行Makefile 不自动调用flex链接时-lfl这是 Flex 库提供yywrap()等基础函数缺它会报undefined reference to yywrap。验证命令mkdir -p build gcc -Wall -g -I./src -c src/main.c -o build/main.o gcc -Wall -g -I./src -c src/lex.yy.c -o build/lex.yy.o gcc -Wall -g -I./src -c src/parser.tab.c -o build/parser.tab.o gcc build/main.o build/lex.yy.o build/parser.tab.o -o compiler -lfl如果这四行能成功执行说明环境和源码完全就绪。2.4 运行测试让hello.c在你的终端里“被编译”一次准备测试输入test/hello.c内容通常为int main() { int a 10; if (a 5) { return a; } }执行./compiler test/hello.c预期输出取决于实验要求若只做词法分析打印INT,IDENTIFIER main,LBRACE,INT,IDENTIFIER a,ASSIGN,NUMBER 10, ... 一串 token若完成语法分析输出缩进格式的语法树如(FuncDef (Type INT) (Ident main) ...)或三地址码如t1 10,t2 a 5,if t2 goto L1。关键观察点当./compiler test/hello.c报错时第一行错误信息永远来自main.c中的yyparse()调用位置而非lexer.l或parser.y。这意味着错误定位要先看main.c的printf(Parse error at line %d\n, yylineno);是否启用再顺藤摸瓜查yylineno为何没更新——这往往暴露了lexer.l中yylineno的缺失或位置错误。3. 语法分析器调试为什么yyparse()总是返回 1三步定位文法冲突3.1 理解yyparse()返回值的工程含义yyparse()是 Bison 生成的解析器入口函数其返回值有严格语义0成功解析完整个输入遇到YYEOF1语法错误如if (x缺少右括号2内存分配失败极少见。当你反复得到return 1说明parser.y定义的文法无法匹配当前lexer.l输出的 token 流。这不是代码 bug而是文法设计与词法输出不匹配——比如lexer.l把识别为单个EQtoken但parser.y中却写成expr EQ expr而实际lexer.l输出的是两个EQ即就会导致归约失败。3.2 开启 Bison 调试模式让语法树“开口说话”修改parser.y头部加入调试开关%define parse.error verbose %debug %error-verbose并在main.c中启用调试输出extern int yydebug; int main(int argc, char *argv[]) { if (argc ! 2) { /* ... */ } yydebug 1; // 关键开启 Bison 调试日志 FILE *fp fopen(argv[1], r); if (!fp) { /* ... */ } yyin fp; int result yyparse(); printf(Parse result: %d\n, result); fclose(fp); return result; }重新编译运行make clean make ./compiler test/hello.c你会看到类似输出Starting parse Entering state 0 Reading a token: Next token is token INT () Shifting token INT () Entering state 2 Reading a token: Next token is token IDENTIFIER () ... Reducing stack by rule 5 (line 45): - $1逻辑说明这些日志显示了 LR(1) 分析器的状态转移过程。“Shifting” 表示移进“Reducing” 表示归约。当卡在某个状态迟迟不归约或反复Reading a token却不Shifting说明当前 token 不在该状态的 ACTION 表中——即文法未覆盖该 token 组合。3.3 用bison -v生成详细分析表定位 shift/reduce 冲突在src/目录下执行bison -v parser.y会生成parser.output文件。用less parser.output查看重点搜索conflictState 17 conflicts: 1 shift/reduce ... state 17 expr: expr . expr expr: expr . - expr expr: expr . * expr shift, and go to state 20 - shift, and go to state 21 * shift, and go to state 22 reduce using rule 5 (expr) - reduce using rule 5 (expr)这表示在 state 17遇到时既可以移进期待后续expr也可以按 rule 5 归约把已有的expr当作完整表达式。这就是典型的优先级/结合性缺失。解决方法在parser.y顶部添加%left - %left * / %right UMINUS /* 用于负号 -5 */然后删除旧的parser.tab.*重新bison -d parser.y。%left告诉 Bison当冲突发生时优先选择shift即把当作运算符而非归约边界并赋予左结合性。参数说明%left和%right不是语法糖而是直接改写 Bison 内部的冲突解决策略。没有它们a b c会被错误地解析为a (b c)右结合而数学要求左结合。4. 避坑指南编译原理实验中最常踩的 5 个“血泪坑”4.1 现象make报错makefile:18: *** No rule to make target libs原因Makefile第 18 行写了libs:目标但当前目录下既无libs文件也无对应规则。常见于学生从其他项目复制 Makefile 时未清理冗余目标。解决打开Makefile定位第 18 行删除整行libs:及其后续所有以 Tab 开头的命令行或确认是否真需构建libs若是则补全libs: $(OBJ_DIR)/utils.o等依赖。4.2 现象lexer.l中printf(KEYWORD: %s\n, yytext);输出乱码或截断原因yytext是 Flex 内部指针指向扫描缓冲区不能长期持有或跨函数使用。若在lexer.l中将其赋值给全局变量char* last_token下次扫描时该内存已被覆盖。解决立即拷贝内容。改为strncpy(last_token, yytext, sizeof(last_token)-1); last_token[sizeof(last_token)-1] \0;且last_token必须声明为static char last_token[256];。4.3 现象yyparse()成功返回 0但语法树为空或节点缺失原因parser.y中的语义动作{ ... }块未正确设置$$当前产生式左部值。例如stmt: IF ( expr ) stmt { $$ new_if_node($3, $5); }若$3expr或$5stmt本身为NULLnew_if_node可能返回NULL导致父节点丢失。解决在每个语义动作中加空值检查stmt: IF ( expr ) stmt { if ($3 NULL || $5 NULL) { YYERROR; // 主动报错而非静默返回 NULL } $$ new_if_node($3, $5); }4.4 现象test/hello.c中int a 10;被识别为INT IDENTIFIER NUMBER ;但parser.y中decl: TYPE IDENTIFIER ASSIGN NUMBER SEMI规则不触发原因lexer.l中和、!等混淆。典型错误是 { return EQ; } { return ASSIGN; } ! { return NE; } { return ASSIGN; } // ❌ 重复定义Flex 按最长匹配优先但此处两条 规则冲突解决删除重复规则确保每个 pattern 唯一用flex -v lexer.l查看警告Flex 会提示 can match...。4.5 现象在 macOS 上编译报错ld: library not found for -lfl原因macOS 自带的libfl位于非标准路径且新版 Xcode 命令行工具默认不链接。解决两种方案任选其一① 安装 Homebrew Flex 并强制使用brew install flex export PATH/opt/homebrew/opt/flex/bin:$PATH # Apple Silicon # 或 export PATH/usr/local/opt/flex/bin:$PATH # Intel make clean make② 修改Makefile用绝对路径链接# 替换原链接行 $(TARGET): $(OBJ_DIR)/main.o $(OBJ_DIR)/lex.yy.o $(OBJ_DIR)/parser.tab.o gcc $^ -o $ /opt/homebrew/opt/flex/lib/libfl.a # Apple Silicon 路径5. 进阶技巧用 GDB 逐行跟踪yylex()如何切分while (i 10)5.1 设置断点前的关键准备让yylex()可见且可停默认情况下Flex 生成的lex.yy.c中yylex()函数被声明为static int yylex(void)GDB 无法直接break yylex。需在lexer.l顶部添加%{ #include parser.tab.h #include stdio.h // 强制取消 static 修饰 #undef yylex int yylex(void); %}然后重新生成flex lexer.l。此时lex.yy.c中的yylex声明变为int yylex(void)GDB 可识别。5.2 GDB 调试实战三步锁定while识别逻辑假设test/while.c内容为while (i 10) { i i 1; }启动 GDBgdb ./compiler (gdb) break yylex (gdb) run test/while.c首次停在yylex()入口。用step逐行执行重点关注yy_c_buf_p指针位置当前扫描字符yytext内容当前匹配文本yyleng长度匹配长度。典型流程yy_c_buf_p指向w→ 匹配规则while→yytext while→return WHILE;yy_c_buf_p移至 空格→ 匹配[ \t\n]→yytext →return 0跳过空白yy_c_buf_p移至(→ 匹配(→return LPAREN;技巧在 GDB 中用print (char*)yytext查看当前 token 字符串用x/10c yy_c_buf_p查看后续 10 字符原始内容。你会发现while被识别后yy_c_buf_p自动跳过 5 字节精准停在空格上——这就是 Flex 的input()函数在底层控制的指针移动。5.3 用yy_scan_string()注入动态字符串绕过文件 IO 调试不想每次改test/while.c再重跑在main.c中临时替换文件读取逻辑#include string.h // ... int main(int argc, char *argv[]) { // 注释掉原文件读取 // FILE *fp fopen(argv[1], r); // yyin fp; // 改为内存字符串扫描 const char *test_code while (i 10) { i i 1; }; yy_scan_string(test_code); // Flex 提供的 API int result yyparse(); printf(Parse result: %d\n, result); // fclose(fp); // 不需要 fclose return result; }重新编译运行GDB 断点直接落在内存字符串上修改test_code字符串即可秒级验证新规则。5.4 一个真实教训我曾花 7 小时 debugyylineno不更新那是我第一次实现#line指令支持。lexer.l中写了#line[ \t][0-9] { sscanf(yytext, #line %d, yylineno); // ❌ 错误yytext 格式为 #line 123sscanf 会失败 }实际yytext是#line 123带空格sscanf读不到数字。正确写法是#line[ \t][0-9] { char *p yytext 5; // 跳过 #line while (*p || *p \t) p; yylineno atoi(p); }后来我在lexer.l顶部加了一行#define DEBUG_LEXER并在每个规则末尾加{ printf(LINE %d: %s - %s\n, yylineno, yytext, KEYWORD); }——从此所有 token 都带行号输出yylineno问题再没出现过。希望帮到你。本文还有配套的精品资源点击获取
返回列表