☰
Pascal子集编译器设计:词法语法语义与四元式生成实战
2026/10/31 7:36:01 网站建设 项目流程

简介:这份资源是面向计算机专业学生与编译原理初学者的Pascal子集编译器课程设计报告,围绕词法分析、语法分析、语义分析与中间代码生成等核心环节展开,帮助读者理解并实践一个用C++手工实现的小型编译器。压缩包内共1个doc文档,约952KB,完整记录了北京邮电大学课程设计的选题背景、需求分析、总体结构与详细设计说明,涵盖词法分析器接口、符号表构建、LL或LR分析方法选择、三地址码与四元式表示、类型检查及参数传递机制等具体内容,并附有团队分工与成绩评定标准。已有240人学习下载,适合需要参考课程设计框架、梳理各阶段设计资料或准备编译原理实验报告的学生,可从中获取模块划分思路、接口定义方式与文档组织范例。

1. 从一份“能跑通”的编译课设说起:Pascal 子集编译器到底要交付什么

很多人做编译原理课设,第一反应是先把词法分析器写出来,跑通几个 token 就以为成功了一半,结果到语法分析阶段发现记号流接口对不上,语义分析又拿不到符号表的行号列号,最后整条链路推倒重来。小型 Pascal 子集编译器的设计报告,本质上不是让你证明“我会写正则匹配”,而是要求你交付一条从源程序到中间代码的完整流水线,并且每个阶段都有可单独验收的产物。这套方案适合正在做编译原理课程设计、需要交完整设计报告和可运行代码的人,也适合想拿一个真实编译器前端项目练手的开发者。它要解决的问题很具体:给定 Sub_P 文法,手工实现词法分析、语法分析、语义分析和中间代码生成,不用 YACC 这类工具,最终能对合法 Pascal 子集程序输出四元式或三地址码,对非法程序给出带位置的错误信息。

2. 总体架构设计:三模块划分与阶段间接口约定

2.1 为什么把词法、语法、语义拆成三个独立模块

编译前端最常见的翻车方式,是把词法分析和语法分析揉在一个大函数里,边读字符边递归下降。这样做在实验课上看似省事,但一旦老师要求“词法分析器能单独测试,输入源文件输出终结符文件”,你就得把代码拆开重写。我一般会按职责切成三个模块:词法分析器负责字符流到记号流的转换,同时维护符号表和常数表;语法分析器只跟记号流打交道,不碰原始字符;语义分析器挂在语法分析树上,做类型检查和中间代码生成。三个模块之间用明确的数据结构通信,而不是靠全局变量到处传。

这样划分的直接好处是验收点清晰。词法阶段可以单独跑,输入一个.pas源文件,输出记号流.txt、符号表.txt、常数表.txt、统计信息.txt四个文件;语法阶段输入记号流,输出分析树和预测分析表;语义阶段输入分析树,输出四元式序列。每个阶段都能独立演示,答辩时老师问“词法怎么证明是对的”,你直接打开终结符文件对照源程序逐行指,比口头解释有说服力得多。

2.2 阶段之间的接口:记号流数组与符号表

词法和语法之间的接口是整个项目里最容易被忽视、又最容易出问题的地方。原文给出的做法是:词法分析器lexout()没有参数和返回值,通过往两个全局数组赋值来传递结果,一个是Symbol_stream[1000]记号流数组,另一个是symbol_table[100]符号表数组。语法分析器调用完lexout()之后,直接读这两个数组。

这个设计在课程设计规模下是可行的,但有几个细节必须提前定死,否则后面联调会非常痛苦。第一,记号流数组里每个元素要同时携带记号类别编码和属性值,类别编码用整数,属性值可以是标识符在符号表中的下标或者常数的值。第二,符号表里每个条目必须记录声明行和引用行,原文的Tuple结构里declare_row和use_row[5]就是干这个的,行号从 1 开始计,列号可以在词法阶段一并记下来。第三,数组容量要留余量,Symbol_stream[1000]对课设规模的源程序够用,但如果测试用例里循环体写得长,记号数可能超,建议在lexout()里加一个越界检查,超了就报“记号流溢出”而不是静默截断。

