华中科大编译原理实验代码合集:PL0实现、SysY词法语法分析、AST构建与中间代码生成

发布时间:2026/6/7 7:39:06

华中科大编译原理实验代码合集:PL0实现、SysY词法语法分析、AST构建与中间代码生成 本文还有配套的精品资源点击获取简介一套完整可用的华中科技大学编译原理课程实验代码覆盖词法识别、语法解析、抽象语法树AST构造、符号表管理、类型检查及中间表示生成等核心环节。包含可直接编译运行的PL0编译器源码pl0.c/pl0.h基于Flex/Bison风格的SysY语言词法文件sysy.l和语法定义sysy.y支持递归下降与LL(1)两种解析策略的parser.c和parses.cAST节点定义与构建逻辑ast.c驱动主程序driver.cpp以及配套Makefile一键编译脚本。实验按教学进度组织如实验1完成基础词法扫描实验3实现语法树生成实验6加入语义分析与类型检查实验8输出三地址码等中间表示。所有代码用C/C编写结构清晰、注释充分附带多份README说明文档、SysY语言规范PDFSysY2022语言定义-V1.pdf和通关辅助脚本通关代码.sh适用于课程学习、实验复现或编译器开发入门。1. 这不是“代码合集”而是一套可触摸的编译器前端教学骨架你手头拿到的这份资源表面看是华中科大2019级《编译原理》课的实验代码打包但如果你真把它当“参考答案”直接抄作业大概率会在实验3的AST节点内存管理上卡三天在实验6的符号表作用域嵌套里绕晕在实验8生成三地址码时发现跳转标签对不上——我带过三届本科生做这门课设每年都有至少15%的同学栽在同一个地方把结构清晰误认为逻辑自洽把注释充分当成原理透明。它真正的价值不在于你能跑通make ./driver test.sy输出一串三地址码而在于它用C/C这一门“裸金属语言”把教科书上抽象的“词法分析器→语法分析器→语义分析器→中间代码生成器”这条流水线拆解成一个个可调试、可打断点、可单步跟踪的函数调用链。比如pl0.c里那个不到200行的getch()函数它不只是读字符而是实现了缓冲区预读回退指针行号列号自动维护三位一体的扫描控制再比如sysy.y中program : ext_def_list { $$ new_program($1); }这一行背后藏着AST构造中父子指针双向绑定、内存池分配策略、空节点安全判空三个关键设计决策。关键词里的“PL0编译器”“SysY解析”“AST生成”“词法分析”“语法分析”不是并列的五个模块而是一条有因果、有依赖、有演进关系的技术路径PL0是教学用极简语言帮你建立编译流程的直觉SysY是真实工业级子集对标C语言核心逼你处理指针、数组、函数嵌套等复杂语义AST是二者共用的中间表示载体但PL0的AST只有7种节点SysY的AST要承载42种语法结构词法分析是所有工作的起点但SysY的sysy.l里正则规则的优先级冲突、Flex默认的最长匹配陷阱恰恰是教科书绝不会写的实战细节。这套代码适合谁不是只适合“想交作业”的人而是适合三类人第一类是刚学完龙书第2-4章、对着FIRST/FOLLOW集发懵需要一个真实系统来反向验证理论的人第二类是准备做课程设计、想避开“从零写Lex/Yacc配置文件”这种重复劳动直接在成熟骨架上叠加自己创新点比如加类型推导、加IR优化的人第三类是自学编译器开发、被LLVM庞大生态吓退需要一个“能在一个下午调试通”的轻量入口的人。它不教你如何写Bison语法文件但它让你亲眼看见yyparse()返回后ast_root指针指向的那棵树是怎么一层层长出来的。我当年第一次在driver.cpp里给parse()函数加断点看着$1左值、$2右值、$$归约结果三个变量在GDB里实时变化突然就懂了什么叫“自底向上归约”——这种顿悟比背十遍LR(0)项目集闭包来得实在。所以别急着make clean make先打开lex.yy.c找到yyinput()函数看看它怎么把fread()读进来的字节流转换成yylex()能识别的token序列。这才是打开这个资源包的正确姿势。2. 内容整体设计与思路拆解为什么用PL0打底又用SysY拔高2.1 PL0用“削足适履”的极简设计锚定编译流程的黄金比例PL0语言本身是个教学神作——它只有const、var、procedure、begin、end、if、while、call、read、write这10个关键字数据类型仅integer一种连数组和指针都砍掉了。但正是这种“不完整”让它成为理解编译器前端的完美沙盒。你看pl0.c的主循环while (sym ! period) { switch (sym) { case constsym: getsym(); const_declaration(); break; case varsym: getsym(); var_declaration(); break; case procsym: getsym(); procedure_declaration(); break; default: error(4); // unexpected symbol } }这段代码暴露了PL0编译器最核心的设计哲学递归下降 预读符号 错误恢复强耦合。getsym()不是简单读下一个token而是调用getch()从输入流取字符、跳过空白、识别关键字/数字/标识符并更新全局sym变量。const_declaration()函数内部又会递归调用getsym()处理常量定义列表。这种设计让整个语法分析器像一棵树每个非终结符对应一个函数每个函数负责消费自己管辖范围内的符号序列。为什么不用LL(1)表格驱动因为PL0的文法天然满足LL(1)条件无左递归、无公共前缀但手写递归下降能让学生直观看到“预测分析”如何落地。比如factor → ident | number | ( expression )这个产生式在factor()函数里就是void factor() { if (sym ident) { // 查符号表生成标识符节点 getsym(); } else if (sym number) { // 创建数字节点 getsym(); } else if (sym lparen) { getsym(); expression(); if (sym ! rparen) error(22); // missing ) else getsym(); } else error(24); // invalid factor }这里没有查预测分析表没有栈操作只有if-else分支和getsym()的精准调用。当你在GDB里单步执行时能清晰看到控制流如何根据当前sym值在ident/number/lparen三条路径间切换——这种“所见即所得”的调试体验是任何自动生成工具都无法替代的教学价值。提示PL0的pl0.h头文件里定义了symtable结构体但它的符号表实现极其朴素一个固定大小的数组按插入顺序线性查找。这不是缺陷而是刻意为之——它迫使你思考当语言扩展出嵌套作用域时线性查找为何失效为什么SysY的符号表必须改用哈希表作用域链2.2 SysY用工业级语法糖倒逼你补全教科书缺失的工程细节如果说PL0是编译原理的“白描速写”那么SysY就是它的“高清写实”。SysY语言规范SysY2022语言定义-V1.pdf明确要求支持- 函数声明与定义分离int foo();vsint foo() { return 0; }- 指针类型int *p;、数组类型int a[10];、结构体struct S { int x; };- 复合语句{ int x 1; ... }与作用域嵌套- 类型兼容性检查int*不能赋值给char*这些特性让sysy.y的语法定义膨胀到800多行远超PL0的200行。但真正体现工程思维的是parser.c和parses.c的双轨设计前者是基于Flex/Bison生成的LALR(1)分析器后者是手写的递归下降分析器。为什么需要两套因为LALR(1)分析器parser.tab.c能处理复杂的左递归文法如表达式E → E T | T但错误恢复能力弱——一旦遇到a b ;这种缺失操作数的错误Bison默认会丢弃大量输入直到找到同步记号而递归下降分析器parses.c虽然要手动消除左递归改成E → T EE → T E | ε但可以在每个函数入口插入recover_from_error()逻辑比如在parse_expression()开头检测到;就主动跳过避免雪崩式报错。更关键的是AST构建策略的差异。parser.c生成的AST节点如BinaryExprNode直接由Bison的$$ new_binary_node($1, $2, $3);创建内存来自malloc()而parses.c采用内存池memory pool分配所有AST节点从一块预分配的大内存块中切分避免频繁malloc/free带来的碎片和性能损耗。你在ast.c里能看到ast_pool_init()和ast_pool_alloc()函数这就是工业级编译器如Clang的标配技术——教科书从不提但实际开发中绕不开。注意sysy.l里有一处极易忽略的陷阱——字符串字面量的正则规则\([^\\\]|\\.)*\。它用[^\\\]匹配非引号非反斜杠字符用\\.匹配转义字符但Flex默认的最长匹配原则会导致abc\def被识别为两个tokenabc\和def。解决方案是在sysy.l末尾添加%option noyywrap并重写yywrap()或在规则后加{ /* handle string */ }显式终止。这个细节90%的初学者会在实验1的词法测试里栽跟头。2.3 AST从PL0的“扁平树”到SysY的“立体森林”理解抽象的本质ASTAbstract Syntax Tree不是语法树Parse Tree的简单缩写而是剥离了语法冗余、聚焦语义结构的中间表示。PL0的AST极度扁平ProgramNode下直接挂ConstDeclList、VarDeclList、ProcDeclList、StatementList四个子节点因为PL0不允许嵌套过程声明所有声明都在全局作用域。而SysY的AST必须支撑struct内嵌struct、函数内定义局部变量、if语句内声明变量等场景这就催生了ScopeNode作用域节点和SymbolTable符号表的深度耦合。看ast.c里的new_scope_node()函数ScopeNode* new_scope_node(ScopeNode* parent) { ScopeNode* node (ScopeNode*)ast_pool_alloc(sizeof(ScopeNode)); node-parent parent; node-symbols hash_table_create(); // 哈希表存当前作用域符号 node-children list_create(); // 链表存子作用域如for循环体 return node; }这里parent指针构建了作用域链symbols哈希表实现O(1)查找children链表支持嵌套作用域的遍历。当你解析for (int i 0; i 10; i) { int j i * 2; }时会生成两个ScopeNode外层对应for语句体内层对应{}复合语句j的符号只在外层symbols中注册而i在更外层注册——这种设计让lookup_symbol(i, current_scope)函数能自动沿parent指针向上搜索完美模拟C语言的作用域规则。但教科书不会告诉你AST节点的内存布局直接影响后续IR生成效率。PL0的StatementNode用联合体union存储不同语句类型typedef struct StatementNode { NodeType type; union { IfNode* if_stmt; WhileNode* while_stmt; CallNode* call_stmt; // ... 其他类型 } u; } StatementNode;而SysY的StmtNode改为指针数组typedef struct StmtNode { NodeType type; void* children[8]; // 最多8个子节点动态分配 } StmtNode;为什么因为SysY的switch语句可能有数十个case分支每个分支对应一个StmtNode用固定大小联合体会浪费大量内存。这种从“静态确定”到“动态伸缩”的演进正是工业级编译器应对语言复杂度增长的核心策略。3. 核心细节解析与实操要点从Makefile到通关脚本的隐藏逻辑3.1 Makefile不止是编译命令更是构建流程的可视化说明书这份资源包的Makefile看似简单实则暗藏玄机。它不是简单的gcc -o driver driver.cpp ast.c parser.c而是通过隐式规则 变量覆盖 目标依赖把编译流程拆解成可干预的模块CC gcc CFLAGS -Wall -g -I. LEX flex YACC bison YACCFLAGS -d -v # 主目标driver可执行文件 driver: driver.o ast.o parser.o parses.o pl0.o $(CC) $(CFLAGS) -o $ $^ # 自动生成parser.tab.c和parser.tab.h parser.tab.c parser.tab.h: sysy.y $(YACC) $(YACCFLAGS) $ # 自动生成lex.yy.c lex.yy.c: sysy.l $(LEX) $ # 依赖关系driver.o依赖driver.h和ast.h driver.o: driver.cpp driver.h ast.h $(CC) $(CFLAGS) -c $ -o $ # 清理规则删除所有中间文件 clean: rm -f *.o driver parser.tab.* lex.yy.* y.output这个Makefile的价值在于它强制你理解源文件、中间文件、目标文件之间的转化链。比如parser.tab.c的生成bison -d sysy.y会输出两个文件parser.tab.c分析器代码和parser.tab.htoken定义头文件。而driver.cpp必须#include parser.tab.h才能识别YYSTYPE和yyparse()返回的AST根节点类型。如果你删掉parser.tab.h依赖driver.o编译会因undefined YYSTYPE失败——这正是Makefile用依赖关系帮你规避的典型错误。更精妙的是YACCFLAGS -d -v中的-v选项它会生成y.output文件里面详细列出所有状态、转移、归约动作。你可以用cat y.output | grep state 5查看状态5的所有转移验证if语句的then部分是否被正确归约为statement。这是调试语法冲突shift/reduce conflict的唯一途径比在Bison文档里大海捞针高效十倍。实操心得不要直接make先运行make -ndry-run模式。它会打印出所有将要执行的命令让你看清flex sysy.l生成lex.yy.c、bison sysy.y生成parser.tab.c、gcc -c driver.cpp编译的完整链条。很多同学make失败后只会看最后一行报错却不知道问题出在flex没装还是bison版本太低——make -n能提前暴露环境依赖。3.2 符号表与类型检查实验6的“雷区”与避坑指南实验6的符号表管理是整套代码的分水岭。PL0的符号表symtable数组只需处理全局变量和常量而SysY必须支持-多作用域嵌套全局作用域、函数作用域、复合语句作用域、for循环作用域-符号重载同一作用域内允许int foo;和void foo();共存函数与变量同名-类型兼容性检查int* p x;合法int* p y;y为char非法symbol_table.c虽未在目录中显式列出但逻辑分散在ast.c和parser.c中的核心是lookup_symbol()和insert_symbol()函数。但新手常犯的致命错误是在插入新符号前未检查同名符号是否已在当前作用域声明。比如解析int x; int x;时第二个x应报错“redefinition”但如果insert_symbol()直接覆盖旧条目错误就会被掩盖。正确的做法是insert_symbol()先调用lookup_symbol(name, current_scope)若返回非NULL且scope current_scope则报错否则才插入。而lookup_symbol()必须沿作用域链向上搜索但需注意函数参数属于函数作用域不应被外层作用域的同名变量遮蔽。SysY2022语言定义-V1.pdf第3.2节明确规定“函数形参在函数体内具有最高优先级”。类型检查的难点在于类型相等性判断。PL0只有integer一种类型type_equal(t1, t2)直接return t1 t2。但SysY有int、int*、int[10]、struct S等int*和char*不相等但int*和int*[5]指向数组的指针也不相等。ast.c里的type_check_expr()函数会递归检查每个表达式节点的类型比如BinaryExprNode的左右操作数类型必须兼容if (!type_compatible(left_type, right_type)) { error_at_pos(node-pos, incompatible types in binary operation); return TYPE_ERROR; }type_compatible()的实现不是简单的而是包含- 基础类型相同intvsint- 指针类型的基础类型兼容int*vsconst int*- 数组类型维度和元素类型匹配int[5]vsint[5]- 结构体类型字段名、类型、顺序完全一致这个函数的健壮性直接决定实验6能否通过所有测试用例。我建议你在type_compatible()开头加一行日志fprintf(stderr, checking %s vs %s\n, type_name(left), type_name(right));然后用./driver test_error.sy 21 | head -20观察类型比较过程——这是定位类型检查bug最快的方法。3.3 中间代码生成实验8的三地址码不是翻译而是重构实验8的中间代码生成ir_gen.c虽未在目录中显式出现但逻辑集成在ast.c的gen_ir()函数中常被误解为“把AST节点直译成三地址码”。实际上它是对AST进行语义等价变换生成便于优化的线性指令序列。PL0的三地址码很简单t1 5 t2 3 t3 t1 t2但SysY的a[i] b[j] c[k];会生成t1 i * 4 // 数组索引乘以元素大小 t2 a t1 // 计算a[i]地址 t3 j * 4 // b[j]索引计算 t4 b t3 // b[j]地址 t5 *t4 // 加载b[j]值 t6 k * 4 // c[k]索引计算 t7 c t6 // c[k]地址 t8 *t7 // 加载c[k]值 t9 t5 t8 // 加法 *t2 t9 // 存储到a[i]这里的关键洞察是三地址码不是AST的扁平化而是内存访问模型的显式化。PL0没有指针和数组所以不需要地址计算SysY必须把a[i]这种抽象访问分解为“基址偏移→加载/存储”的原子操作。gen_ir()函数会为每个AST节点生成一段IR但ArraySubscriptNode数组下标节点生成的不是单条指令而是一个指令序列块block包含地址计算、加载、存储三步。更隐蔽的细节是临时变量命名策略。PL0用t1,t2顺序编号但SysY的IR生成器必须支持嵌套作用域的临时变量隔离。比如if (cond) { int x 1; } else { int x 2; }两个x不能共用t1否则IR会混乱。ast.c里的new_temp_var()函数会维护一个作用域相关的计数器确保if块内的临时变量名是t1_if1,t2_if1else块内是t1_else1,t2_else1。注意通关代码.sh脚本不是万能钥匙而是压力测试工具。它会遍历test/目录下的所有.sy文件对每个文件执行./driver $file $file.ir然后用diff比对生成的.ir文件与标准答案。如果你的IR生成有细微差异如临时变量名顺序不同、多余空行diff会报错。此时不要盲目修改gen_ir()先用./driver -v test.sy加-v参数开启详细日志查看AST结构和IR生成步骤确认是逻辑错误还是格式问题。4. 实操过程与核心环节实现从零开始复现实验3的AST构建4.1 实验3手把手构建SysY的AST节点体系实验3的目标是实现ast.c中的AST节点创建函数。我们以BinaryExprNode二元表达式节点为例展示如何从零开始构建第一步定义节点结构体在ast.h中添加// 二元操作符枚举 typedef enum { OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_EQ, OP_NE, OP_LT, OP_GT, // ... 其他操作符 } BinaryOp; // 二元表达式节点 typedef struct BinaryExprNode { ASTNode base; // 继承基类含type、pos等通用字段 BinaryOp op; // 操作符类型 ASTNode* left; // 左操作数 ASTNode* right; // 右操作数 } BinaryExprNode;第二步实现节点创建函数在ast.c中BinaryExprNode* new_binary_expr_node(ASTNode* left, ASTNode* right, BinaryOp op) { BinaryExprNode* node (BinaryExprNode*)ast_pool_alloc(sizeof(BinaryExprNode)); init_ast_node(node-base, NODE_BINARY_EXPR, get_current_pos()); // 初始化基类 node-op op; node-left left; node-right right; return node; } // 必须实现基类初始化函数 void init_ast_node(ASTNode* node, NodeType type, Position pos) { node-type type; node-pos pos; node-parent NULL; // 父节点由父节点创建函数设置 }第三步在语法分析器中调用修改sysy.y在additive_expr产生式中additive_expr : multiplicative_expr | additive_expr multiplicative_expr { $$ new_binary_expr_node($1, $3, OP_ADD); } | additive_expr - multiplicative_expr { $$ new_binary_expr_node($1, $3, OP_SUB); }第四步验证节点构建正确性在driver.cpp的main()函数中解析后添加打印逻辑ASTNode* root parse_file(argv[1]); printf(AST Root Type: %s\n, node_type_name(root-type)); if (root-type NODE_BINARY_EXPR) { BinaryExprNode* bin (BinaryExprNode*)root; printf(Binary Op: %s, Left Type: %s, Right Type: %s\n, op_name(bin-op), node_type_name(bin-left-type), node_type_name(bin-right-type)); }运行./driver test_add.sy你会看到类似输出AST Root Type: BINARY_EXPR Binary Op: ADD, Left Type: IDENT_EXPR, Right Type: NUMBER_EXPR这证明AST节点已正确构建。但要注意$1和$3是Bison传递的语义值它们的类型必须与$$匹配。如果multiplicative_expr的语义值类型是ASTNode*而你误写成int编译会报错incompatible types in assignment。4.2 实验6符号表的三层嵌套实现实验6要求实现支持嵌套作用域的符号表。我们用哈希表链表实现三层结构第一层全局符号表Global Symbol Table在symbol_table.h中typedef struct SymbolTable { HashTable* table; // 当前作用域符号哈希表 struct SymbolTable* parent; // 父作用域指针 List* children; // 子作用域链表用于作用域销毁时递归清理 } SymbolTable; extern SymbolTable* global_scope; extern SymbolTable* current_scope; SymbolTable* new_symbol_table(SymbolTable* parent); void enter_scope(SymbolTable* scope); void exit_scope();第二步符号插入与查找在symbol_table.c中Symbol* insert_symbol(SymbolTable* scope, const char* name, SymbolType type, void* data) { // 检查当前作用域是否已存在同名符号 Symbol* existing hash_table_lookup(scope-table, name); if (existing scope current_scope) { error(redefinition of %s, name); // 报错重定义 return NULL; } Symbol* sym create_symbol(name, type, data); hash_table_insert(scope-table, name, sym); return sym; } Symbol* lookup_symbol(const char* name) { SymbolTable* scope current_scope; while (scope ! NULL) { Symbol* sym hash_table_lookup(scope-table, name); if (sym ! NULL) return sym; scope scope-parent; } return NULL; // 未找到 }第三步作用域管理在driver.cpp中解析函数声明时void parse_function_definition() { // 解析函数头后创建新作用域 SymbolTable* func_scope new_symbol_table(current_scope); enter_scope(func_scope); // 解析函数体含参数、局部变量声明 parse_compound_statement(); // 函数体解析完毕退出作用域 exit_scope(); }enter_scope()将current_scope指向新作用域exit_scope()将其还原为父作用域。这样lookup_symbol(x)在函数体内会先查func_scope未找到再查global_scope完美模拟C语言作用域规则。4.3 实验8三地址码生成器的核心算法实验8的IR生成器核心是深度优先遍历AST 指令序列拼接。以IfStmtNode为例void gen_ir_if_stmt(IfStmtNode* node) { // 生成条件表达式的IR结果存入临时变量t_cond char* cond_temp gen_ir_expr(node-cond); // 生成条件跳转指令 fprintf(ir_out, if %s 0 goto L%d\n, cond_temp, next_label); // 生成then分支IR gen_ir_stmt(node-then_body); // 生成跳转到endif的指令 fprintf(ir_out, goto L%d\n, next_label); // 输出then分支结束标签 fprintf(ir_out, L%d:\n, next_label - 1); // 如果有else分支 if (node-else_body) { // 生成else分支IR gen_ir_stmt(node-else_body); } // 输出endif标签 fprintf(ir_out, L%d:\n, next_label); }这里next_label是全局标签计数器确保每个L1,L2唯一。gen_ir_expr()返回临时变量名如t1gen_ir_stmt()递归生成语句IR。关键点是每个IR生成函数只负责自己节点的指令不关心父节点如何使用其结果。这种松耦合设计让IR生成器易于扩展——添加新节点类型只需实现对应的gen_ir_*函数。5. 常见问题与排查技巧实录那些年踩过的坑与独家解法5.1 Flex/Bison环境配置Ubuntu/WSL与macOS的差异陷阱问题现象在Ubuntu 22.04上make报错bison: invalid option -- v或flex: command not found。根本原因Ubuntu默认安装的bison版本过低3.0.4不支持-v选项flex未预装。解决方案# Ubuntu/WSL sudo apt update sudo apt install -y flex bison build-essential # 若bison版本仍低于3.7手动编译安装 wget https://ftp.gnu.org/gnu/bison/bison-3.8.2.tar.xz tar -xf bison-3.8.2.tar.xz cd bison-3.8.2 ./configure --prefix/usr/local make sudo make install sudo ldconfigmacOS问题brew install bison后bison命令在/usr/local/bin/bison但系统PATH可能未包含此路径。排查命令which bison # 应输出 /usr/local/bin/bison echo $PATH # 确认包含 /usr/local/bin # 若未包含添加到 ~/.zshrcexport PATH/usr/local/bin:$PATH独家技巧在Makefile顶部添加环境检测makefile $(shell bison --version | grep -q 3\.[7-9] || (echo ERROR: Bison 3.7 required; exit 1))5.2 AST内存泄漏为什么valgrind报告“definitely lost”问题现象程序运行正常但valgrind --leak-checkfull ./driver test.sy显示大量内存泄漏。原因分析ast_pool_alloc()分配的内存未被释放。ast.c中缺少ast_pool_destroy()函数或driver.cpp未在main()结尾调用。修复方案在ast.c中添加void ast_pool_destroy() { if (ast_pool) { free(ast_pool-buffer); free(ast_pool); ast_pool NULL; } }在driver.cpp的main()末尾添加atexit(ast_pool_destroy); // 程序退出时自动清理更深层问题AST节点间的循环引用。例如FunctionNode包含ParamListParamList节点又指向FunctionNode作为父节点。ast_pool_destroy()无法处理循环引用需在构建时避免或用引用计数。临时解法在ast_pool_init()中分配足够大的缓冲区如1024*1024字节确保一次分配满足所有实验需求避免频繁realloc()。5.3 类型检查失败为什么int* p x;报错“incompatible types”问题现象test_pointer.sy中int* p x;编译报错但x明明是int类型。排查步骤1. 用./driver -v test_pointer.sy查看AST确认x节点的类型是否为int*2. 检查UnaryExprNode操作符的type_check()函数c if (operand_type-kind TYPE_BASIC operand_type-basic TYPE_INT) { result_type new_pointer_type(operand_type); // 正确创建int*类型 } else { error(cannot take address of non-lvalue); // 错误operand_type未正确推导 }3. 关键点操作的对象必须是左值lvalue即具有内存地址的实体。x是变量是左值但x 1是右值不能取地址。type_check_unary_expr()必须先检查操作数是否为左值再推导类型。终极解法在ASTNode结构体中增加is_lvalue标志位在parse_identifier()中为变量节点设is_lvalue true在parse_binary_expr()中为操作的结果设is_lvalue false。5.4 通关脚本失败diff显示“Binary files differ”问题现象通关代码.sh执行后diff报告二进制文件不同但你的IR文件用cat查看与标准答案一致。真相IR文件末尾有多余空行或Windows换行符\r\n。排查命令# 查看文件末尾是否有空行 tail -n 5 test.sy.ir # 查看换行符类型 file test.sy.ir # 输出 test.sy.ir: ASCII text, with CRLF line terminators # 转换为Unix换行符 dos2unix test.sy.ir预防措施在gen_ir()函数末尾添加// 确保IR文件以单个换行符结尾 if (ftell(ir_out) 0) { fseek(ir_out, -1, SEEK_END); int last fgetc(ir_out); if (last ! \n) fprintf(ir_out, \n); }5.5 GDB调试AST如何查看AST节点的完整结构问题在GDB中print *root只显示部分字段无法查看left/right子节点。高效调试法1. 在ast.h中为每个节点类型添加print_ast_node()函数c void print_ast_node(ASTNode* node, int indent); void print_binary_expr_node(BinaryExprNode* node, int indent);2. 在GDB中调用gdb (gdb) call print_ast_node(root, 0)3. 或使用GDB Python脚本自动展开python # ~/.gdbinit python import gdb class ASTPrinter: def __init__(self, val): self.val val def to_string(self): return ASTNode(type%s) % self.val[type] gdb.pretty_printers.append(lambda val: ASTPrinter(val) if str(val.type) ASTNode else None) end最后分享一个小技巧在parser.y的%error-verbose指令后Bison会生成详细的错误信息如“syntax error, unexpected ‘;’, expecting ‘{’ or ‘(‘”。但默认关闭需在%define parse.error verbose。把这个加到sysy.y顶部调试语法错误时会事半功倍。本文还有配套的精品资源点击获取简介一套完整可用的华中科技大学编译原理课程实验代码覆盖词法识别、语法解析、抽象语法树AST构造、符号表管理、类型检查及中间表示生成等核心环节。包含可直接编译运行的PL0编译器源码pl0.c/pl0.h基于Flex/Bison风格的SysY语言词法文件sysy.l和语法定义sysy.y支持递归下降与LL(1)两种解析策略的parser.c和parses.cAST节点定义与构建逻辑ast.c驱动主程序driver.cpp以及配套Makefile一键编译脚本。实验按教学进度组织如实验1完成基础词法扫描实验3实现语法树生成实验6加入语义分析与类型检查实验8输出三地址码等中间表示。所有代码用C/C编写结构清晰、注释充分附带多份README说明文档、SysY语言规范PDFSysY2022语言定义-V1.pdf和通关辅助脚本通关代码.sh适用于课程学习、实验复现或编译器开发入门。本文还有配套的精品资源点击获取

相关新闻