资讯详情

广工编译原理实验:PL/0扩展ELSE语句的避坑指南

📅 2026/10/10 15:38:29 | 华诺云谱 👁 阅读
广工编译原理实验:PL/0扩展ELSE语句的避坑指南
简介这份资源面向计算机专业学习编译原理的学生围绕教学型PL/0编译程序展开词法、语法与语义分析的修改扩充实验。核心任务包括新增ELSE、FOR、TO、DOWNTO、RETURN等保留字及、-、、--运算符将不等号#改为并为条件语句补充ELSE子句帮助读者在动手改造中理解编译过程的基本原理与实现方法。压缩包共24个文件约644KB以cpp与h源码、dsp与dsw工程文件、doc实验报告为主另含exe、obj、pdb等编译产物及若干临时文件可直接在开发环境中打开调试。目前已有789人学习下载适合作为课程实验的参考实现与排错对照读者可据此完成词法表扩充、语法规则调整与语义动作修改并借助实验报告梳理整体设计思路。1. 广工编译原理实验从 PL/0 扩展 ELSE 语句我踩过的那些坑如果你正在做广工编译原理实验大概率已经拿到了那个经典的 PL/0 编译器源码包。它不大几个 C 文件词法、语法、语义分析全在里面但真正动手改的时候你会发现——保留字表里没有 ELSE语法分析里没有对应的产生式代码生成阶段更没有现成的分支跳转模板。这个实验的核心任务就是在 PL/0 基础上扩展 ELSE 语句让if 条件 then 语句 else 语句能正确编译执行。听起来只是加一个关键字实际上你要同时改词法、语法、语义和代码生成四个模块任何一个环节没对齐编译器就会在某个阶段静默出错。这份资源适合正在上编译原理课、需要交实验报告的同学也适合想通过一个完整小编译器理解编译全流程的开发者。下面我按实际改代码的顺序把每一步拆开讲。2. 词法分析把 ELSE 塞进保留字表只是第一步2.1 保留字表的修改位置与原理PL/0 的词法分析器通常用一个word数组或枚举来存保留字。你需要在里面加上else同时确保它的序号和后续语法分析里的判断一致。常见做法是定义一个SYM_ELSE常量然后在保留字表里把字符串else映射到这个常量。// 保留字表定义通常放在 lexer.h 或类似头文件里 #define SYM_IF 1 #define SYM_THEN 2 #define SYM_ELSE 3 // 新增 #define SYM_WHILE 4 // ... 其他保留字 // 保留字字符串数组顺序必须与上面的常量对应 char *reserved_words[] { , if, then, else, while, // 索引 0 留空 // ... 后续保留字 };这里有个容易翻车的地方很多同学只改了字符串数组忘了同步改常量定义或者改了常量但数组顺序没对齐。结果就是else被识别成了普通标识符语法分析阶段直接报“意外的标识符”。我一般会建议在改完之后先单独跑一个词法测试输入if a then b else c看输出的 token 序列里else是不是SYM_ELSE。2.2 词法分析的验证方法改完保留字表后不要急着往下走。先写一个最小的测试用例只调用词法分析函数把 token 流打印出来。PL/0 的词法分析通常有一个getsym()函数每次调用返回一个符号。你可以临时在main里加一段循环// 临时测试代码验证 else 是否被正确识别 void test_lexer() { char *input if x 0 then y : 1 else y : 2; // 假设有一个初始化词法分析器的函数 init_lexer(input); int sym; while ((sym getsym()) ! SYM_EOF) { printf(sym: %d, value: %s\n, sym, get_token_string(sym)); } }运行后如果看到else对应的sym是SYM_ELSE说明词法阶段没问题。如果还是普通标识符的编号回去检查保留字表的遍历逻辑——有些实现是线性查找有些是二分查找二分查找要求数组有序加else的时候位置放错也会导致查不到。提示词法阶段改完一定要单独验证不要等到语法分析报错再回头查那样你会同时面对两个模块的问题排查成本翻倍。3. 语法分析递归下降里加 ELSE 分支注意 FOLLOW 集3.1 修改 IF 语句的产生式PL/0 的语法分析通常是递归下降。原来的 IF 语句处理逻辑大概是匹配if解析条件表达式匹配then解析 then 分支的语句。现在要扩展成匹配then之后解析语句然后看下一个 token 是不是else如果是就继续解析 else 分支。// 语法分析中 IF 语句的处理函数 void parse_if() { match(SYM_IF); parse_condition(); // 解析条件表达式 match(SYM_THEN); parse_statement(); // 解析 then 分支 // 新增检查是否有 else if (current_sym SYM_ELSE) { match(SYM_ELSE); parse_statement(); // 解析 else 分支 } }这段代码看起来简单但有一个隐藏的坑parse_statement()在解析完 then 分支后可能会消耗掉后面的 token。如果 then 分支是一个复合语句比如begin ... end那end之后的下一个 token 才是else。但如果 then 分支是一个简单语句比如赋值那赋值语句解析完当前 token 就已经是else了。所以current_sym的更新时机很关键——必须在parse_statement()返回后立即检查不能提前读下一个 token。3.2 处理悬挂 ELSE 的歧义编译原理课上一定讲过“悬挂 ELSE”问题if a then if b then s1 else s2到底 else 匹配哪个 ifPL/0 的递归下降实现天然是“就近匹配”也就是 else 匹配最近的未匹配 if。这符合大多数语言的设计但你在改代码的时候要确认自己的实现没有破坏这个规则。// 错误的做法在 parse_if 开头就预读下一个 token void parse_if_wrong() { match(SYM_IF); parse_condition(); match(SYM_THEN); int next getsym(); // 提前读可能把 else 吞掉 // ... 后续逻辑混乱 }我见过不少同学为了“提前知道有没有 else”在解析 then 分支之前就调用了getsym()结果 then 分支的解析函数拿到的 token 已经不对了。正确的做法是让parse_statement()正常消耗 token返回后当前 token 自然就是 else 或者别的。如果你不确定当前 token 的状态可以在parse_if里加一句打印观察current_sym的值。3.3 语法分析的测试用例设计改完语法分析后用几个典型用例覆盖所有分支测试输入预期行为if a 0 then b : 1无 else正常解析if a 0 then b : 1 else b : 2有 else正常解析if a 0 then if b 0 then c : 1 else c : 2else 匹配内层 ifif a 0 then begin b : 1; c : 2 end else b : 3then 分支为复合语句每个用例跑一遍看语法分析是否报错。如果报“意外的 else”说明parse_statement()之后没有正确检查SYM_ELSE如果报“意外的标识符”可能是词法阶段的问题还没解决。4. 语义分析与代码生成ELSE 跳转指令的偏移量计算4.1 中间代码生成的基本思路PL/0 通常生成类似 P-code 的中间代码指令格式是操作码 层差 偏移量。IF 语句的代码生成逻辑是计算条件条件不成立时跳转到 then 分支之后。加上 ELSE 之后then 分支执行完要跳过 else 分支所以需要两条跳转指令。// 代码生成IF-THEN-ELSE 的指令序列 // 假设条件不成立时跳转到 else 分支 int jmp_false gen_jmp_false(); // 条件为假时跳转回填地址待定 parse_statement(); // 生成 then 分支代码 int jmp_end gen_jmp(); // then 分支结束后跳过 else回填待定 backpatch(jmp_false, current_addr); // 条件为假跳到这里即 else 开始 if (has_else) { parse_statement(); // 生成 else 分支代码 } backpatch(jmp_end, current_addr); // then 分支跳过 else到这里结束这里的关键是backpatch——因为生成跳转指令的时候目标地址还不知道需要等代码生成到那个位置再回填。很多同学在这里翻车是因为回填的地址算错了导致程序跳到了错误的指令位置运行时表现为死循环或者段错误。4.2 偏移量计算的常见错误PL/0 的跳转指令偏移量通常是相对于当前指令的下一条指令来算的。如果你用绝对地址回填而解释器按相对地址执行就会跳飞。我一般会在回填函数里加一句断言确保目标地址在当前指令之后void backpatch(int instr_index, int target_addr) { if (target_addr instr_index) { fprintf(stderr, backpatch error: target %d instr %d\n, target_addr, instr_index); exit(1); } code[instr_index].offset target_addr - instr_index - 1; }这个-1是因为 PL/0 的解释器在执行跳转时通常会把程序计数器先加一然后再加偏移量。如果你不确定自己的解释器怎么算就手动构造一个最简单的if ... else ...把生成的指令序列打印出来单步跟踪解释器的执行过程。4.3 语义检查ELSE 分支的类型一致性虽然 PL/0 是弱类型语言但 then 和 else 分支的语句类型最好保持一致。比如 then 分支是赋值语句else 分支也是赋值语句这样生成的代码在栈平衡上不会出问题。如果 then 分支是begin ... endelse 分支是单条语句要注意栈指针的恢复。// 语义检查记录 then 分支和 else 分支的栈深度变化 int stack_before current_stack_depth; parse_statement(); // then 分支 int stack_after_then current_stack_depth; if (has_else) { parse_statement(); // else 分支 int stack_after_else current_stack_depth; if (stack_after_then ! stack_after_else) { fprintf(stderr, warning: then/else stack depth mismatch\n); } }这个检查不是必须的但加上之后能帮你提前发现一些隐蔽的栈不平衡问题。尤其是当 then 分支里有嵌套的 IF 语句时栈深度的变化容易算错。5. 避坑与排查ELSE 扩展实验中的五个血泪教训5.1 现象编译通过但运行结果不对原因跳转指令的偏移量回填错误then 分支执行完没有正确跳过 else 分支导致 else 分支被重复执行。解决在解释器里加指令跟踪打印每条指令的执行地址和操作码。对比if ... else ...的预期执行路径看跳转目标是不是落在了 else 分支的第一条指令上。如果偏移量差 1检查回填时有没有减掉当前指令占用的位置。5.2 现象嵌套 IF 时 ELSE 匹配错层原因递归下降实现中parse_statement()返回后没有立即检查SYM_ELSE而是继续解析了其他内容导致 else 被外层的 IF 捕获。解决确保parse_if的结构是“解析 then 分支 → 立即检查 else → 解析 else 分支”中间不要插入其他 token 读取操作。可以在检查 else 之前加一句assert(current_sym SYM_ELSE || current_sym SYM_EOF || ...)来验证状态。5.3 现象词法分析把 ELSE 识别成标识符原因保留字表的遍历顺序或比较逻辑有问题。比如用了二分查找但数组没排序或者字符串比较时大小写敏感但输入是大写。解决打印保留字表的实际内容和查找过程。如果是线性查找确认else在数组里如果是二分查找确认数组按字典序排列。另外检查输入是否被转成了小写PL/0 通常要求关键字小写。5.4 现象代码生成阶段报“指令数组越界”原因PL/0 的代码数组通常有固定大小比如 200 条指令加上 ELSE 分支后指令数增加如果测试用例嵌套层数多可能超出数组容量。解决把代码数组的容量调大比如从 200 改成 500。同时检查gen函数里有没有边界检查没有的话加上避免写越界导致段错误。5.5 现象ELSE 分支的变量作用域出错原因PL/0 的变量作用域是按层差管理的then 分支和 else 分支如果声明了同名变量或者引用了外层变量层差计算可能不一致。解决在语义分析阶段记录每个分支的层差确保 then 和 else 在相同的层差下生成代码。如果 PL/0 不支持分支内声明变量那就检查变量引用时的层差查找逻辑确保 else 分支能正确找到外层变量。注意以上五个问题里第 1 个和第 2 个出现的频率最高。我建议改完代码后先跑一个最简单的if a 0 then b : 1 else b : 2确认基本功能没问题再逐步增加嵌套和复合语句的测试用例。6. 进阶技巧用指令跟踪验证 ELSE 代码生成的正确性6.1 给解释器加一个跟踪开关PL/0 的解释器通常是一个大循环从代码数组里取指令执行。你可以在循环开头加一个条件打印把当前指令的地址、操作码、层差、偏移量都输出出来。用一个全局变量trace_on控制默认关闭需要的时候在main里打开。// 解释器主循环中的跟踪代码 while (pc code_length) { instruction instr code[pc]; if (trace_on) { printf(pc%3d op%2d l%2d a%3d | stack_top%d\n, pc, instr.op, instr.level, instr.offset, stack_top); } pc; switch (instr.op) { case OP_JMP: pc instr.offset; break; case OP_JPC: if (!pop()) pc instr.offset; break; // ... 其他指令 } }运行if a 0 then b : 1 else b : 2观察指令序列。预期是计算a 0条件为假时跳转到 else 分支的第一条指令then 分支结束后跳转到整个 IF 语句之后。如果跳转目标不对跟踪输出会直接告诉你跳到了哪条指令。6.2 手工构造指令序列做单元测试除了跑完整程序你还可以直接构造一段指令数组绕过词法和语法分析单独测试解释器的跳转逻辑。比如// 手工构造的指令序列模拟 if ... else ... 的执行 instruction test_code[] { {OP_LIT, 0, 1}, // 压入 1条件为真 {OP_JPC, 0, 4}, // 条件为假跳转到地址 4 {OP_LIT, 0, 10}, // then 分支压入 10 {OP_JMP, 0, 6}, // 跳过 else跳转到地址 6 {OP_LIT, 0, 20}, // else 分支压入 20 {OP_STO, 0, 0}, // 存储到变量 0 {OP_RET, 0, 0} // 返回 };把这段指令加载到解释器里执行看变量 0 最终是 10 还是 20。如果条件为真时结果是 10说明 then 分支和跳转逻辑正确如果结果是 20说明OP_JMP的偏移量算错了then 分支执行完没有跳过 else。6.3 用对比法定位偏移量错误如果你不确定偏移量该怎么算可以先用一个已知正确的 IF 语句不带 else生成指令观察它的跳转偏移量。然后加上 else对比指令序列的变化。通常 else 的加入只会增加一条OP_JMP指令并调整两个跳转目标。把两次生成的指令序列并排打印差异点就是你需要重点检查的地方。# 假设你的编译器可以输出指令序列到文件 ./pl0c test_if_no_else.pl0 no_else.txt ./pl0c test_if_else.pl0 with_else.txt diff no_else.txt with_else.txt这个对比法我每次改跳转逻辑的时候都会用比盯着代码空想快得多。尤其是当嵌套层数多的时候手工算偏移量几乎必错让编译器自己输出指令序列然后对比差异是最可靠的验证方式。从那以后我每次改编译器的跳转逻辑都会先手工构造一段最小指令序列跑通解释器再回头改代码生成。这个习惯帮我省下了大量调试时间也让我对 PL/0 的指令执行模型有了更直观的理解。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