ARTICLE DETAIL

资讯详情

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

PL/0编译器扩展实战:词法语法增强与运行时机制解析

PL/0编译器扩展实战:词法语法增强与运行时机制解析 简介本资源是广东工业大学《编译原理》课程的PL/0编译器实验报告文档面向计算机专业本科生及编译原理初学者聚焦教学型编译系统的设计与扩展实践。报告完整覆盖PL/0语言子集实现、词法与语法分析核心流程、符号表管理、运行时存储组织含静态链/动态链/返回地址机制、错误处理策略并详细说明了对原始PL/0系统的功能扩充——新增ELSE/FOR/TO等保留字、* / --等运算符、不等号及ELSE子句附有对应文法、语法图与语义规则。资源为单个565KB的Word文档.doc格式内容包含11页实验正文、模块函数说明表、PCODE指令解释逻辑、测试用例及开发截图结构严谨、注释详实。目前已有1586人学习下载可直接用于课程作业参考、编译器原理理解深化或C语言实现复现。1. 这不是一份普通实验报告它是一份可运行的 PL/0 编译器扩充实战手记你打开这份《广东工业大学编译原理实验报告.doc》第一眼看到的可能是“学生学院计算机学院”“指导教师XXX”这类格式化信息。但真正值得你花十分钟细读的是第 5 页开始的SSYM[#]NEQ;被注释掉、第 8 页else if (CH|) { GetCh(); if (CH|) { SYMORSYM; ... }这段带缩进的 C 代码以及第 10 页那个嵌套三层if-else的CASE IFSYM修改块——它们不是教学示例而是真实跑通的编译器补丁。这份文档本质是一份带完整上下文的 PL/0 扩展工程日志从词法分析器新增||和!的字符匹配逻辑到语法分析中IF-ELSE语义动作的三地址码生成规则backpatch、merge、makelist再到符号表管理里dx初值设为 3 的深层原因RA/DL/SL 占用前三个单元。它面向的不是“交作业”的学生而是正在用 Visual C 6.0 调试gen()函数、反复比对CODE[CX1].ACX和CODE[CX2].ACX赋值时机的实践者。如果你正卡在 PL/0 的JPC指令跳转地址填空、或困惑于base()函数如何沿静态链回溯 N 层基址这份报告里第 4 页的存储组织图和第 11 页的CX1/CX2双跳转结构就是你缺的那张调试地图。2. 词法分析层扩展从单字符到复合运算符的精准识别PL/0 原生词法分析器采用查表驱动SSYM[]数组与关键字线性匹配KWORD[]/WSYM[]结合的方式其扩展难点不在新增单词数量而在于运算符优先级冲突与字符流状态机设计。原始实现中*和/直接映射为TIMES/SLASH但*,/等复合赋值运算符要求分析器具备“前瞻一个字符”的能力且需避免与*单独出现时的语义混淆。本实验的解决方案是重构GetSym()中的字符处理分支将CH的当前值与下一个字符GetCh()的结果联合判断。2.1 关键字与保留字表的扩容策略新增ELSE,FOR,TO,DOWNTO,RETURN五个保留字需同步更新三处核心数据结构// 符号枚举类型定义关键保持顺序与 KWORD/WSYM 严格对应 typedef enum { NUL, IDENT, NUMBER, PLUS, MINUS, TIMES, SLASH, ODDSYM, EQL, NEQ, LSS, LEQ, GTR, GEQ, LPAREN, RPAREN, MA, SEMICOLON, PERIOD, BEES, BEGINSYM, ENDSYM, IFSYM, THENSYM, WHILESYM, WRITESYM, READSYM, DOSYM, CALLSYM, CONSTSYM, VARSYM, PROCSYM, PROGSYM, ELSESYM, FORSYM, STEPSYM, UNTILSYM, RETURNSYM, // 新增5个 TIMESBEES, SLASHBEES, ANDSYM, ORSYM, NOTSYM // 新增5个运算符符号 } SYMBOL; // 关键字字符串数组长度必须等于 NORW char *KWORD[] { , BEGIN, CALL, CONST, DO, ELSE, // 索引1~5ELSE在第5位 END, FOR, IF, ODD, PROCEDURE, PROGRAM, READ, RETURN, STEP, THEN, UNTIL, VAR, WHILE, WRITE }; // 对应符号码数组KWORD[i] → WSYM[i] SYMBOL WSYM[] { NUL, BEGINSYM, CALLSYM, CONSTSYM, DOSYM, ELSESYM, // 索引1~5ELSESYM在第5位 ENDSYM, FORSYM, IFSYM, ODDSYM, PROCSYM, PROGSYM, READSYM, RETURNSYM, STEPSYM, THENSYM, UNTILSYM, VARSYM, WHILESYM, WRITESYM };注意NORW宏定义必须从14改为19否则position()函数在KWORD数组中线性查找时会越界访问。所有硬编码的33原单词总数需替换为4319个关键字 24个操作符/分隔符但Error(33)例外——该错误码专指“未声明标识符”与单词总数无关强行修改会导致错误提示错乱。2.2 复合运算符的字符流解析逻辑GetSym()函数中对CH的处理需插入复合运算符判断分支核心是避免破坏原有单字符运算符的语义// 在 GetSym() 函数中原有单字符处理之后插入 else if (CH *) { GetCh(); // 向前读取下一个字符 if (CH ) { SYM TIMESBEES; // 复合赋值符号 GetCh(); // 消耗 字符 } else { SYM TIMES; // 恢复为乘法运算符 // 注意此处不调用 GetCh()因为 * 已被消耗CH 已是下一个字符 } } else if (CH /) { GetCh(); if (CH ) { SYM SLASHBEES; // 复合赋值 GetCh(); } else { SYM SLASH; // 除法 } } else if (CH ) { SYM ANDSYM; // 单字符 直接赋值 GetCh(); // 消耗 } else if (CH |) { GetCh(); // 先读取下一个字符 if (CH |) { SYM ORSYM; // 成功匹配 || GetCh(); // 消耗第二个 | } else { Error(19); // 错误码19非法字符 | 后非 | } } else if (CH !) { SYM NOTSYM; GetCh(); }表新增运算符在 PL/0 词法分析中的行为对比运算符输入序列GetSym()内部状态流转最终SYM值是否消耗后续字符**→CH*→GetCh()→CH→SYMTIMESBEES→GetCh()TIMESBEES是消耗**→CH*→GetCh()→CH→SYMTIMESTIMES否留给下一次GetSym()→→CH2.3 不等号#到的迁移陷阱原始 PL/0 使用#表示不等但 Pascal 标准为。迁移时需双重清理删除旧映射注释或删除SSYM[#] NEQ;第 7 页明确指出重写分支在else if (CH )块中增加对的判断else if (CH ) { GetCh(); if (CH ) { SYM LEQ; // GetCh(); } else if (CH ) { SYM NEQ; // 关键此处覆盖了旧的 # 映射 GetCh(); } else { SYM LSS; // } }提示此修改极易引发歧义。若源程序含 空格分隔被识别为LSS被识别为EQL语法分析器会报错而三元运算符在此版本中不被支持。实验报告未提及此边界情况实践中需在test()函数中增强FSYS集合确保和作为原子符号被完整读取。3. 语法分析层增强IF-ELSE 的语义动作与目标代码生成PL/0 原生IF语句仅支持IF B THEN S结构其目标代码生成逻辑简单计算条件B得到真/假出口地址JPC指令跳转至S结束位置。引入ELSE后语法树变为IF B THEN S1 ELSE S2需生成两段跳转指令B为假时跳过S1执行S2S1结束后无条件跳过S2。这要求语法分析器在STATEMENT()过程中精确控制CX代码指针的分配与回填。3.1 IF-ELSE 语法的 BNF 与语义规则落地实验报告第 10 页给出的文法S → if B then M1 S1 N else M2 S2中M1/M2为跳转标记N为ELSE前的无条件跳转占位符。其语义动作需转化为具体代码产生式语义动作三地址码对应 C 代码逻辑S → if B then M1 S1 N else M2 S2backpatch(B.truelist, M1.quad); backpatch(B.falselist, M2.quad); S.nextlist merge(S1.nextlist, N.nextlist, S2.nextlist)CX1记录JPC地址CX2记录JMP地址CODE[CX1].A和CODE[CX2].A在S1/S2结束后回填3.2 STATEMENT() 函数中 IF-ELSE 的代码改造详解原始IF处理第 10 页修改前仅有一个跳转点CX1case IFSYM: GetSym(); CONDITION(...); // 生成 B 的代码B.truelist/B.falselist 已填充 if (SYMTHENSYM) GetSym(); else Error(16); CX1CX; GEN(JPC,0,0); // JPC 0 0 占位A域待填 STATEMENT(...); // 生成 S1 代码 CODE[CX1].ACX; // 回填S1 结束地址 break;增强后的ELSE版本第 10 页修改后引入双跳转case IFSYM: GetSym(); CONDITION(SymSetUnion(SymSetNew(THENSYM,DOSYM),FSYS),LEV,TX); if (SYMTHENSYM) GetSym(); else Error(16); CX1CX; GEN(JPC,0,0); // 第一跳B为假时跳转至 S2 起始 // 关键S1 的 FOLLOW 集合需包含 ELSE否则无法识别 ELSESYM STATEMENT(SymSetUnion(SymSetNew(ELSESYM),FSYS),LEV,TX); if (SYM ! ELSESYM) { // 无 ELSE按原逻辑回填 CX1 为当前 CX CODE[CX1].A CX; } else { // 有 ELSE先生成无条件跳转 JMP 越过 S2 GetSym(); // 消耗 ELSESYM CX2CX; GEN(JMP,0,0); // JMP 0 0 占位指向 S2 结束后 // 回填 CX1S1 结束地址即 S2 起始地址 CODE[CX1].ACX; // 生成 S2 代码 STATEMENT(FSYS,LEV,TX); // 回填 CX2S2 结束地址 CODE[CX2].ACX; } break;表IF-ELSE 目标代码生成的地址回填流程以IF A0 THEN B:1 ELSE B:0为例步骤CX值生成指令CODE[CX].A回填时机说明110LOD 0 3(加载 A)—条件表达式开始211LIT 0 0(加载 0)——312OPR 0 10()—B.truelist[12],B.falselist[13]413JPC 0 0CX113CX1记录此处待回填514LIT 0 1—S1开始B:1615STO 0 4—S1结束716JMP 0 0CX216CX2记录此处待回填817LIT 0 0—S2开始B:0918STO 0 4—S2结束10——CODE[13].A16CX1回填S1 结束后跳至 S2 起始1611——CODE[16].A19CX2回填S2 结束地址193.3 FOLLOW 集合的动态调整必要性STATEMENT(SymSetUnion(SymSetNew(ELSESYM),FSYS),LEV,TX)中的FSYS是当前语句的“同步集合”决定语法分析器在出错时跳过哪些符号。为使ELSE被正确识别S1的FOLLOW必须包含ELSESYM。若忽略此点STATEMENT()在解析完S1后遇到ELSE会触发Error(19)非法字符而非进入ELSE分支。实验报告第 10 页代码明确使用SymSetUnion(SymSetNew(ELSESYM),FSYS)这是保障ELSE语法合法性的底层机制。4. 运行时存储与符号表静态链、基址计算与变量偏移PL/0 的栈式执行模型依赖严格的运行时存储布局。每个过程激活记录Activation Record固定包含三个头部字段静态链SL、动态链DL、返回地址RA随后才是局部变量。理解dx数据区偏移指针的初始化与递增逻辑是读懂enter()、position()及base()函数的关键。4.1 数据区布局与dx的生命周期实验报告第 4 页指出“每个数据区包含三个部分 RA, DL 和 SL”且dx初值为3。这意味着主程序数据区起始地址为0SLDLRA0占用[0]、[1]、[2]dx3指向第一个可用变量槽位索引3每声明一个变量dx变量地址即为dx-1进入子过程时dx重置为3为其独立分配空间。// block() 函数中变量声明处理vardeclaration void vardeclaration(int TX, int dx) { if (SYM VARSYM) { GetSym(); do { if (SYM IDENT) { enter(TX, dx); // 将标识符加入符号表地址为 dx dx; // dx 指向下一槽位 GetSym(); } else Error(4); // 标识符错误 if (SYM MA) GetSym(); // 处理逗号分隔 } while (SYM IDENT); if (SYM SEMICOLON) GetSym(); else Error(5); // 缺少分号 } }4.2base()函数静态链导航的核心算法base()函数用于计算任意嵌套层级变量的基址其参数L表示“当前过程相对于目标过程的层数差”。例如主程序层 0中调用过程 A层 1A 中又调用过程 B层 2B 需访问主程序的变量则L 2 - 0 2。// base() 函数原型int base(int b, int l, int s) // b: 当前基址, l: 层数差, s: 栈顶地址 int base(int b, int l, int s) { // 沿静态链向上跳 l 层 while (l 0) { b stack[b]; // stack[b] 存储上一层的 SL l--; } return b; }逻辑说明stack[b]是地址b处存储的值即静态链指针。初始b为当前过程数据区基址每次b stack[b]即跳转至上一层数据区基址循环l次后得到目标层基址。lod指令加载局部变量调用base(b, l, s)获取基址再加偏移量a得到最终地址stack[base(b,l,s)a]。4.3 符号表结构与enter()/position()实现符号表table[]是结构体数组每个元素包含标识符名、种类CONST/VAR/PROC、层数level、地址adr常量值或变量偏移struct table_entry { char name[12]; int kind; // CONST, VAR, PROC int level; // 声明所在嵌套层 int adr; // 地址常量值或变量偏移 int size; // 过程代码长度仅 PROC }; table_entry table[100]; // 符号表 int tx 0; // 符号表指针 // 登录新标识符 void enter(int tx, int dx) { strcpy(table[tx].name, id); // id 为全局变量存上次识别的标识符 table[tx].kind VAR; table[tx].level lev; // lev 为当前嵌套层 table[tx].adr dx; // dx 为变量在数据区的偏移 tx; } // 查找标识符 int position(char *name) { int i tx - 1; while (i 0 strcmp(table[i].name, name) ! 0) i--; return i; // 返回索引-1 表示未找到 }5. 调试与验证从测试用例到目标代码清单的端到端检查实验报告第 6 页的测试用例PROGRAM EX01; VAR A,B,C; BEGIN A:9; B:6; IF AB THEN WRITE(A) END.是验证扩展功能的黄金标准。但仅运行成功不够需通过listcode()输出的目标代码清单PCODE反向验证语义正确性。5.1 测试用例的 PCODE 解析要点运行该程序后listcode()生成类似以下清单简化0: LIT 0 9 // A : 9 1: STO 0 3 // 地址3为Adx3 2: LIT 0 6 // B : 6 3: STO 0 4 // 地址4为B 4: LOD 0 3 // 加载A 5: LOD 0 4 // 加载B 6: OPR 0 10 // 比较假设由 和 ! 组合实际需看CONDITION实现 7: JPC 0 12 // 若假跳至12WRITE前 8: LOD 0 3 // A为真执行WRITE(A) 9: CAL 0 13 // 调用WRITE过程 10: JMP 0 13 // 无条件跳过ELSE分支本例无ELSE但结构存在 11: ... // 其他指令关键验证点STO 0 3和STO 0 4确认变量A/B地址分配正确dx从3开始JPC 0 12的A域值12应等于WRITE指令起始地址证明CX1回填准确若添加ELSE分支如IF AB THEN WRITE(A) ELSE WRITE(B)清单中应出现JMP指令及第二段WRITE代码。5.2 常见故障定位指南现象可能原因检查点程序死循环CX指针未递增或GEN()未写入CODE[]检查gen()函数中CX是否被注释确认CODE[CX].f,.l,.a赋值无误IF-ELSE总执行ELSE分支JPC指令A域未回填或回填错误地址在STATEMENT()中CODE[CX1].ACX前加printf(CX1%d, CX%d\n, CX1, CX)调试变量A访问报错地址越界dx初始化错误或enter()未传入正确dx在block()开头打印dx值确认vardeclaration()调用enter(TX, dx)后dxbase()返回错误基址静态链未正确建立或lev层级计算错误在block()进入时打印lev检查sl静态链是否在gen()中正确写入stack[base]5.3 一键验证脚本Windows 批处理为快速复现编译-运行-查看清单流程可创建run_test.batecho off rem 编译 PL/0需 VC6.0 环境 cl /c /nologo pl0.c link pl0.obj /out:pl0.exe /subsystem:console rem 运行测试并输出清单 echo PROGRAM EX01; VAR A,B,C; BEGIN A:9; B:6; IF AB THEN WRITE(A) END. test.pl0 pl0.exe test.pl0 output.txt rem 提取目标代码清单假设 listcode() 输出以 CODE: 开头 findstr /C:CODE: output.txt code_list.txt echo 目标代码已保存至 code_list.txt pause运行此脚本后code_list.txt将包含完整的 PCODE可逐行对照JPC/JMP地址验证IF-ELSE逻辑。本文还有配套的精品资源点击获取
返回列表