从文法到PCODE:用C++ Builder 6为PL/0编译器实现Else语句的完整指南
当你在编译原理实验中第一次看到"为PL/0增加ELSE子句"的要求时,是否感到无从下手?本文将以代码行级的操作细节,带你完整走通从文法设计到PCODE生成的实战路径。不同于教科书式的理论讲解,这里每一处修改都有明确的代码定位和实现逻辑。
1. 理解PL/0编译器的基本架构
PL/0编译器采用经典的递归下降分析法,整个编译过程以语法分析为核心,词法分析和代码生成作为独立模块被调用。在C++ Builder 6环境中,主要代码结构如下:
// 核心函数调用关系 PL0() → Block() → Statement() ↓ ┌──────┴──────┐ Condition() Expression()符号表管理采用一维数组TABLE实现,每个标识符包含三个关键属性:
- KIND:标识符类型(常量/变量/过程)
- LEVEL:声明所在的层次
- ADR:在数据区中的相对地址
运行时数据空间S是一个栈结构,通过三个关键寄存器管理:
- B:基址寄存器,指向当前过程数据段的起始地址
- T:栈顶寄存器,指向最新分配的数据单元
- P:程序地址寄存器,保存下一条要执行的指令地址
2. Else语句的文法扩展
原始PL/0的条件语句文法非常简单:
〈条件语句〉::=IF〈条件〉THEN〈语句〉我们需要将其扩展为支持Else的形式:
〈条件语句〉::=IF〈条件〉THEN〈语句〉[ELSE〈语句〉]对应的语法分析图需要增加Else分支:
┌───────────────┐ │ IF │ └──────┬────────┘ │ ┌──────▼───────┐ │ 条件表达式 │ └──────┬───────┘ │ ┌──────▼───────┐ │ THEN │ └──────┬───────┘ │ ┌──────▼───────┐ ┌─────────┐ │ 语句块 ├───► ELSE │ └──────────────┘ └────┬────┘ │ ┌──────▼───────┐ │ 语句块 │ └──────────────┘3. 词法分析器的修改
首先需要在词法分析器中添加ELSE关键字识别:
// 在SYMBOL枚举中增加ELSESYM typedef enum { // ...原有符号... ELSESYM // 新增的else关键字 } SYMBOL; // 关键字表扩展 strcpy(KWORD[6], "ELSE"); // 注意调整数组索引避免冲突 WSYM[6] = ELSESYM; // 关联符号编码 // 修改NORW常量(关键字总数) const int NORW = 19; // 原为14,根据新增关键字数量调整4. 语法分析的核心修改
关键修改点在STATEMENT函数中的条件语句处理部分。原始代码只处理IF-THEN结构:
case IFSYM: GetSym(); CONDITION(..., LEV, TX); if (SYM==THENSYM) GetSym(); else Error(16); CX1 = CX; GEN(JPC, 0, 0); // 生成条件跳转 STATEMENT(..., LEV, TX); CODE[CX1].A = CX; // 回填跳转地址 break;修改后需要支持ELSE分支:
case IFSYM: GetSym(); CONDITION(..., LEV, TX); if (SYM==THENSYM) GetSym(); else Error(16); CX1 = CX; GEN(JPC, 0, 0); // 条件为假时跳转 STATEMENT(..., LEV, TX); // THEN分支 CX2 = CX; GEN(JMP, 0, 0); // 跳过ELSE分支 CODE[CX1].A = CX; // 回填第一个跳转地址 if (SYM==ELSESYM) { GetSym(); STATEMENT(..., LEV, TX); // ELSE分支 } CODE[CX2].A = CX; // 回填第二个跳转地址 break;5. 目标代码生成策略
PL/0的PCODE指令集中,我们需要重点使用两条跳转指令:
| 指令 | 格式 | 功能描述 |
|---|---|---|
| JPC | JPC 0 a | 栈顶值为假时跳转到地址a |
| JMP | JMP 0 a | 无条件跳转到地址a |
对于如下示例代码:
IF a > 0 THEN x := 1 ELSE x := 2生成的PCODE序列及注释:
// 条件判断代码 LOD 0 3 // 加载变量a LIT 0 0 // 加载常数0 OPR 0 12 // 执行>比较 JPC 0 ? // 条件为假时跳转(地址待回填) // THEN分支 LIT 0 1 STO 0 5 // x := 1 JMP 0 ? // 跳过ELSE分支(地址待回填) // ELSE分支 (地址1) LIT 0 2 STO 0 5 // x := 2 // 后续代码 (地址2) ...回填过程:
- 第一个
?回填为ELSE分支起始地址(示例中为地址1) - 第二个
?回填为ELSE分支结束地址(示例中为地址2)
6. 测试用例设计与验证
为验证修改的正确性,需要设计多种测试场景:
测试用例1:基本功能验证
PROGRAM TEST1; VAR A; BEGIN A := 1; IF A = 1 THEN WRITE(1) ELSE WRITE(0); END.预期输出:1
测试用例2:嵌套IF-ELSE
PROGRAM TEST2; VAR X,Y; BEGIN READ(X); IF X > 10 THEN IF X < 20 THEN WRITE(1) ELSE WRITE(2) ELSE WRITE(3); END.测试数据:
- 输入15 → 输出1
- 输入25 → 输出2
- 输入5 → 输出3
测试用例3:无ELSE分支兼容性
PROGRAM TEST3; VAR B; BEGIN B := 0; IF B <> 0 THEN WRITE(B); WRITE(999); END.预期输出:999
7. 常见问题与调试技巧
在实际实现过程中,可能会遇到以下典型问题:
问题1:跳转地址计算错误
- 现象:程序执行进入错误分支或死循环
- 排查:在生成JPC和JMP指令后立即输出CX值,确认回填地址是否正确
- 解决:确保每个STATEMENT调用后及时更新跳转地址
问题2:符号表冲突
- 现象:ELSE被识别为标识符而非关键字
- 排查:检查GetSym函数中关键字的二分查找逻辑
// 在GetSym函数中确认关键字查找 i=1; j=NORW; do { k=(i+j)/2; if (strcmp(ID,KWORD[k])<=0) j=k-1; if (strcmp(ID,KWORD[k])>=0) i=k+1; } while(i<=j);问题3:代码生成顺序错误
- 调试技巧:在关键点插入调试输出
// 在STATEMENT函数中添加调试信息 Form1->printfs("生成JPC指令,当前CX=%d", CX); GEN(JPC, 0, 0);8. 进阶思考:从实现到原理
通过这个实验,我们可以深入理解几个重要的编译原理概念:
- 回填技术:在无法立即确定跳转目标时,先预留地址后填充
- 语法制导翻译:语法分析过程中同步生成中间代码
- 上下文处理:通过符号表管理不同作用域的变量
对于想进一步探索的同学,可以考虑:
- 实现ELSE IF的多级条件判断
- 添加FOR循环语句支持
- 优化错误恢复机制,在语法错误后能继续分析
修改后的完整代码需要特别注意:
- 所有出现数字33的地方需要替换为sysnum(错误码33除外)
- 新增关键字后需要调整相关枚举和常量的值
- 确保原始功能不受影响
这个看似简单的ELSE添加实验,实际上贯穿了编译器设计的多个关键环节。当你看到自己修改的编译器成功处理第一个IF-ELSE语句时,那种成就感正是编译原理实验的魅力所在。