ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PL/0编译器扩充实战:从词法分析到P-code生成

PL/0编译器扩充实战:从词法分析到P-code生成 简介面向编译原理课程设计这份资源提供了一套对PL/0编译器进行修改扩充的完整实现方案适合计算机专业本科生在完成课程设计或理解编译器内部结构时参考。资源包共42个文件包含PL/0源程序、编译生成的目标代码、可直接运行的可执行程序、工程配置文件以及配套的编译原理设计说明文档压缩后仅1.25MB结构清晰便于按需查阅。设计内容覆盖了基本要求中的赋值运算、-和Pascal风格FOR语句TO/DOWNTO并实现了选做部分的自增自减、--以及一维数组扩充字符类型、实数类型等未实现项也在描述中明确标出便于对照查漏补缺。随包附带的测试程序可验证词法分析、语法分析和目标代码生成效果配套文档则梳理了整体设计思路可帮助读者快速定位关键代码节省重复编译调试的时间。该资源已有1052人浏览学习对希望借鉴完整课程设计范例、快速上手PL/0扩充开发的同学具有不错的参考价值。1. 对PL/0做修改扩充先看清它哪里“教条”哪里值得改如果你正在做编译原理课程设计题目是“对PL/0作出修改扩充”你大概率已经见过那份只有几百行的教学编译器词法分析一个函数语法分析用递归下降贯穿产生式最后把中间代码交给一个简易虚拟机解释执行。PL/0 精简到几乎没有布尔类型、没有数组、没有 for 循环过程也只支持按值传参所以课程设计的重点并不是从零写编译器而是在这个骨架上加东西。这时最反直觉的一点是语法扩充本身很轻松真正烧时间的是跳转地址回填、层差计算和栈平衡。这篇笔记面向正在做编译原理实验的从业者按词法、语法、P-code 生成、避坑与验证的顺序走一遍保证每个扩展都能直接落代码。2. 先读懂PL/0的三段式骨架词法、递归下降和P-code生成标准 PL/0 源码看起来是一个文件实际上分成三块读字符序列的词法分析器、按产生式做递归下降的语法分析器以及把中间代码搬进栈式虚拟机执行的解释器。后面加任何功能都要同时动这三处。不要在语法层偷偷改词法结果否则最基础的“:能不能被识别成两个字符”都会变成大问题。2.1 PL/0的词法分析为什么最好先改把单字符运算符扩展成多字符原版 PL/0 的运算符大多是单个字符加上取模、负号、短路与或之后词法层必须先把双字符运算符识别出来。常见做法是维护一张保留字表读标识符时先查表读到运算符时再看下一个字符决定是不是双字符终结符。typedef struct { char name[16]; int kind; /* 0: 保留字, 1: 标识符, 2: 常数, 3: 运算符 */ } Symbol; Symbol reserved[] { {begin, TK_BEGIN}, {end, TK_END}, {if, TK_IF}, {then, TK_THEN}, {while, TK_WHILE}, {do, TK_DO}, {call, TK_CALL}, {const, TK_CONST}, {var, TK_VAR}, {procedure, TK_PROCEDURE}, {odd, TK_ODD} }; int get_symbol(void) { while (ch || ch \n || ch \t) next_ch(); if (isalpha(ch)) { int p 0; while (isalnum(ch)) name[p] ch, next_ch(); name[p] 0; for (int i 0; i sizeof(reserved)/sizeof(Symbol); i) if (strcmp(name, reserved[i].name) 0) return reserved[i].kind; return TK_IDENT; } if (isdigit(ch)) { /* 读常数到 value返回 TK_NUMBER */ } switch (ch) { case :: next_ch(); if (ch ) { next_ch(); return TK_ASSIGN; } return TK_ERROR; case : next_ch(); if (ch ) { next_ch(); return TK_AND; } return TK_ERROR; /* 继续扩展 、、! 等 */ } }这段代码的逻辑顺序是先跳过空白再分标识符和运算符两条路。标识符路径里先拼完整名字再查保留字表所以begin不会掉进标识符分支。运算符路径里用next_ch()吃掉第二个字符如果第二个字符不匹配要退回一个字符否则后面语法层会把两个独立符号拼成一个错误终结符。这里的参数说明很重要TK_开头的枚举值必须和语法层的预测集合保持一致比如TK_ASSIGN在 statement 函数里用来判断赋值语句TK_AND在 bool_term 里用来判断逻辑与。我一般把枚举、保留字表、运算符表集中在一个头文件里避免改一处漏一处。如果你想让关键字不区分大小写可以在拼 name 时统一转成小写再查表标识符保留原始大小写这是词法层最安全的做法。2.2 递归下降语法分析的结构每个产生式对应一个函数PL/0 语法分析用的是典型的递归下降每个非终结符对应一个 C 函数。课程设计里最常见的几个产生式是program → block .block → [const 声明] [var 声明] [procedure 声明] statementstatement → 赋值 | call | begin…end | if…then | while…do | repeat…until | for…toexpression → [−] term {(|−) term}term → factor {(*|/|mod) factor}factor → 标识符 | 常数 | ( expression )statement 函数是所有控制结构的入口。我习惯用一个大的 if/else 链按当前符号sym决定走哪个分支void statement(void) { if (sym TK_IDENT) { /* 赋值语句先读变量名再等 : */ get_symbol(); if (sym ! TK_ASSIGN) error(expect :); get_symbol(); expression(); gen(STO, level, offset); } else if (sym TK_IF) { get_symbol(); condition(); if (sym ! TK_THEN) error(expect then); get_symbol(); statement(); } else if (sym TK_BEGIN) { get_symbol(); while (sym ! TK_END) { statement(); if (sym ;) get_symbol(); } get_symbol(); } else if (sym TK_REPEAT) { /* 后面第 4.3 节展开 */ } }这里有一个理论点必须说清楚递归下降不能直接处理左递归产生式。expression → expression term | term这种写法会让 expression 函数无限调用自己必须改写成expression → term {(|-) term}的迭代形式。这就是为什么 term 函数里用while循环而不是递归去读乘除号。语句分支的判断依据是“当前符号能否开启这个产生式”。比如TK_IDENT后面只能跟:所以看到标识符就直接走赋值分支看到if走条件分支看到begin走语句块。遇到不认识的符号时error()要做符号恢复常见策略是跳过当前符号继续分析而不是立刻退出整个程序否则一个错误会连报几十条。2.3 P-code 指令集与解释器的对应中间代码怎么落地PL/0 的中间代码是一组自定义指令通常叫 P-code。我习惯把每条指令固定成三元组操作码、参数 A、参数 B这样在回填跳转地址时特别方便。表格里列出课程设计里最常见的那几条助记符参数含义LIT0, 常数 C把常数 C 压栈LOD层差 L, 偏移 A把声明在第 L 层的变量 A 压栈STO层差 L, 偏移 A把栈顶值存入变量CAL层差 L, 指令地址 A调用过程INT0, 数量 N为过程活动记录分配 N 个单元JMP0, 指令地址 A无条件跳到 AJPC0, 指令地址 A栈顶为假时跳到 AOPR0, 运算码 X执行 X 指定的运算解释器主循环并不复杂逐条取出三元组按操作码分派while (pc code_len) { op code[pc]; a code[pc]; b code[pc]; switch (op) { case LIT: stack[top] b; break; case LOD: stack[top] get_var(a, b); break; case STO: set_var(a, b, stack[top--]); break; case OPR: execute_opr(b); break; case JMP: pc b; break; case JPC: if (stack[top--] 0) pc b; break; } }这段循环的意义在于生成器负责往code[]里填三元组解释器负责消费它们。LOD 的第一个参数是层差而不是绝对层号第二个参数是变量在过程活动记录里的偏移。如果你在符号表里存了声明时的绝对层号生成 LOD 时必须做一次减法这个问题到第 5.4 节还会专门踩一次。调试时我建议在解释器里加一个 dump 开关每条指令执行前打印pc、op、a、b、top这样中间代码生成一错马上能看出是生成器问题还是解释器问题。课程设计答辩时能现场打出 P-code 执行轨迹比空讲原理有说服力得多。3. 给PL/0的表达式动刀取模与一元负号的语法与代码生成为什么先动表达式因为后面所有条件判断、循环控制都在复用 expression 和 factor。表达式层一旦算错加出来的布尔表达式和循环全都会跟着错到时候你分不清是循环回填的问题还是表达式求值的问题。取模和一元负号是课程设计里最常见、最小的一组扩充但能把栈式表达式求值的关键点全部练到。3.1 修改语法产生式与对应函数从 term 层把 factor 打开原版 term 只处理乘除加取模时只需要在 term 函数的 while 循环里增加一个分支void term(void) { factor(); while (sym MUL || sym DIV || sym MOD) { int op sym; get_symbol(); factor(); switch (op) { case MUL: gen(OPR, 0, OPR_MUL); break; case DIV: gen(OPR, 0, OPR_DIV); break; case MOD: gen(OPR, 0, OPR_MOD); break; } } }逻辑说明expression 管加减term 管乘除模factor 管常数、变量和括号表达式。取模放进 term 层后a * b mod c会从左到右依次执行因为 while 循环每读到一个运算符就立即把栈顶两个值运算并压回。这里隐含了一个约定表达式求值过程中栈上始终只有一个中间结果不会出现多个操作数堆积。参数说明OPR_MOD 是解释器里 execute_opr 的一个 case 编号。不同教材的编号可能不一样我习惯在头文件里定义成枚举常量而不是在代码里写魔法数字。用 Java 重写这份课程设计的读者可以把gen换成自己中间代码列表的 add 方法枚举常量照抄即可递归下降部分的逻辑完全一致。3.2 一元负号怎么进语法用 factor 层处理而不是表达式层一元负号最容易做错的位置是 expression 层。如果负号放进 expression-x * y会被解析成-(x * y)因为 expression 的优先级低于 term。大多数语言里-x * y等价于(-x) * y所以负号必须放在 factor 层void factor(void) { if (sym TK_SUB) { get_symbol(); factor(); gen(OPR, 0, OPR_NEG); /* 弹出栈顶取负压回 */ } else if (sym TK_NUMBER) { gen(LIT, 0, value); get_symbol(); } else if (sym TK_IDENT) { /* 查到符号表后生成 LOD */ get_symbol(); } else if (sym () { get_symbol(); expression(); if (sym ! )) error(expect )); get_symbol(); } }逻辑说明factor()里遇到减号时先递归解析后面的 factor再生成 NEG。递归调用保证了--x也能工作先内层 factor 解析x并压栈外层再生成一次 NEG等价于-(-x)。如果不希望支持连续负号可以在递归前加一个判断但递归下降天然支持不建议刻意禁止。这里有一个很隐蔽的坑词法层不要把-3直接识别成一个负数常量。如果你在词法里把负号吃进数字a - 3会被拆成a和-3两个 token语法层看到标识符后没有:立刻报错。正确做法是保留二元减号 TK_SUB让 factor 层决定它是不是一元负号。注意一元负号不新增终结符复用二元减号的 TK_SUB。判断依据是当前位置语句里遇到-且前一个符号不能结束一个表达式那它就是一元负号。递归下降实现时factor 里遇到的-必然是一元负号因为二元减号不会进入 factor 分支。3.3 P-code 生成OPR 指令与运算码的编码约定P-code 的 OPR 指令用同一个操作码带上不同的运算码。解释器的 execute_opr 是一个大 switch新增取模和取负时必须同时改生成器和解释器两边的运算码常量要对得上void execute_opr(int x) { int left, right; switch (x) { case OPR_RETURN: /* 过程返回弹活动记录 */ break; case OPR_ADD: right stack[top--]; left stack[top--]; stack[top] left right; break; case OPR_SUB: right stack[top--]; left stack[top--]; stack[top] left - right; break; case OPR_MUL: right stack[top--]; left stack[top--]; stack[top] left * right; break; case OPR_DIV: right stack[top--]; left stack[top--]; if (right 0) error(division by zero); stack[top] left / right; break; case OPR_MOD: right stack[top--]; left stack[top--]; if (right 0) error(mod by zero); stack[top] left % right; break; case OPR_NEG: stack[top] -stack[top]; break; } }逻辑说明栈式虚拟机里stack[top]是右操作数stack[top-1]是左操作数。算术运算先弹出右再弹出左结果压回栈顶。取模和除法都要检查除数为 0这是原版 PL/0 解释器经常漏掉的运行时错误。参数说明OPR_NEG 是单操作数运算直接在栈顶改值不需要弹两次。如果你把 NEG 设计成弹一次再压一位逻辑也对但要保持和生成器一致。我见过最典型的“黑匣子”现象是生成器写gen(OPR, 0, 11)解释器里 11 却代表别的运算运行时结果完全随机所以运算码一定用枚举并且在解释器入口加一个 default 分支报“unknown OPR”。4. 扩充布尔表达式与循环结构短路求值、repeat-until 和 for 的实现原版 PL/0 的 condition 只能写一个关系比较比如a b、x # 0这让它看起来特别“教学”。要让它能写if a 0 and b 0 then这类真实条件需要把 condition 扩展成布尔表达式同时实现短路求值。数值表达式的栈是 int布尔值约定用 0 表示假、1 表示真这样关系运算的结果可以直接参与后续算术不需要额外类型系统。4.1 把 condition 从“单个比较”改成“布尔项与布尔因子的组合”我把布尔表达式设计成三层bool_expr 管 orbool_term 管 andbool_factor 管 not 和关系比较。优先级从低到高是 or、and、not和 C 语言里||、、!的优先级顺序一致。void bool_expr(void) { bool_term(); while (sym TK_OR) { get_symbol(); bool_term(); gen(OPR, 0, OPR_OR); /* 逻辑或 */ } } void bool_term(void) { bool_factor(); while (sym TK_AND) { get_symbol(); bool_factor(); gen(OPR, 0, OPR_AND); /* 逻辑与 */ } } void bool_factor(void) { if (sym TK_NOT) { get_symbol(); bool_factor(); gen(OPR, 0, OPR_NOT); /* 逻辑非 */ } else if (sym () { get_symbol(); bool_expr(); if (sym ! )) error(expect )); get_symbol(); } else { expression(); int relop sym; /* 、#、、、、 */ get_symbol(); expression(); gen(OPR, 0, relop_to_opcode(relop)); } }逻辑说明关系比较只出现在 bool_factor 的最底层。a 0 and b 0的解析过程是bool_term 读到左 bool_factor进入 else 分支比较出一个 0/1 结果遇到 and 后再读右 bool_factor再得到一个 0/1 结果最后执行 OPR_AND 合并。括号可以出现在 bool_factor 里所以(a 0 or b 0) and c 0也能正确解析。参数说明relop_to_opcode负责把词法层的#、、等终结符映射成 OPR 的比较运算码。注意原版 PL/0 里#表示不等于如果你加了!要么把词法层的!也映射到同一个运算码要么保留#并让!等价于#。我建议两个都识别课程设计报告里写“兼容原版记号”是个加分项。这里还需要注意一个优先级细节not 只作用于一个 bool_factor所以not a 0会被解析成not (a 0)因为 bool_factor 里先递归处理 not再在递归里比较。如果写作(not a) 0反而会报错因为 not 的右操作数要求是布尔因子不是算术表达式。这个行为和 Pascal 一致不用刻意改成 C 风格。4.2 短路求值在 P-code 里怎么实现JPC 跳转的地址回填如果只把 and/or 翻译成 OPR_AND、OPR_OR那是非短路求值左右两个布尔因子都会先求值再合并。对a 0 and b / a 2这样的条件a 为 0 时右侧b / a仍然执行解释器直接除零崩溃。所以短路求值不只是性能优化而是正确性问题。短路的思路是用跳转把不需要求值的右侧整个跳过。以 and 为例左边为假时右边的结果已经不影响整体结果直接跳到表达式结束左边为真时才需要继续求右边。代码生成需要先把跳转指令的目标地址留空等右边生成完成后回填int skip_right 0; gen_code_for_bool_factor(); /* 左边布尔因子栈顶是 0/1 */ skip_right gen(JPC, 0, 0); /* 左边为假跳过右边 */ gen_code_for_bool_factor(); /* 右边布尔因子 */ gen(OPR, 0, OPR_AND); /* 走到这里说明左边为真 */ patch_address(skip_right, code_len); /* 跳过右边后落在整个表达式之后 */逻辑说明JPC 是“栈顶为假才跳”。左边为假时跳转目标指向 AND 指令之后左边为真时顺序执行右边因子再执行 AND把 1 和右结果合并。这里容易出错的地方是回填的时机必须在完整生成右因子和 AND 指令之后取code_len而不是生成右因子一开始的位置否则会跳过右边的代码却把 AND 留下栈上只剩一个左值。or 的短路是镜像逻辑左边为真时跳过右边需要一条无条件 JMP 和一条 JPC 配合。我的实现是求完左边后生成JPC跳到表达式结束左边为假才过去求右边再生成JMP跳到合并结果后面左边为真时跳过右边然后在 JPC 目标处生成右因子并执行 OR最后把 JMP 的目标也回填。两条跳转指令分别负责“满足短路条件”和“不满足短路条件”各司其职。调试时把 P-code 打印出来重点看三条指令JPC 的目标是否指向右因子之后JMP 的目标是否指向 OR 之后。手推一遍a and b确认三种输入00、01、11下栈顶结果都是对的。这一步做完短路才算真正落到了代码上。4.3 加 repeat-until 和 for 循环语法入口与回填策略原版 PL/0 只有 while没有 repeat-until 和 for。repeat-until 的语法是repeat statement; statement; ... until condition语义是先执行循环体再判断条件条件为假继续循环。这意味着循环体的起始地址要提前保存生成条件后用 JPC 回填到循环开始case TK_REPEAT: { int loop_start code_len; get_symbol(); do { statement(); if (sym ;) get_symbol(); } while (sym ! TK_UNTIL); if (sym ! TK_UNTIL) error(expect until); get_symbol(); condition(); gen(JPC, 0, loop_start); /* 条件为假跳回循环体开头 */ break; }for 循环要麻烦一些。语法for i : 初值 to 终值 do statement需要先给循环变量赋初值再把终值保存到临时变量每次循环比较循环变量和临时变量循环体执行完后自增再无条件跳回循环开始case TK_FOR: { get_symbol(); /* for */ if (sym ! TK_IDENT) error(loop var expected); get_symbol(); /* 循环变量名 */ get_symbol(); /* : */ expression(); /* 初值 */ gen(STO, current_level, loop_var_addr); get_symbol(); /* to 或 downto */ int downto (sym TK_DOWNTO); get_symbol(); expression(); /* 终值 */ gen(STO, current_level, temp_addr); /* 保存终值副本 */ int loop_start code_len; gen(LOD, current_level, loop_var_addr); gen(LOD, current_level, temp_addr); gen(OPR, 0, downto ? OPR_GE : OPR_LE); /* 比较 */ int exit_pos gen(JPC, 0, 0); /* 不满足则退出 */ get_symbol(); /* do */ statement(); /* 循环体 */ gen(LOD, current_level, loop_var_addr); gen(LIT, 0, downto ? -1 : 1); gen(OPR, 0, OPR_ADD); /* 自增或自减 */ gen(STO, current_level, loop_var_addr); gen(JMP, 0, loop_start); patch_address(exit_pos, code_len); /* 出口回填 */ break; }这里有一个设计取舍终值必须存进临时变量。原因是循环体可能修改计算终值所用的变量如果每次比较都重新计算终值表达式结果会漂移甚至死循环。临时变量槽可以在每个过程的局部变量区尾部预留两个位置嵌套 for 时交替使用block 结束就把占用标记清掉。参数说明loop_var_addr是循环变量在活动记录里的偏移current_level是当前过程层号。比较方向用downto控制自增步长固定为 1这是最简单也最符合 Pascal 原语义的做法。课程设计如果不需要 downto可以删掉这个分支但保留它在报告里写“扩展了步进循环”更有亮点。5. PL/0扩充避坑实录跳转回填、层差与短路求值最容易翻车的五个点下面按“现象 → 原因 → 解决”的格式写五条都是做 PL/0 扩充时真正会浪费一整晚的问题。每一条都能在只加几十行代码的规模里复现。5.1 现象取模和负号生成的 P-code 总是“反过来”我给 term 加完MOD后10 mod 3的结果一直等于 1而不是 1 之外的数。把 P-code 打出来才发现生成器先压了 10再压 3但解释器里写的运算式是stack[top] % stack[top-1]即 3 % 10。原因栈式虚拟机里后压入的是右操作数。生成表达式left op right时代码顺序是“先求 left 压栈再求 right 压栈”所以栈顶是 right栈顶下面是 left。运算时必须先弹 right再弹 left然后执行left op right。解决统一所有二元运算的弹出顺序。我在 execute_opr 里固定写right stack[top--]; left stack[top--]; stack[top] left op right;并且在注释里写明“栈顶是右操作数”。一元负号没有这个问题但同样要在生成器里保证 factor 先压操作数、再生成 NEG顺序反了等于对别的值取负。5.2 现象repeat-until 的条件跳转把循环体直接跳过写完 repeat-until 后循环体一次都没执行直接进入循环后面的代码。打印 P-code 发现 JPC 的目标地址指向了循环结束位置而不是循环体开头。原因把loop_start记录放在了 condition 生成之后。生成 repeat 时code_len已经是循环体结束的位置JPC 回填到那个位置自然就把循环体全部跳过了。解决循环入口必须在生成循环体之前记录也就是get_symbol()读完 repeat 关键字后就立即保存code_len。条件生成完毕后的 JPC 第二操作数填这个入口。另外 check 一点condition 内部如果还有 and/or 短路也会生成跳转指令这些指令的回填目标是条件内部的标签不要和循环回填的地址混在一起。我给每条跳转指令都打一个独立的 label 编号回填时按 label 找避免用裸地址。5.3 现象for 循环的终值被循环体修改结果多跑或者死循环for i : 1 to n do begin n : n - 1; ... end这种程序里循环次数完全不可控。原因是我最初生成的 for 每次比较都重新读n的值循环体一旦修改 n比较用的上界就跟着变。解决进入循环前先把终值求出来存进临时变量槽比较时始终读临时变量。临时变量槽在局部变量区尾部预留两个位置生成 for 时分配for 结束后释放。如果是嵌套 for先申请内层再申请外层释放顺序反过来保证不会覆盖。这样n在循环体里被改多少次循环次数都按初始值算符合大多数语言的 for 语义。5.4 现象过程嵌套超过两层局部变量访问错乱只写一层过程时LOD 和 STO 都正常一旦 procedure 里再嵌套 procedure内层过程读写外层变量时取到的却是当前层的局部变量或者直接读到活动记录里乱七八糟的数据。原因P-code 的 LOD/STO 第一个参数是层差不是绝对层号。符号表里通常记录一个变量“在第几层声明”生成代码时要用当前层减去声明层得到层差。如果直接把声明层填进 LOD解释器按层差寻址时就会走出错误的访问链。解决生成 LOD 前算level_diff current_level - declared_level然后调用int get_var(int level_diff, int offset) { int base display[level_diff]; return stack[base offset]; }display 表在进入过程时压入当前过程活动记录基址退出时弹出。调用嵌套过程时还要注意 display 表恢复的顺序否则外层过程返回后访问链就断了。这里的排查技巧是在每个 CAL 指令处打印调用前后的 display 内容对比手推的活跃记录链。5.5 现象布尔短路求值后栈顶残留“假值”给 and/or 加完短路后if a 0 and b 0 then的条件判断经常反过来而且语句块结束后栈顶还多出一个值。现象是条件为真时执行了 else 分支或者后续语句的变量偏移全部错位。原因短路跳转把一部分布尔结果留在栈上。比如 or 短路里左边为真时用 JMP 直接跳过右边但跳转前左边那个真值没有被弹出到达表达式合并点时栈顶多出一个值导致后面的 JPC 判断或变量访问全部错位。解决给短路结构加一条约束每个 bool_factor 生成结束时栈上只能有一个布尔值并且所有跳转出口都要保证栈深一致。我的做法是在解释器主循环里加一个栈深断言每条语句生成前后检查栈深是否回到相同值if (top ! saved_top) error(stack depth mismatch at statement boundary);这个断言救了我很多次。短路逻辑每多一层栈上值就多一个断言能直接指出是哪条语句把栈弄脏了。课程设计报告里写“解释器内置栈平衡检查”比口头讲正确性更让人信服。6. 用最小测试集和数据流检查验证扩充后的PL/0错误信息与栈平衡两个习惯扩充做完最值得投入时间的不是继续加语法而是把验证流程固定下来。我给解释器加了一个--dump开关运行每条指令前打印pc、指令三元组、执行后栈深再用一组最小测试样例做回归。样例不用多每个新功能配一行程序就够功能输入期望结果取模begin a : 10 mod 3 enda 等于 1一元负号begin a : -2 * 3 enda 等于 -6and 短路begin if 0 and 1/0 2 then a : 1 else a : 0 end不触发除零a 等于 0repeatbegin i : 0; repeat i : i 1 until i 3 endi 等于 4forbegin for i : 1 to 3 do a : a i enda 等于 6第二个必须养成的习惯是错误报告带行列号。词法分析器在读字符时维护cur_line和cur_col语法错误、解释器运行时错误统一走同一个 error 函数void error(const char *msg) { fprintf(stderr, line %d, col %d: %s\n, cur_line, cur_col, msg); recover(); }有了行列号再配合--dump任何一条扩充功能翻车都能在五分钟内定位到是词法、语法、生成器还是解释器的问题。我做这个课程设计时最花时间的不是加语法而是排地址回填和层差。后来养成的习惯是每个语法分支生成完立刻把 code_len 附近的指令打印成文本对照手推的栈执行过程走一遍。这个习惯帮我挡住了大半翻车。编译原理课程设计做到最后比的不是谁加的语法多而是谁在三天后还能看懂自己的中间代码。希望帮到你。本文还有配套的精品资源点击获取
返回列表