// 词法与语法之间的核心数据结构,定义在公共头文件里 typedef struct symbol { int atr; // 记号类别编码,如 id=36, num=35 char sname[9]; // 记号原文,标识符最长 8 字符 } Symbol; typedef struct table_item { char name[9]; // 变量名 int address; // 目标地址(语义阶段填) int type; // 类型编码:integer=11, real=18, boolean=4 int demension; // 维数,数组为 1,普通变量为 0 int declare_row; // 声明所在行 int use_row[5]; // 引用行,最多记 5 次 int block; // 块索引号 } Tuple; extern Symbol Symbol_stream[1000]; extern Tuple symbol_table[100]; extern int symbol_count; // 符号表实际条目数

这段结构体定义是整个项目的“合同”。词法分析器往里填,语法分析器按atr字段做分支,语义分析器按name查表拿类型和地址。注意sname长度给 9 是因为 Pascal 标准标识符有效长度通常取 8,多留一位放结束符。use_row只给 5 个槽位是课设的简化,真实编译器会用链表,但报告里要说明这个限制,避免老师追问“引用超过 5 次怎么办”时答不上来。

2.3 模块职责与数据流

把数据流画清楚,报告里的总体设计章节就有骨架了。源程序文件先被读入字符缓冲区buffer,词法分析器逐字符扫描,跳过空白和注释,识别出关键字、标识符、常数和运算符,每识别一个就往Symbol_stream里追加一条,标识符和常数同时写入符号表或常数表。扫描结束后,lexout()把统计信息(行数、单词数、错误数)写到文件里。

语法分析器拿到记号流后,用预测分析法自顶向下建树。原文采用 LL(1) 方法,需要先求 FIRST 集和 FOLLOW 集,再构造预测分析表。分析表是一个二维数组,行是非终结符编码,列是终结符编码,单元格里存产生式编号,-1 表示出错。分析过程中用显式栈代替递归,每步根据栈顶非终结符和当前输入记号查表,把产生式右部逆序压栈,同时把节点信息记到分析树结构里。

语义分析器遍历分析树,按翻译方案执行语义动作。赋值语句检查等号两边类型是否一致,不一致就报类型错误;算术运算遇到 integer 和 real 混合时插入inttoreal转换;函数调用检查实参个数和类型是否匹配形参。每处理完一个语句,就往四元式表里追加一条中间代码。最终四元式表按顺序输出,就是整个编译器的交付物。

提示:三个模块的全局数组要在同一个头文件里声明,用extern引出,避免每个.c文件各自定义一份导致链接时重复符号。这是小组协作里最常见的低级错误,一个人改了结构体字段,另一个人没同步头文件,编译能过但运行结果全错。

3. 词法分析器实现:从字符流到记号流与符号表构建

3.1 关键字识别与内部编码设计

词法分析器第一件事是把 Pascal 子集的关键字表建起来。原文用了一个二维字符数组keyword[N][10]存 24 个关键字,从and到write。识别逻辑是:先按标识符规则读出一个字母开头的字符串,然后拿它去关键字表里比对,命中就返回对应的类别编码,没命中就当普通标识符处理。

内部编码的设计直接影响后面语法分析的查表效率。原文给出的编码表里,关键字and到write占了 1 到 24,运算符和分隔符从 25 开始,(是 25、)是 26、:是 27,一直到.是 32;关系运算符Relop是 33,赋值号assignop是 34,乘性运算符mulop是 35,加性运算符addop是 39,标识符id是 36,整数num是 35。这里有个细节要注意:原文编码表里num和mulop都标了 35,这是笔误还是有意合并,报告里必须说清楚。我的做法是给num单独分配 37,mulop保持 35,避免语法分析时把数字和乘号混淆。

// 关键字识别:返回类别编码,未命中返回 0 表示普通标识符 int Iskeyword(char s[]) { static char keyword[24][10] = { "and","array","begin","boolean","do","else","end","false", "function","if","integer","not","of","or","procedure","program", "read","real","record","then","true","var","while","write" }; for (int i = 0; i < 24; i++) { if (strcmp(s, keyword[i]) == 0) { return i + 1; // 关键字编码 1~24 } } return 0; // 不是关键字,按标识符处理 }

