从文法到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)
...

回填过程:

  1. 第一个 ? 回填为ELSE分支起始地址(示例中为地址1)
  2. 第二个 ? 回填为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. 进阶思考:从实现到原理

通过这个实验,我们可以深入理解几个重要的编译原理概念:

  1. 回填技术 :在无法立即确定跳转目标时,先预留地址后填充
  2. 语法制导翻译 :语法分析过程中同步生成中间代码
  3. 上下文处理 :通过符号表管理不同作用域的变量

对于想进一步探索的同学,可以考虑:

  • 实现ELSE IF的多级条件判断
  • 添加FOR循环语句支持
  • 优化错误恢复机制,在语法错误后能继续分析

修改后的完整代码需要特别注意:

  • 所有出现数字33的地方需要替换为sysnum(错误码33除外)
  • 新增关键字后需要调整相关枚举和常量的值
  • 确保原始功能不受影响

这个看似简单的ELSE添加实验,实际上贯穿了编译器设计的多个关键环节。当你看到自己修改的编译器成功处理第一个IF-ELSE语句时,那种成就感正是编译原理实验的魅力所在。

更多推荐