手把手教你用C++ Builder 6给PL/0编译器加个‘Else’:从文法到PCODE的完整实战
从文法到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语句时,那种成就感正是编译原理实验的魅力所在。
更多推荐
所有评论(0)