这个函数看着简单,但有两个坑。第一,strcmp区分大小写,Pascal 关键字通常小写,如果测试用例里出现BEGIN,按标准应该报错还是接受,报告里要写明你的选择,我一般按“只接受小写”处理,并在错误信息里提示“关键字大小写不匹配”。第二,关键字表长度写死 24,如果后续要扩展子集加入repeat、until,得同步改循环上界,建议用宏#define KEYWORD_NUM 24统一管理。

3.2 符号表插入与行列号记录

符号表是词法阶段最有价值的产物,也是语义阶段类型检查的依据。原文的Word_insert函数逻辑是:拿识别出的标识符去WordList里查,已存在就返回下标,不存在就追加到末尾并返回 0。这个返回值设计有点绕,更清晰的做法是统一返回下标,用另一个标志位表示是否新建。

行列号的记录要在扫描时同步做。维护一个line_count和一个col_count,每读一个换行符line_count++且col_count归零,每读一个普通字符col_count++。识别出一个标识符时,把当前line_count写进declare_row或use_row。注意声明和引用的区分:在var段里出现的标识符算声明,在语句体里出现的算引用。词法阶段其实分不清这两者,所以原文的做法是先都记成引用行,等语法分析建树后再回填声明行。这个回填逻辑要写在报告里,否则老师会问“词法阶段怎么知道哪行是声明”。

// 标识符插入符号表,返回下标;is_decl 为 1 表示声明,0 表示引用 int Word_insert(char name[], int line, int is_decl) { for (int i = 0; i < symbol_count; i++) { if (strcmp(symbol_table[i].name, name) == 0) { if (!is_decl && symbol_table[i].use_row[4] == 0) { // 找第一个空槽记录引用行 for (int k = 0; k < 5; k++) { if (symbol_table[i].use_row[k] == 0) { symbol_table[i].use_row[k] = line; break; } } } return i; } } // 新标识符 strcpy(symbol_table[symbol_count].name, name); symbol_table[symbol_count].declare_row = is_decl ? line : 0; symbol_table[symbol_count].demension = 0; symbol_table[symbol_count].type = 0; // 类型待语义阶段填 return symbol_count++; }

这段代码里use_row[4] == 0的判断是为了防止引用行槽位写满后越界,写满就丢弃后续引用行,报告里要注明“每个标识符最多记录 5 次引用”。type字段先置 0,等语义分析阶段根据var x: integer这样的声明回填,这样词法和语义的职责就分开了。

3.3 注释过滤与错误恢复

Pascal 子集的注释有两种:花括号{ ... }和(* ... *)。词法分析器遇到{就进入注释状态,一直跳到}才回到正常扫描;遇到(*同理跳到*)。原文用digit_l和digit_r两个计数器记录花括号个数,这是为了处理嵌套注释,但标准 Pascal 不允许嵌套,报告里可以说明“本实现按非嵌套处理,遇到嵌套注释报错”。

错误恢复策略决定词法分析器能不能“继续跑完”。遇到非法字符,比如@或#,不能直接exit(1),而要记录错误行号、错误计数加一,然后跳过该字符继续扫描。原文的error_line[30]数组就是干这个的,最多记 30 个错误行。这样即使源程序里有多个错误,也能一次性全部报出来,而不是改一个跑一次。

// 注释与非法字符处理片段 if (ch == '{') { while ((ch = fgetc(fp)) != '}' && ch != EOF) { if (ch == '\n') line_count++; } if (ch == EOF) { error_count++; error_line[error_count - 1] = line_count; printf("第 %d 行:注释未闭合\n", line_count); } } else if (!IsLetter(&ch, 1) && !IsDigit(ch) && !IsOperator(ch)) { error_count++; if (error_count <= 30) error_line[error_count - 1] = line_count; printf("第 %d 行:非法字符 %c\n", line_count, ch); // 不退出,继续扫描下一个字符 }

这里IsOperator需要你自己补一个判断函数,覆盖+ - * / = < > : ; , . ( ) [ ]这些符号。错误信息里带行号是硬要求,原文的统计信息文件里专门有“错误个数”一项,答辩时老师很可能拿一个故意写错的源程序让你现场跑,看你能不能定位到行。

