简介:面向编译原理课程设计与PL/0编译器改造练习的完整资料包,适合正在学习词法分析、语法分析、目标代码生成并需完成课程设计的学生参考。内容围绕PL/0语言扩展展开:实现+=、-=赋值运算,加入Pascal风格FOR…TO…DO与FOR…DOWNTO…DO循环,并补充++、--运算及一维数组类型;字符类型、实数类型、带返回值或参数函数等选做项保留说明,便于对照理解实现边界。压缩包为rar格式,共42个文件,约1.25MB,包含pl0源程序、cod目标代码、cpp与pas实现文件、doc说明文档及可执行文件等,结构覆盖源码、工程与测试样例。已有1056人学习下载,可用于拆解编译器扩充思路、比较语法与语义处理方式、定位课程设计中的实现重点与未完成项。
1. 从一次“运算符加错优先级”的翻车说起:PL/0 扩充到底改哪里
编译原理课程设计里选 PL/0 做修改扩充,最先被点名的一般是运算符扩充——加几个符号看起来只是词法表多几行,真正跑起来却会把表达式优先级、语法树和解释器指令一起拖下水。这份资源就是围绕“对 PL/0 作出修改扩充”整理的一套可编译实现:词法层补多字符运算符,语法层重排优先级,语义层补左值和类型检查,代码生成层新增操作码,解释器补执行与除零检查。
它适合正在做课程设计、需要一套能跑通又能讲清原理的基线的人,也适合已经写完但一改运算符就翻车的读者拿来对照。先别急着加十种符号,先把一条最小链路跑顺:词法能正确切出 token,语法能按优先级生成指令,解释器能算出结果并报出可定位的错误。
2. 词法分析扩充运算符:从单字符扫描到双字符最长匹配
拿到 PL/0 源码,第一个要动的是getsym。原始 PL/0 对+ - * /都是单字符判断,加+=、++、&&、||、^、%后,词法层要先回答两个问题:运算符集合有多大,扫描时怎么保证最长匹配。常见做法是把sym枚举扩成 token 枚举,再把双字符判断集中到一个分支,避免在表达式解析里到处补字符判断。先定表再写代码,后面语法层才能按 token 写。
2.1 先定运算符集合与词法单元编码
运算符一多,最怕的是“词法层认识,语法层不认识”或者“语法层认识,解释器不认识”。我一般先列一张三列的表:源符号、token 名、语义备注。表定下来后,词法枚举、语法函数、代码生成操作码都按这张表对齐,后面排错能省很多时间。
| 源符号 | token 名 | 备注 |
|---|---|---|
+-*/% | SYM_PLUS等 | 二元算术,%新增 |
^ | SYM_POWER | 乘方,右结合 |
++-- | SYM_PLUS_PLUS等 | 语句级自增自减 |
+=-=*=/= | SYM_PLUS_ASSIGN等 | 复合赋值,左值必须是变量 |
&&||! | SYM_AND等 | 逻辑运算,结果 0/1 |
=!=<># | SYM_EQSYM_NEQ | #兼容原 PL/0 不等 |
:= | SYM_ASSIGN | 赋值,不要和=混 |
typedef enum { SYM_NUL, SYM_IDENT, SYM_NUMBER, SYM_PLUS, SYM_MINUS, SYM_TIMES, SYM_SLASH, SYM_PERCENT, SYM_POWER, SYM_ASSIGN, SYM_PLUS_ASSIGN, SYM_MINUS_ASSIGN, SYM_TIMES_ASSIGN, SYM_SLASH_ASSIGN, SYM_PLUS_PLUS, SYM_MINUS_MINUS, SYM_EQ, SYM_NEQ, SYM_LT, SYM_LE, SYM_GT, SYM_GE, SYM_AND, SYM_OR, SYM_NOT, SYM_LPAREN, SYM_RPAREN, SYM_COMMA, SYM_SEMI, SYM_DOT, SYM_BEGIN, SYM_END, SYM_IF, SYM_THEN, SYM_WHILE, SYM_DO, SYM_READ, SYM_WRITE, SYM_CALL, SYM_CONST, SYM_VAR, SYM_PROC, SYM_EOF } Sym;逻辑说明:枚举里把单字符算术、双字符复合赋值、双字符逻辑运算、关系运算分开编码,语法层就不需要再根据字符猜语义。参数说明:SYM_ASSIGN对应:=,SYM_EQ对应=;SYM_NEQ同时承接!=、<>和#,后续语法层统一按“不等”处理。SYM_POWER和SYM_PERCENT是新增单字符运算符,编码时不要挤占原有 token 的序号,否则旧测试用例的打印结果会错位。
2.2 getsym 里做最长匹配与回退
原始getsym通常是“看一个字符,switch 一下,直接返回”。加双字符运算符后,必须改成“先读当前字符,再nextch看下一个字符”。这里最容易出的错是:看到+就直接返回SYM_PLUS,结果+=被拆成+和=,语法层报“赋值缺少表达式”。下面的写法把双字符判断集中在每个 case 里,保证最长匹配。
/* ch 是当前输入字符,nextch() 读入下一个字符并更新 line/col */ void getsym(void) { while (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') nextch(); if (isalpha(ch)) { int n = 0; while (isalnum(ch) || ch == '_') { id[n++] = ch; nextch(); } id[n] = '\0'; sym = SYM_IDENT; } else if (isdigit(ch)) { int n = 0; while (isdigit(ch)) { num[n++] = ch; nextch(); } num[n] = '\0'; sym = SYM_NUMBER; } else { switch (ch) { case '+': nextch(); if (ch == '=') { nextch(); sym = SYM_PLUS_ASSIGN; } else if (ch == '+') { nextch(); sym = SYM_PLUS_PLUS; } else sym = SYM_PLUS; break; case '-': nextch(); if (ch == '=') { nextch(); sym = SYM_MINUS_ASSIGN; } else if (ch == '-') { nextch(); sym = SYM_MINUS_MINUS; } else sym = SYM_MINUS; break; case '*': nextch(); if (ch == '=') { nextch(); sym = SYM_TIMES_ASSIGN; } else sym = SYM_TIMES; break; case '/': nextch(); if (ch == '=') { nextch(); sym = SYM_SLASH_ASSIGN; } else sym = SYM_SLASH; break; case '%': nextch(); sym = SYM_PERCENT; break; case '^': nextch(); sym = SYM_POWER; break; case '&': nextch(); if (ch == '&') { nextch(); sym = SYM_AND; } else { error("单个 & 不是合法运算符"); sym = SYM_NUL; } break; case '|': nextch(); if (ch == '|') { nextch(); sym = SYM_OR; } else { error("单个 | 不是合法运算符"); sym = SYM_NUL; } break; case '!': nextch(); if (ch == '=') { nextch(); sym = SYM_NEQ; } else sym = SYM_NOT; break; case '<': nextch(); if (ch == '=') { nextch(); sym = SYM_LE; } else if (ch == '>') { nextch(); sym = SYM_NEQ; } else sym = SYM_LT; break; case '>': nextch(); if (ch == '=') { nextch(); sym = SYM_GE; } else sym = SYM_GT; break; case '=': nextch(); sym = SYM_EQ; break; case '#': nextch(); sym = SYM_NEQ; break; /* 兼容原 PL/0 */ case ':': nextch(); if (ch == '=') { nextch(); sym = SYM_ASSIGN; } else { error(": 后必须跟 ="); sym = SYM_NUL; } break; case '(': nextch(); sym = SYM_LPAREN; break; case ')': nextch(); sym = SYM_RPAREN; break; case ',': nextch(); sym = SYM_COMMA; break; case ';': nextch(); sym = SYM_SEMI; break; case '.': nextch(); sym = SYM_DOT; break; default: error("非法字符"); nextch(); /* 关键:错误分支也要推进,否则死循环 */ sym = SYM_NUL; break; } } }逻辑说明:每个运算符 case 先执行一次nextch(),把当前字符推进到下一个位置,再判断第二个字符;命中双字符时再nextch()一次,保证 token 边界正好落在运算符之后。参数说明:ch是全局当前字符,nextch()负责从源缓冲区读下一个字符并累加行号列号;id和num是标识符、数字缓冲区;sym是输出 token。错误分支必须调用nextch(),否则遇到非法字符时输入指针不动,语法层会反复读到同一个SYM_NUL,程序卡死。
注意:
a+++b按最长匹配会切成a、++、+、b,这不是词法错误,语法层会因为++后没有语句结束符而报错。不要为了让这个输入“看起来合理”在词法层做猜测,词法只负责最长匹配,语义交给语法层。
2.3 词法测试用例和错误定位
词法改完,先别急着跑完整程序,写一个dump_tokens把 token 序列打印出来。PL/0 的课程设计代码通常不大,花十分钟加这个函数,后面排错能少花两小时。测试用例要覆盖双字符边界、三字符连续、关系运算符混用和非法单字符。
| 输入片段 | 期望 token 序列 | 边界点 |
|---|---|---|
a+=1 | ident += number | 复合赋值不能切成+= |
a+++b | ident ++ + ident | 最长匹配后语法层报错 |
a&&&b | ident && & ident | 第二个&走非法分支 |
a^b%c | ident ^ ident % ident | 两个新增单字符 |
x:=1 | ident := number | :必须跟= |
a<>b | ident <> ident | 不等兼容 |
a!=b | ident != ident | !单独是 NOT |
a<=b | ident <= ident | 不要切成<= |
static const char *tokname(Sym s) { switch (s) { case SYM_IDENT: return "ident"; case SYM_NUMBER: return "number"; case SYM_PLUS: return "+"; case SYM_PLUS_ASSIGN: return "+="; case SYM_PLUS_PLUS: return "++"; case SYM_POWER: return "^"; case SYM_PERCENT: return "%"; case SYM_AND: return "&&"; case SYM_OR: return "||"; case SYM_NOT: return "!"; case SYM_ASSIGN: return ":="; case SYM_EQ: return "="; case SYM_NEQ: return "!="; default: return "?"; } } void dump_tokens(void) { do { getsym(); printf("%d:%d %s", line, col, tokname(sym)); if (sym == SYM_IDENT || sym == SYM_NUMBER) printf("(%s)", id); printf("\n"); } while (sym != SYM_EOF); }逻辑说明:tokname只用于调试打印,不参与语义;dump_tokens循环调用getsym,把行号、列号和 token 名打出来,遇到标识符或数字再附带原文。参数说明:line、col是词法分析器维护的当前位置;id在数字 token 时可能为空,调试时看 token 类型即可。错误定位要尽量在error里带上line和col,否则语法层报“缺少分号”时根本不知道是哪个运算符切错了。
3. 递归下降里的运算符优先级:把表达式拆成七层而不是硬塞
词法只给 token,优先级必须在语法层定。PL/0 原始expression只有simple_expression和term两层,新增^、&&、||、!、%后不能全塞进term。常见做法按优先级从低到高拆:logic_or、logic_and、equality、relational、additive、multiplicative、unary、power、primary。每个函数对应一层,调试时看调用栈就知道表达式在哪一层被切开。
3.1 产生式重排:从 logic_or 到 primary
递归
本文还有配套的精品资源,点击获取