简介这份资源是面向计算机专业学生与编译原理初学者的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; // 记号类别编码如 id36, num35 char sname[9]; // 记号原文标识符最长 8 字符 } Symbol; typedef struct table_item { char name[9]; // 变量名 int address; // 目标地址语义阶段填 int type; // 类型编码integer11, real18, boolean4 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单独分配 37mulop保持 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那段把四种组合都列了intint、realreal、intreal、realint。后两种要调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 Li1、Li:、else 部分、Li1:这几条。while类似先生成Li: if expression.name goto Li1、goto Li2、Li1:、循环体、goto Li、Li2:。标号用一个全局计数器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)分支每个分支单独写转换逻辑写完拿三组测试数据intint、realreal、intreal各跑一遍人工核对四元式顺序。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目录随代码一起提交老师翻报告时能直接对照省去很多口头解释。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?