4. 语法分析器实现:LL(1) 预测分析表与分析树构建

4.1 FIRST 集与 FOLLOW 集的程序化求解

LL(1) 方法的第一步是把文法符号的 FIRST 集和 FOLLOW 集算出来。原文用 C++ 类FirstGroup和FollowGroup封装,每个类里有value、firstgroup[15]、count三个成员,insertFsg负责把子符号的 FIRST 集并入父符号。这个设计的关键是“迭代到不动点”:反复扫描所有产生式,直到某一轮没有任何集合发生变化为止。

FIRST 集的规则原文列了三条,翻译成代码就是:终结符的 FIRST 是它自己;产生式右部第一个符号的 FIRST 去掉 ε 并入左部;如果第一个符号能推出 ε,就看第二个,依此类推。FOLLOW 集规则同样三条:开始符号的 FOLLOW 含$;产生式A -> αBβ中 FIRST(β) 去掉 ε 并入 FOLLOW(B);如果 β 能推出 ε,FOLLOW(A) 并入 FOLLOW(B)。

// FIRST 集求解核心循环,sym 为符号编码 void GetFirstGroup(int sym) { if (is_terminal(sym)) { add_to_first(sym, sym); // 终结符的 FIRST 是自己 return; } for (int p = 0; p < production_count; p++) { if (generator[p][0] != sym) continue; int all_derive_epsilon = 1; for (int i = 1; generator[p][i] != -1; i++) { int y = generator[p][i]; GetFirstGroup(y); // 递归求子符号 FIRST merge_first(sym, y, /*exclude_eps=*/1); if (!has_epsilon(y)) { all_derive_epsilon = 0; break; } } if (all_derive_epsilon) add_to_first(sym, EPSILON); } }

这段递归写法在课设规模下没问题,但如果文法有左递归会死循环。Sub_P 文法本身不含左递归,报告里要明确写“本方案采用 LL(1),已确认文法无左递归、无公共左因子”,这是选型合理性的依据。merge_first的第三个参数控制是否排除 ε,因为并入父符号时 ε 要单独判断,不能直接并进去。

4.2 预测分析表的构造与存储

分析表用二维整数数组analysistable[NT_NUM][T_NUM]存,行是非终结符,列是终结符,单元格存产生式编号,-1 表示出错。构造算法就是原文说的课本算法 4.2:先把全表初始化为 -1,然后逐条扫描产生式A -> α,对 FIRST(α) 里每个终结符 a,把analysistable[A][a]赋成该产生式编号;如果 ε 在 FIRST(α) 里,再对 FOLLOW(A) 里每个 b,把analysistable[A][b]也赋成该编号。

// 构造预测分析表 void CreateAnalysisTable() { for (int i = 0; i < NT_NUM; i++) for (int j = 0; j < T_NUM; j++) analysistable[i][j] = -1; // 初始化为出错 for (int p = 0; p < production_count; p++) { int A = generator[p][0]; // 求右部 FIRST 集 int first_set[15], fc = 0; get_rhs_first(p, first_set, &fc); for (int k = 0; k < fc; k++) { if (first_set[k] != EPSILON) analysistable[A][first_set[k]] = p; } if (contains(first_set, fc, EPSILON)) { // ε 在 FIRST 中,用 FOLLOW(A) 填表 for (int k = 0; k < follow_count[A]; k++) analysistable[A][followgroup[A][k]] = p; } } }

填表时如果同一个单元格被赋了两次不同的产生式编号,说明文法有冲突,不是严格的 LL(1)。报告里要加一段冲突检测:填表前先判断analysistable[A][a]是否已经是 -1,不是就打印“冲突:非终结符 X 遇终结符 Y 有多条产生式可选”,这样能提前暴露文法问题,而不是等到分析时莫名其妙走错分支。

4.3 分析树输出与源程序变更验证

语法分析跑完后要能直观输出分析树,这是原文的硬性要求。我的做法是在分析过程中维护一个树节点数组,每个节点存symbol(符号编码)、parent(父节点下标)、child[10](子节点下标数组)和child_count。每展开一个非终结符,就为产生式右部每个符号建一个子节点,挂到当前节点下。分析结束后从根节点递归打印,用缩进表示层级。

