
简介本资源是四川大学编译原理课程配套实验教学材料面向计算机专业本科生及编译技术初学者系统支撑词法分析、语法分析、语义分析与代码生成四大核心阶段的动手实践。压缩包共53个文件涵盖C语言源码c/c-、头文件h、测试用例txt/tny、说明文档docx/md、PPT课件pptx及Makefile构建脚本等其中C/C-源码与测试用例构成完整实验闭环README.md与各周说明文档提供清晰实验目标与执行指引.gv文件辅助理解语法分析过程。资源大小2.24MB结构按Week 6–12分层组织便于循序渐进学习。已有157人下载学习读者可直接复现实验环境、对照源码理解编译器各模块实现逻辑、参考测试用例验证功能正确性并借助PPT与说明书掌握理论与工程的衔接要点是夯实编译原理实践能力的高价值入门套件。1. 四川大学编译原理实验课不是“抄报告”而是用真实源码打通词法扫描→语法分析→语义处理的完整链路你手头这个.zip文件表面看是“课程配套材料”实则是国内高校中少有的、从零构建一个可运行微型编译器前端的全栈实验包。它不依赖 LLVM 或 ANTLR 黑盒所有核心模块——从scanner.lFlex 生成词法分析器到parser.yBison 生成递归下降式语法分析器再到ast.c和symbol_table.c的手工实现全部开源、可调试、可单步跟踪。这不是玩具项目它能正确解析类似int a 3 b * (c - 1);的 C 风格声明与表达式并输出带行号、类型、作用域信息的抽象语法树AST和符号表。适合两类人一是刚学完《编译原理》第三版第二章、卡在“正规式怎么转 NFA”就放弃的本科生二是想补足工程落地能力、但被网上碎片化“手写 parser”教程绕晕的转行者。它不教理论推导只给你一把能砍开编译器黑匣子的刀——刀柄上刻着 Makefile刀刃上写着yy_scan_string()和yyparse()。2. 用 Flex Bison 在本地跑通最小可执行编译器前端从解压到看到 AST 输出2.1 解压后目录结构解读哪些文件动不得哪些必须改解压后你会看到典型三层结构scu-compiler-lab/ ├── src/ # 核心源码C 实现 │ ├── scanner.l # Flex 词法规则定义标识符、数字、运算符等 token │ ├── parser.y # Bison 语法规则EBNF 描述 program → stmt*expr → term (|-) expr 等 │ ├── ast.c / ast.h # 手工实现的 AST 节点创建与遍历含内存管理 │ ├── symbol_table.c # 哈希表实现的符号表支持嵌套作用域 │ └── main.c # 入口调用 yy_scan_string() 输入字符串yyparse() 启动分析 ├── docs/ # 说明书 PDF重点看“实验二语法分析器设计”章节的错误处理要求 └── Makefile # 关键控制 flex/bison 代码生成与 C 编译全流程提示scanner.l和parser.y是你的主战场但绝不能直接编译。Flex/Bison 会将它们分别生成lex.yy.c和parser.tab.c—— 这两个文件才是 GCC 真正编译的对象。Makefile就是干这个转换的。2.2 三步跑通生成 lexer/parser → 编译 → 运行测试用例第一步确认环境并生成中间代码确保系统已安装flex、bison、gccLinux/macOS 原生支持Windows 用户请用 WSL2不要用 MinGW 或 CygwinBison 在后者下有路径解析 bug# 检查版本关键本实验要求 flex ≥ 2.6.4, bison ≥ 3.0.4 flex --version bison --version # 进入 src 目录手动触发生成比直接 make 更易定位问题 cd src flex scanner.l # 生成 lex.yy.c bison -d parser.y # 生成 parser.tab.c 和 parser.tab.h参数说明-d选项强制生成parser.tab.h其中包含YYSTYPE定义和 token 枚举如TOK_INT,TOK_ID。main.c通过#include parser.tab.h获取这些符号否则编译必报unknown type name YYSTYPE。第二步用 Makefile 编译可执行文件回到项目根目录执行make clean # 清理上次残留的 .o 和可执行文件 make # 执行 Makefile 中的 all: target此时Makefile的核心逻辑生效# Makefile 片段已简化实际含详细依赖规则 all: compiler compiler: lex.yy.o parser.tab.o ast.o symbol_table.o main.o gcc -o compiler lex.yy.o parser.tab.o ast.o symbol_table.o main.o -lfl lex.yy.o: lex.yy.c parser.tab.h gcc -c lex.yy.c parser.tab.o: parser.tab.c parser.tab.h gcc -c parser.tab.c # 关键依赖parser.tab.c 必须在 lex.yy.c 之后生成否则 parser.tab.h 不存在 lex.yy.c: scanner.l flex scanner.l parser.tab.c parser.tab.h: parser.y bison -d parser.y逻辑说明-lfl链接 Flex 运行时库提供yy_scan_string等函数。若省略链接阶段报错undefined reference to yy_scan_string。这是新手最常翻车的第一关。第三步运行并验证输出编译成功后执行./compiler程序会等待标准输入。粘贴以下测试代码注意末尾分号int x 10; x x 2 * 3;回车后应输出类似[AST] ProgramNode └── [Stmt] DeclStmt: int x 10 └── [Expr] AssignExpr: x 10 └── [Stmt] AssignStmt: x x 2 * 3 └── [Expr] BinaryOp: (x, BinaryOp: * (2, 3)) [SymbolTable] Scope: global x - typeint, line1, scopeglobal验证意义这证明词法扫描识别int,x,10,,*、语法分析构建出AssignStmt和BinaryOp节点、语义处理符号表记录x类型为int三阶段全部贯通。不是“打印 hello world”而是真实 AST 结构体指针的递归遍历结果。3. 语法分析器深度定制修改 parser.y 实现 if-else 支持与错误恢复3.1 在 parser.y 中添加 if-else 语法规则EBNF 到 C 代码映射打开src/parser.y找到%token声明区追加新 token%token TOK_IF TOK_ELSE TOK_LBRACE TOK_RBRACE然后在语法规则部分%%之后在stmt规则下插入if_stmt分支stmt : decl_stmt | assign_stmt | if_stmt /* 新增 */ ; if_stmt : TOK_IF ( expr ) stmt %prec TOK_IF /* %prec 解决悬空 else 二义性 */ | TOK_IF ( expr ) stmt TOK_ELSE stmt ;参数说明%prec TOK_IF是关键。Bison 默认按最后出现的 token 定义优先级此处强制if分支使用TOK_IF的优先级高于TOK_ELSE使if (a) if (b) x; else y;中的else绑定到最近的if而非外层if—— 这正是 C 语言的悬空 else 规则。不加此行Bison 会报 shift/reduce conflict。接着在if_stmt的动作块中构建 AST 节点需先在ast.h中定义IfNode结构if_stmt : TOK_IF ( expr ) stmt { $$ new_if_node($3, $5, NULL); // $3expr, $5then_stmt, NULLelse_stmt } | TOK_IF ( expr ) stmt TOK_ELSE stmt { $$ new_if_node($3, $5, $7); // $7else_stmt } ;逻辑说明$1,$2... 是规则中第 1、2 个符号的语义值$$是当前规则的返回值。new_if_node()是你在ast.c中实现的函数负责 malloc 内存并初始化IfNode结构体字段cond,then_branch,else_branch。必须检查$3条件表达式是否为布尔类型否则在语义分析阶段报错。3.2 错误恢复机制当语法错误发生时让解析器跳过非法 token 继续工作默认情况下Bison 遇到错误如int x 5;中多了一个会打印syntax error并终止。要实现“吃掉错误 token 后继续解析”需在parser.y中添加错误产生式// 在 stmt 规则后添加 error_stmt : error ; { yyerrok; } /* 匹配任意错误后跟分号重置错误状态 */ ; stmt : decl_stmt | assign_stmt | if_stmt | error_stmt /* 将 error_stmt 加入 stmt 可选分支 */ ;参数说明error是 Bison 内置 token代表任意非法输入yyerrok是 Bison 提供的函数调用后清除错误标志允许后续正常解析。{ yyerrok; }必须放在动作块中不能写成error ; { yyerrok; }以外的形式。这是让编译器具备“容错性”的最小代价方案。4. 词法扫描器精准控制定制 scanner.l 处理浮点数、注释与预处理器指令4.1 扩展 scanner.l 支持浮点数字面量如 3.14, .5, 1e-3打开src/scanner.l在规则区%%之后添加浮点数正则表达式/* 在整数规则之后、运算符规则之前插入 */ [0-9]\.[0-9]*([eE][-]?[0-9])? { yylval.dval strtod(yytext, NULL); return TOK_FLOAT; } \.[0-9]([eE][-]?[0-9])? { yylval.dval strtod(yytext, NULL); return TOK_FLOAT; } [0-9][eE][-]?[0-9] { yylval.dval strtod(yytext, NULL); return TOK_FLOAT; }逻辑说明Flex 按规则从上到下匹配且取最长匹配。因此必须把浮点规则放在整数规则[0-9]之前否则123.45会被截断为123整数.45非法字符。yylval.dval是 Bison 定义的联合体成员需在parser.y的%union中声明double dval用于向语法分析器传递浮点数值。4.2 过滤 C 风格注释/* ... */ 和 // ...避免干扰解析在scanner.l的规则区顶部%{和%}之间添加全局变量声明%{ #include parser.tab.h #include stdio.h #include string.h extern int comment_depth; // 声明定义在 scanner.l 底部 %}然后在规则区添加注释处理规则/* { comment_depth 1; BEGIN(COMMENT); } COMMENT/* { comment_depth; } COMMENT*/ { comment_depth--; if (comment_depth 0) BEGIN(INITIAL); } COMMENT.|\n { /* 忽略所有内容 */ } //([^\n])* { /* 单行注释直接忽略 */ }参数说明BEGIN(COMMENT)切换到COMMENT状态需在%s COMMENT声明comment_depth用于嵌套注释计数如/* outer /* inner */ outer */。COMMENT.|\n表示在 COMMENT 状态下忽略任意字符和换行符。切勿用/*.*?*/正则—— Flex 不支持非贪婪匹配会导致跨行注释失败。5. 避坑指南编译原理实验中最常踩的 4 个深坑及血泪解决方案5.1 现象make报错vitis make[2]: *** [makefile:18: libs] error 1原因标题里提到的vitis是 Xilinx FPGA 开发工具与本实验完全无关。此错误源于你误将本项目的 Makefile 复制到了其他含 vitis 工程的目录下或系统 PATH 中存在旧版make冲突。本实验的 Makefile 不含libstarget第 18 行不可能是libs。解决which make确认调用的是 GNU Make路径含/usr/bin/make或/usr/local/bin/makemake -f Makefile显式指定文件排除其他 Makefile 干扰删除项目目录外所有Makefile只保留本项目根目录下的那个。5.2 现象bison -d parser.y成功但gcc -c parser.tab.c报错unknown type name YYSTYPE原因parser.tab.h未被parser.tab.c正确包含。Bison 生成的parser.tab.c默认#include parser.tab.h但若parser.tab.h不在当前目录或#include路径错误则找不到。解决检查parser.tab.c开头是否有#include parser.tab.h不是parser.tab.h确保parser.tab.h与parser.tab.c在同一目录即src/在parser.tab.c顶部手动添加#include parser.tab.h若缺失。5.3 现象输入int a;正常但输入int a 5;时解析失败报syntax error before 原因scanner.l中被识别为TOK_ASSIGN但parser.y的decl_stmt规则未定义赋初值语法。原实验可能只支持声明不支持初始化。解决在parser.y的%token区添加TOK_ASSIGN修改decl_stmt规则decl_stmt : TOK_INT TOK_ID ; { $$ new_decl_node($2, int, NULL); } | TOK_INT TOK_ID TOK_ASSIGN expr ; { $$ new_decl_node($2, int, $4); } ;在ast.c中更新new_decl_node()支持存储初始值表达式指针。5.4 现象./compiler运行后无输出程序直接退出原因main.c中yyparse()返回 0 表示成功但若输入为空或只有空白符yyparse()可能因 EOF 立即返回而main()未打印任何提示。解决在main.c的yyparse()调用后添加int result yyparse(); if (result ! 0) { fprintf(stderr, Parse failed with code %d\n, result); return 1; } printf(Parse succeeded.\n);测试时务必输入以分号结尾的有效语句如int x;而非空行或纯空格。6. 进阶验证用 GDB 单步调试 parser.y 动作块亲眼看见 AST 节点如何被 malloc 出来6.1 编译时加入调试信息并启动 GDB重新编译强制加入-g标志修改Makefile中gcc命令# 将原 gcc 命令改为 gcc -g -c lex.yy.c gcc -g -c parser.tab.c gcc -g -o compiler lex.yy.o parser.tab.o ast.o symbol_table.o main.o -lfl然后启动 GDBgdb ./compiler (gdb) break yyparse # 在语法分析入口设断点 (gdb) run # 当程序停住时输入测试代码回车 int x 1; # GDB 会停在 yyparse() 第一行6.2 在 Bison 动作块中设置断点追踪 AST 构建全过程Bison 生成的parser.tab.c中每个语法规则的动作块会被编译为 C 代码片段。例如decl_stmt规则对应的部分在parser.tab.c中形如case 12: /* decl_stmt: TOK_INT TOK_ID ; */ #line 87 parser.y { $$ new_decl_node($2, int, NULL); } #line 1234 parser.tab.c break;在 GDB 中直接按行号打断点行号见#line指令(gdb) break parser.tab.c:1234 (gdb) continue当解析int x;时GDB 会在new_decl_node($2, int, NULL)这一行暂停。此时可查看$2的值即x的字符串地址(gdb) print $2 $1 0x55555556a2a0 x (gdb) step # 进入 new_decl_node() 函数进入ast.c后可单步执行malloc(sizeof(DeclNode))观察内存分配DeclNode* new_decl_node(char* id, char* type, ExprNode* init) { DeclNode* node malloc(sizeof(DeclNode)); // 此处 step看 node 地址 node-id strdup(id); // 此处 step看 id 是否复制成功 node-type strdup(type); node-init init; return node; }验证价值这让你第一次真正“看见”编译器如何把文本int x;转化为内存中一个结构体实例。不是靠printf猜而是用print node-id确认字符串已拷贝用x/10xw node查看结构体 10 个字的原始内存布局。这种对内存的掌控感是刷一百道 LeetCode 换不来的底层直觉。6.3 用 GDB 检查符号表插入过程验证作用域嵌套是否正确在symbol_table.c的insert_symbol()函数入口设断点(gdb) break symbol_table.c:45 (gdb) continue当解析int x 1;时GDB 会停在insert_symbol()。此时检查传入参数(gdb) print name $2 0x55555556a2a0 x (gdb) print type $3 0x55555556a2b0 int (gdb) print current_scope $4 0x55555556a2c0 # 查看该地址内容p *(Scope*)$4若current_scope-parent为NULL说明x被插入全局作用域若解析if (a) { int y; }后y的current_scope-parent指向if对应的作用域节点则嵌套正确。我带过三届学生做这个实验90% 的人卡在yylval类型不匹配或parser.tab.h包含路径错误上而不是理论不会。所以我的习惯是每次改完scanner.l或parser.y第一件事不是make而是flex -d scanner.l bison -d -v parser.y然后立刻cat parser.output | head -20看 Bison 是否报告 shift/reduce conflict —— 这比编译失败后再查日志快十倍。希望帮到你。本文还有配套的精品资源点击获取