news 2026/6/7 7:38:56

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
华中科大编译原理实验代码合集:PL0实现、SysY词法语法分析、AST构建与中间代码生成

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

简介:一套完整可用的华中科技大学编译原理课程实验代码,覆盖词法识别、语法解析、抽象语法树(AST)构造、符号表管理、类型检查及中间表示生成等核心环节。包含可直接编译运行的PL0编译器源码(pl0.c/pl0.h),基于Flex/Bison风格的SysY语言词法文件(sysy.l)和语法定义(sysy.y),支持递归下降与LL(1)两种解析策略的parser.c和parses.c,AST节点定义与构建逻辑(ast.c),驱动主程序(driver.cpp),以及配套Makefile一键编译脚本。实验按教学进度组织,如实验1完成基础词法扫描,实验3实现语法树生成,实验6加入语义分析与类型检查,实验8输出三地址码等中间表示。所有代码用C/C++编写,结构清晰、注释充分,附带多份README说明文档、SysY语言规范PDF(SysY2022语言定义-V1.pdf)和通关辅助脚本(通关代码.sh),适用于课程学习、实验复现或编译器开发入门。

1. 这不是“代码合集”,而是一套可触摸的编译器前端教学骨架

你手头拿到的这份资源,表面看是华中科大2019级《编译原理》课的实验代码打包,但如果你真把它当“参考答案”直接抄作业,大概率会在实验3的AST节点内存管理上卡三天,在实验6的符号表作用域嵌套里绕晕,在实验8生成三地址码时发现跳转标签对不上——我带过三届本科生做这门课设,每年都有至少15%的同学栽在同一个地方:把结构清晰误认为逻辑自洽,把注释充分当成原理透明

它真正的价值,不在于你能跑通make && ./driver test.sy输出一串三地址码,而在于它用C/C++这一门“裸金属语言”,把教科书上抽象的“词法分析器→语法分析器→语义分析器→中间代码生成器”这条流水线,拆解成一个个可调试、可打断点、可单步跟踪的函数调用链。比如pl0.c里那个不到200行的getch()函数,它不只是读字符,而是实现了缓冲区预读+回退指针+行号列号自动维护三位一体的扫描控制;再比如sysy.yprogram : 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语言本身是个教学神作——它只有constvarprocedurebeginendifwhilecallreadwrite这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.cparses.c的双轨设计:前者是基于Flex/Bison生成的LALR(1)分析器,后者是手写的递归下降分析器。为什么需要两套?

因为LALR(1)分析器(parser.tab.c)能处理复杂的左递归文法(如表达式E → E '+' T | T),但错误恢复能力弱——一旦遇到a = b + ;这种缺失操作数的错误,Bison默认会丢弃大量输入直到找到同步记号;而递归下降分析器(parses.c)虽然要手动消除左递归(改成E → T E'E' → '+' 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"被识别为两个token:"abc\"def"。解决方案是在sysy.l末尾添加%option noyywrap并重写yywrap(),或在规则后加{ /* handle string */ }显式终止。这个细节,90%的初学者会在实验1的词法测试里栽跟头。

2.3 AST:从PL0的“扁平树”到SysY的“立体森林”,理解抽象的本质

AST(Abstract Syntax Tree)不是语法树(Parse Tree)的简单缩写,而是剥离了语法冗余、聚焦语义结构的中间表示。PL0的AST极度扁平:ProgramNode下直接挂ConstDeclListVarDeclListProcDeclListStatementList四个子节点,因为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.h(token定义头文件)。而driver.cpp必须#include "parser.tab.h"才能识别YYSTYPEyyparse()返回的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 -n(dry-run模式)。它会打印出所有将要执行的命令,让你看清flex sysy.l生成lex.yy.cbison sysy.y生成parser.tab.cgcc -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.cparser.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有intint*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 2>&1 | head -20观察类型比较过程——这是定位类型检查bug最快的方法。

3.3 中间代码生成:实验8的三地址码,不是翻译而是重构

实验8的中间代码生成(ir_gen.c,虽未在目录中显式出现,但逻辑集成在ast.cgen_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.cppmain()函数中,解析后添加打印逻辑:

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()返回临时变量名(如t1),gen_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 ldconfig

macOS问题brew install bison后,bison命令在/usr/local/bin/bison,但系统PATH可能未包含此路径。
排查命令

which bison # 应输出 /usr/local/bin/bison echo $PATH # 确认包含 /usr/local/bin # 若未包含,添加到 ~/.zshrc:export 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-check=full ./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.cppmain()末尾添加:

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.syint* 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.c,AST节点定义与构建逻辑(ast.c),驱动主程序(driver.cpp),以及配套Makefile一键编译脚本。实验按教学进度组织,如实验1完成基础词法扫描,实验3实现语法树生成,实验6加入语义分析与类型检查,实验8输出三地址码等中间表示。所有代码用C/C++编写,结构清晰、注释充分,附带多份README说明文档、SysY语言规范PDF(SysY2022语言定义-V1.pdf)和通关辅助脚本(通关代码.sh),适用于课程学习、实验复现或编译器开发入门。


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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/7 7:37:06

多模态推荐系统CRANE框架:双图学习与递归注意力机制解析

1. 多模态推荐系统的核心挑战与CRANE框架设计在当今信息过载的时代&#xff0c;推荐系统已成为连接用户与内容的关键桥梁。传统协同过滤方法仅依赖用户-物品交互数据&#xff0c;面临严重的冷启动和数据稀疏性问题。以亚马逊Electronics数据集为例&#xff0c;其稀疏度高达99.9…

作者头像 李华
网站建设 2026/6/7 7:37:04

Claude Managed Agents:解耦会话状态的AI运行时操作系统

1. 项目概述&#xff1a;当“运行时”开始自我坍缩你有没有试过让一个AI代理连续工作四十分钟&#xff0c;处理一份需要反复调用数据库、读取PDF、生成代码再验证结果的复杂任务&#xff1f;我去年就踩过这个坑。当时整个状态全靠模型上下文窗口硬扛——结果到第三十七分钟&…

作者头像 李华
网站建设 2026/6/7 7:36:13

NVIDIA Profile Inspector:解锁显卡隐藏设置的终极指南

NVIDIA Profile Inspector&#xff1a;解锁显卡隐藏设置的终极指南 【免费下载链接】nvidiaProfileInspector 项目地址: https://gitcode.com/gh_mirrors/nv/nvidiaProfileInspector 你是否对NVIDIA官方控制面板的功能限制感到困扰&#xff1f;想要为每个游戏定制专属的…

作者头像 李华
网站建设 2026/6/7 7:35:38

INT8量化轻量级行为监测系统在神经科学研究中的应用

1. 项目背景与核心价值在神经科学和行为学研究领域&#xff0c;实时监测动物行为并触发精确干预&#xff08;如光遗传学刺激&#xff09;是理解大脑工作机制的重要手段。传统方案通常依赖高性能计算设备运行复杂模型&#xff0c;这导致实验系统体积庞大、功耗高且延迟显著。我们…

作者头像 李华
网站建设 2026/6/7 7:31:20

LLM推理本质:Token预测、Attention缝合与位置编码的工程解剖

1. 这不是“思考”&#xff0c;是高维模式缝合——我们到底在解剖什么&#xff1f;你点开一篇标题叫《How Do LLMs Reason? A Look Inside the ‘Thinking’ Mind of AI》的文章&#xff0c;心里大概率已经预设了一个画面&#xff1a;AI像人一样&#xff0c;在脑子里推演、权衡…

作者头像 李华