// 分析树节点结构与打印 typedef struct tree_node { int symbol; int parent; int child[10]; int child_count; } TreeNode; TreeNode tree[2000]; int tree_count = 0; void print_tree(int idx, int depth) { for (int i = 0; i < depth; i++) printf(" "); printf("%s\n", symbol_name(tree[idx].symbol)); for (int i = 0; i < tree[idx].child_count; i++) print_tree(tree[idx].child[i], depth + 1); }

验证“改变源程序,分析树将发生变化”这个要求,最直接的办法是准备两个只差一个运算符的源程序,比如一个写a := b + c,另一个写a := b * c,分别跑一遍,把两棵树的文本输出并排贴进报告。老师一看就知道你的分析树是真建出来的,不是硬编码打印的。

注意:分析树节点数组容量给 2000 是经验值,Sub_P 文法下一个中等规模的源程序节点数在几百到一千之间。如果测试用例里嵌套if和while特别多,可能超,建议在new_node()里加越界断言,超了直接报“分析树节点溢出”,比数组越界写坏内存好排查。

5. 语义分析与中间代码生成:类型检查、翻译方案与四元式输出

5.1 符号表的栈式组织与类型计算

语义阶段第一件事是把词法阶段建好的符号表补全。原文用SqStack栈结构组织符号表,mktable()新建一张表,enter()往里插变量,lookahead(k)查节点 k 的最左子节点。栈式组织是为了支持过程嵌套,但原文明确“不包含嵌套过程”,所以实际用一层表就够,栈结构更多是形式上的完整。

类型计算的核心在type0函数:遇到integer返回类型码 11、宽度 4;遇到real返回类型码 18、宽度 8;遇到array [d1..d2] of standard_type,先检查d1和d2是否都是整数,是的话类型记为数组,宽度等于(d2 - d1 + 1) * 元素宽度。这个宽度计算后面生成中间代码时要用,数组元素寻址靠它算偏移。

// 类型计算:返回数组,[0] 存类型码,[1] 存宽度 int* type0(int k) { static int result[2]; int sym = tree[k].symbol; if (sym == T_INTEGER) { result[0] = TYPE_INTEGER; result[1] = 4; } else if (sym == T_REAL) { result[0] = TYPE_REAL; result[1] = 8; } else if (sym == T_ARRAY) { int d1 = atoi(node_text(tree[k].child[2])); int d2 = atoi(node_text(tree[k].child[4])); int elem_type = type0(tree[k].child[6])[0]; int elem_width = type0(tree[k].child[6])[1]; result[0] = TYPE_ARRAY; result[1] = (d2 - d1 + 1) * elem_width; } return result; }

这里node_text是从分析树节点取原文的辅助函数,child[2]和child[4]对应array [ digits1 .. digits2 ]里的两个数字位置,具体下标取决于你的分析树建法,报告里要附一张“文法符号属性表”,把每个非终结符对应哪些属性、属性从哪个子节点取写清楚,这是原文明确要求的交付物。

5.2 类型检查与隐式类型转换

类型检查分三类场景。赋值语句variable := expression,要求variable的类型和expression的类型一致,不一致就报错;但 Pascal 允许 integer 赋给 real,这时要插入inttoreal转换。条件语句if expression then,要求expression是 boolean 型,否则报“条件表达式类型错误”。算术运算a + b,如果两边都是 integer 结果就是 integer,都是 real 结果就是 real,一整一实就把整型转成实型再算。

原文的翻译方案里simple_expression'那段把四种组合都列了:int+int、real+real、int+real、real+int。后两种要调newtemp生成临时变量,先emit(u ':= ' 'inttoreal' 操作数),再emit(结果 ':= ' u 'real' 运算符 另一操作数)。这个临时变量分配策略是语义阶段的核心,报告里要单独一节讲newtemp怎么实现——通常就是一个全局计数器,每次调用返回t1、t2这样的名字。

