给PL/0编译器“打补丁”:手把手教你用C语言实现IF-ELSE和复合运算符
从零扩展PL/0编译器:实现IF-ELSE与复合运算符的完整指南
在编译原理的学习过程中,PL/0编译器实验是一个经典的教学案例。这个简单的编译器包含了词法分析、语法分析、语义分析等核心模块,是理解编译器工作原理的绝佳起点。然而,标准PL/0语言的功能相当有限,缺少现代编程语言中常见的控制结构和运算符。本文将带你从零开始,逐步为PL/0编译器添加IF-ELSE条件语句和复合运算符(如++、--、&&等)支持,让你深入理解编译器内部工作机制。
1. 理解PL/0编译器基础架构
PL/0编译器采用单趟扫描的递归下降分析法,整个编译过程以语法分析为核心。在开始扩展之前,我们需要先理解其基本结构和工作原理。
1.1 编译器主要组件
PL/0编译器主要由以下几个关键部分组成:
- 词法分析器(GetSym):负责将源代码转换为一系列token
- 语法分析器:递归下降分析程序结构
- 符号表管理:记录变量、常量和过程的信息
- 代码生成器(GEN):生成P-code中间代码
- 错误处理:报告编译过程中的错误
1.2 关键数据结构
// 符号表结构 struct { char name[AL]; // 标识符名称 int kind; // 类型:常量、变量或过程 union { int val; // 常量的值 struct { int level; // 嵌套层次 int adr; // 地址 } vp; } un; } TABLE[TXMAX]; // 目标代码结构 struct { int f; // 功能码 int l; // 层次差 int a; // 操作数 } CODE[CXMAX];1.3 编译流程概览
PL/0的编译过程遵循以下步骤:
- 初始化符号表和代码区
- 调用Block过程处理程序块
- 在Block中处理声明部分(常量、变量、过程)
- 分析语句部分并生成代码
- 遇到程序结束符时完成编译
2. 扩展词法分析器支持新符号
在添加IF-ELSE和复合运算符前,我们需要先让词法分析器能够识别这些新的符号。
2.1 修改符号类型定义
首先在SYMBOL枚举中添加新的符号类型:
typedef enum { NUL, IDENT, NUMBER, PLUS, MINUS, TIMES, SLASH, // ... 其他原有符号 // 新增符号 ELSESYM, // ELSE关键字 PLUSONE, // ++ MINUSONE, // -- ANDSYM, // && ORSYM, // || NOTSYM // ! } SYMBOL;2.2 更新关键字表
在关键字表中添加ELSE和对应的符号映射:
// 关键字字符串 char *KWORD[] = { "BEGIN", "CALL", "CONST", "DO", "ELSE", // 新增 "END", "IF", "ODD", // ... 其他关键字 }; // 关键字到符号的映射 int WSYM[] = { BEGINSYM, CALLSYM, CONSTSYM, DOSYM, ELSESYM, // 新增 ENDSYM, IFSYM, ODDSYM, // ... 其他映射 };2.3 扩展GetSym函数
修改词法分析函数GetSym以识别新符号:
void GetSym() { // ... 原有代码 else if (CH == '+') { GetCh(); if (CH == '+') { // 处理++运算符 SYM = PLUSONE; GetCh(); } else { SYM = PLUS; } } else if (CH == '-') { GetCh(); if (CH == '-') { // 处理--运算符 SYM = MINUSONE; GetCh(); } else { SYM = MINUS; } } else if (CH == '&') { // 处理&&运算符 SYM = ANDSYM; GetCh(); } else if (CH == '|') { GetCh(); if (CH == '|') { // 处理||运算符 SYM = ORSYM; GetCh(); } else { Error(19); // 非法符号 } } else if (CH == '!') { // 处理!运算符 SYM = NOTSYM; GetCh(); } // ... 其他代码 }3. 实现IF-ELSE条件语句
标准PL/0只支持IF-THEN语句,我们将扩展它支持完整的IF-ELSE结构。
3.1 修改文法定义
首先需要更新语法规则:
<条件语句> ::= IF <条件> THEN <语句> [ELSE <语句>]3.2 扩展STATEMENT函数
在语句处理函数中添加对ELSE的支持:
void STATEMENT(SYMSET FSYS, int LEV, int TX) { switch (SYM) { case IFSYM: { GetSym(); // 分析条件表达式 CONDITION(SymSetUnion(SymSetNew(THENSYM, DOSYM), FSYS), LEV, TX); if (SYM != THENSYM) { Error(16); // 缺少THEN } else { GetSym(); } int cx1 = CX; // 保存IF跳转指令位置 GEN(JPC, 0, 0); // 生成条件跳转指令,地址暂填0 // 处理THEN部分的语句 STATEMENT(FSYS, LEV, TX); if (SYM == ELSESYM) { // 处理ELSE分支 int cx2 = CX; // 保存ELSE跳转指令位置 GEN(JMP, 0, 0); // 生成无条件跳转指令,地址暂填0 // 回填IF跳转地址 CODE[cx1].a = CX; GetSym(); // 处理ELSE部分的语句 STATEMENT(FSYS, LEV, TX); // 回填ELSE跳转地址 CODE[cx2].a = CX; } else { // 没有ELSE分支,直接回填IF跳转地址 CODE[cx1].a = CX; } break; } // ... 其他语句处理 } }3.3 代码生成逻辑解析
IF-ELSE语句的代码生成遵循以下步骤:
- 生成条件表达式代码,结果留在栈顶
- 生成JPC(条件跳转)指令,暂时填写假分支地址为0
- 生成THEN部分的语句代码
- 如果遇到ELSE:
- 生成JMP(无条件跳转)指令跳过ELSE部分
- 回填JPC指令的跳转地址到JMP指令后
- 生成ELSE部分的语句代码
- 回填JMP指令的跳转地址到代码末尾
- 如果没有ELSE:
- 直接回填JPC指令的跳转地址到代码末尾
4. 添加复合运算符支持
除了条件语句,我们还可以为PL/0添加一些实用的复合运算符,如自增/自减和逻辑运算符。
4.1 自增/自减运算符
自增(++)和自减(--)运算符可以简化变量的增减操作。
4.1.1 修改表达式处理
在EXPRESSION函数中添加对自增/自减的支持:
void EXPRESSION(SYMSET FSYS, int LEV, int TX) { SYMBOL addop; if (SYM == PLUSONE || SYM == MINUSONE) { addop = SYM; GetSym(); TERM(FSYS, LEV, TX); if (addop == PLUSONE) { GEN(LIT, 0, 1); GEN(OPR, 0, 2); // 加法操作 } else { GEN(LIT, 0, 1); GEN(OPR, 0, 3); // 减法操作 } } else { // ... 原有表达式处理逻辑 } }4.1.2 支持变量自增/自减
为了实现类似C语言的i++和++i效果,我们需要在因子处理中识别这些运算符:
void FACTOR(SYMSET FSYS, int LEV, int TX) { if (SYM == IDENT) { int i = POSITION(ID, TX); if (i == 0) Error(11); // 未定义标识符 GetSym(); if (SYM == PLUSONE || SYM == MINUSONE) { // 处理后缀自增/自减 GEN(LOD, LEV - TABLE[i].vp.level, TABLE[i].vp.adr); // 加载变量值 if (SYM == PLUSONE) { GEN(LIT, 0, 1); GEN(OPR, 0, 2); // 加法 } else { GEN(LIT, 0, 1); GEN(OPR, 0, 3); // 减法 } GEN(STO, LEV - TABLE[i].vp.level, TABLE[i].vp.adr); // 存回变量 GetSym(); } else { // 普通变量引用 GEN(LOD, LEV - TABLE[i].vp.level, TABLE[i].vp.adr); } } // ... 其他因子处理 }4.2 逻辑运算符实现
PL/0原本只支持ODD运算符,我们可以添加&&、||和!等逻辑运算符。
4.2.1 修改条件处理
在CONDITION函数中添加对新逻辑运算符的支持:
void CONDITION(SYMSET FSYS, int LEV, int TX) { SYMBOL relop; if (SYM == NOTSYM) { // 处理!运算符 GetSym(); CONDITION(FSYS, LEV, TX); GEN(OPR, 0, 1); // 逻辑取反 } else { EXPRESSION(FSYS, LEV, TX); if (SYM == ANDSYM || SYM == ORSYM) { relop = SYM; GetSym(); EXPRESSION(FSYS, LEV, TX); if (relop == ANDSYM) { GEN(OPR, 0, 8); // 逻辑与 } else { GEN(OPR, 0, 9); // 逻辑或 } } else if (SYM == EQL || SYM == NEQ /*...其他关系运算符*/) { // ... 原有关系运算符处理 } } }4.2.2 添加新的OPR操作
在解释器中添加对新逻辑运算的支持:
void INTERPRETER() { // ... 解释器主循环 switch (i.f) { case OPR: switch (i.a) { // ... 原有操作 case 8: // 逻辑与 stack[base(b, i.l, s) + t - 1] = stack[base(b, i.l, s) + t - 1] && stack[base(b, i.l, s) + t]; t--; break; case 9: // 逻辑或 stack[base(b, i.l, s) + t - 1] = stack[base(b, i.l, s) + t - 1] || stack[base(b, i.l, s) + t]; t--; break; } break; } }5. 测试与验证
完成编译器扩展后,我们需要编写测试用例验证新功能的正确性。
5.1 IF-ELSE语句测试
PROGRAM IFELSETEST; VAR X, Y; BEGIN X := 10; READ(Y); IF X > Y THEN WRITE(X) ELSE WRITE(Y); END.测试场景:
- 输入Y=5,应输出10(执行THEN分支)
- 输入Y=15,应输出15(执行ELSE分支)
5.2 复合运算符测试
PROGRAM OPERATORTEST; VAR A, B; BEGIN A := 5; B := A++; // 后缀自增 WRITE(A, B); A := 5; B := ++A; // 前缀自增 WRITE(A, B); A := 10; B := 20; IF (A > 5) && (B < 30) THEN // 逻辑与 WRITE(1); ELSE WRITE(0); END.预期输出:
- 第一行:6 5
- 第二行:6 6
- 第三行:1
5.3 综合测试用例
PROGRAM COMPREHENSIVE; VAR I, J, K; BEGIN I := 0; J := 10; WHILE I++ < J DO BEGIN IF I % 2 == 0 THEN K := K + I; ELSE K := K - I; END; WRITE(K); END.这个测试用例综合使用了WHILE循环、IF-ELSE语句和后缀自增运算符,可以全面测试编译器的扩展功能。
6. 常见问题与调试技巧
在扩展PL/0编译器的过程中,可能会遇到各种问题。以下是一些常见问题及其解决方法:
6.1 符号表管理问题
问题:添加新符号后出现符号表溢出或查找错误。
解决方案:
- 检查TABLE数组大小是否足够
- 确认POSITION函数能正确处理新增符号
- 确保ENTER函数正确添加新符号到表中
// 示例:检查符号表是否已满 if (TX >= TXMAX) { Error(26); // 符号表溢出 return; }6.2 代码生成地址错误
问题:跳转指令的地址回填不正确,导致运行时错误。
解决方案:
- 在生成跳转指令时,确保正确保存指令位置
- 回填地址时,确认目标地址计算正确
- 使用调试输出检查生成的中间代码
// 调试输出示例 printf("生成JPC指令,位置=%d,待回填\n", CX); GEN(JPC, 0, 0); // ... printf("回填JPC指令,位置=%d,目标地址=%d\n", cx1, CX); CODE[cx1].a = CX;6.3 运算符优先级问题
问题:新添加的运算符优先级处理不正确。
解决方案:
- 确保表达式分析函数(EXPRESSION, TERM, FACTOR)正确处理新运算符
- 必要时调整文法规则和解析顺序
- 添加括号测试确保优先级正确
// 在EXPRESSION中确保正确处理逻辑运算符 if (SYM == ANDSYM || SYM == ORSYM) { // 处理逻辑运算符 } else if (SYM == EQL || SYM == NEQ /*...*/) { // 处理关系运算符 }6.4 内存管理问题
问题:添加新功能后出现栈溢出或内存错误。
解决方案:
- 检查数据栈大小是否足够
- 确保过程调用时正确管理活动记录
- 验证静态链和动态链的计算
// 检查栈溢出 if (t >= STACKSIZE) { Error(25); // 栈溢出 return; }扩展PL/0编译器是一个深入理解编译原理的绝佳实践。通过添加IF-ELSE语句和复合运算符,我们不仅增强了PL/0语言的功能,也深入探索了编译器各个组件的工作机制。这个过程可能会遇到各种挑战,但每一步的突破都会带来对编译技术更深的理解。