// 生成新临时变量名 int temp_count = 0; char* newtemp() { static char buf[10]; sprintf(buf, "t%d", ++temp_count); return buf; } // 二元算术运算的类型处理,op 为运算符原文 char* gen_arith(char* a, int ta, char* op, char* b, int tb) { if (ta == TYPE_INTEGER && tb == TYPE_INTEGER) { char* t = newtemp(); emit(t, op, a, b, "-"); // 四元式:(op, a, b, t) return t; } else if (ta == TYPE_REAL && tb == TYPE_REAL) { char* t = newtemp(); emit(t, op, a, b, "-"); return t; } else { // 混合类型,先转 real char* u = newtemp(); char* int_op = (ta == TYPE_INTEGER) ? a : b; char* real_op = (ta == TYPE_INTEGER) ? b : a; emit(u, "inttoreal", int_op, "-", "-"); char* t = newtemp(); emit(t, op, u, real_op, "-"); return t; } }

emit函数把一条四元式追加到全局四元式表里,参数顺序是(结果, 运算符, 操作数1, 操作数2, 备用)。注意混合类型时哪个操作数转 real 要看类型,不能写死,上面代码用ta == TYPE_INTEGER判断,报告里要把这个分支逻辑用表格列出来,老师对照表格就能验证你的类型转换规则对不对。

5.3 控制流语句的四元式生成

if和while的四元式生成靠标号回填。原文翻译方案里if expression then statement else statement生成if expression.name goto Li、goto Li+1、Li:、else 部分、Li+1:这几条。while类似,先生成Li: if expression.name goto Li+1、goto Li+2、Li+1:、循环体、goto Li、Li+2:。

标号用一个全局计数器label_count管理,每次需要新标号就L(++label_count)。这里容易翻车的是标号顺序:if语句里then分支的入口标号和else分支的入口标号必须严格按翻译方案来,写反了程序逻辑就错了。我一般会在生成完一个控制流语句后,把四元式序列打印出来人工走一遍,确认跳转目标都落在正确位置。

// if 语句的四元式生成片段 void gen_if(int expr_node, int then_node, int else_node) { char* cond = expression(expr_node); // 递归生成条件表达式 int L1 = ++label_count; int L2 = ++label_count; emit("-", "if", cond, "-", label_name(L1)); // if cond goto L1 emit("-", "goto", "-", "-", label_name(L2)); // goto L2 emit(label_name(L1), ":", "-", "-", "-"); // L1: statement(then_node); // then 分支 if (else_node != -1) { int L3 = ++label_count; emit("-", "goto", "-", "-", label_name(L3)); emit(label_name(L2), ":", "-", "-", "-"); statement(else_node); emit(label_name(L3), ":", "-", "-", "-"); } else { emit(label_name(L2), ":", "-", "-", "-"); } }

这段代码里emit的第一个参数是结果字段,控制流四元式没有结果,统一填"-"。label_name把整数标号转成L1、L2这样的字符串。注意else分支存在时要多分配一个L3做出口汇合,不存在时L2直接就是出口。这个细节在报告里用一张“控制流语句四元式模板表”列出来,比大段文字描述清楚。

6. 避坑与排查:编译课设里最容易翻车的五个点

6.1 记号流数组越界导致语法分析读到脏数据

现象是语法分析跑到一半突然报“意外的记号”,但源程序本身没问题。原因多半是词法阶段Symbol_stream写超了容量,越界写坏了相邻内存,或者symbol_count没同步更新,语法分析读到了未初始化的槽位。解决办法是在lexout()每次追加记号前检查stream_count < 1000,超了就报“记号流溢出”并停止扫描;同时在语法分析入口处断言stream_count与symbol_count一致,不一致直接打印两个值方便定位。

6.2 FIRST/FOLLOW 集迭代不收敛导致分析表大面积空缺

现象是预测分析表里很多单元格还是 -1,合法程序跑到某个非终结符就报错。原因是 FIRST 集求解时递归没有处理“子符号 FIRST 还没算完”的情况,或者 FOLLOW 集传播时漏了A -> αB这种 β 为空的情形。解决办法是把 FIRST 和 FOLLOW 的求解都改成“外层 while 循环 + changed 标志”,每轮扫描所有产生式,只要有一轮没有任何集合新增元素就退出。调试时把每个非终结符的 FIRST 和 FOLLOW 打印出来,对照课本例题手工验算几个,能快速发现漏算。

6.3 类型转换插入位置错误导致四元式语义颠倒

现象是a := b + c里b是 integer、c是 real,生成的四元式变成先算b + c再转 real,结果精度丢了。原因是gen_arith里判断哪个操作数需要转换时写反了条件。解决办法是把四种类型组合列成真值表,代码里用switch (ta * 10 + tb)分支,每个分支单独写转换逻辑,写完拿三组测试数据(int+int、real+real、int+real)各跑一遍,人工核对四元式顺序。

6.4 符号表声明行与引用行混淆导致错误定位不准

现象是报告里说“第 5 行变量未声明”,但源程序第 5 行明明是var x: integer。原因是词法阶段把所有标识符都记成了引用行,语义阶段回填声明行时覆盖了原有记录。解决办法是在Tuple里把declare_row和use_row严格分开,词法阶段只填use_row,语义阶段遇到var声明时再填declare_row,两边互不覆盖。查表报错时优先看declare_row是否为 0,为 0 才报“未声明”。

6.5 分析树节点父子关系挂错导致语义动作取错子节点

现象是语义分析遍历分析树时,type0取数组上下界取到了别的符号,宽度算出来是负数。原因是建树时子节点挂载顺序和文法右部符号顺序不一致,比如把array后面的[也建了节点,导致child[2]偏移。解决办法是建树时只给“有语义动作的文法符号”建节点,终结符如[、]、..不建节点,或者建了但在type0里按符号编码查找而不是按固定下标取。报告里附一张“分析树节点与文法符号对应表”,把每个child[i]对应哪个符号写死,后面改代码有据可查。

7. 验收与答辩:怎么用一份测试用例集证明编译器真的能跑

验收这件事,老师不会只看你演示一个“Hello World”级别的源程序。我一般会准备一套分层测试用例,按难度分三组,每组三到五个文件,答辩时按顺序跑,让老师看到边界处理能力。第一组是合法程序,覆盖赋值、算术运算、if-else、while循环、数组访问、函数调用,每个文件只测一个特性,输出对应的四元式序列,人工核对跳转标号是否闭合。第二组是词法错误程序,故意放非法字符、未闭合注释、超长标识符,看错误信息能不能定位到行。第三组是语法和语义错误程序,比如缺分号、类型不匹配、数组下标越界,看报错信息是否具体到“第几行什么错误”。

答辩时最容易被问的三个问题,提前准备好答案。第一个是“为什么选 LL(1) 不选 LR”,回答要点是 Sub_P 文法规模小、无左递归,LL(1) 手工实现分析表直观,调试时能直接看栈内容,LR 的分析栈和动作表构造对课设来说复杂度偏高。第二个是“符号表为什么用数组不用哈希”,回答课设规模下标识符数量在百级,线性查找足够快,数组结构简单、方便按声明顺序输出,哈希的冲突处理反而增加调试成本。第三个是“中间代码为什么选四元式不选三地址码”,回答四元式字段固定、打印对齐好看、后续如果要做目标代码生成,四元式到汇编的映射更直接。

验收表格可以这样设计,放在报告最后一节,答辩时直接投屏:

测试编号源程序特性预期输出实际结果是否通过
T01整数赋值与加法四元式 3 条,无错误一致是
T02混合类型算术含 inttoreal 转换一致是
T03if-else 嵌套标号 L1~L4 闭合一致是
T04while 循环回跳标号正确一致是
T05数组声明与访问宽度计算正确一致是
T06非法字符 @报第 N 行错误一致是
T07缺分号报语法错误行号一致是
T08类型不匹配赋值报类型错误一致是

这张表填满,答辩时老师随便挑一行让你现场跑,你都能对上,基本就稳了。血泪经验是:测试用例一定要提前一天全部跑一遍并截图存档,现场环境可能因为路径、编码问题跑不出预期结果,有截图至少能证明你跑通过。从那以后我每次交课程设计,都会把测试用例、运行截图、四元式输出文件打包成一个test目录随代码一起提交,老师翻报告时能直接对照,省去很多口头解释。希望帮到你。

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

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

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

立即咨询