简介:本资源是东北大学秦皇岛分校编译原理课程的词法分析实验报告,面向计算机专业本科生及编译技术初学者,聚焦PL/0语言词法分析器的设计与C++实现,解决从理论到代码落地的关键实践问题。压缩包含1个DOC文档(122KB),完整呈现实验目的、环境配置(Windows+C++)、核心函数设计(isLetter/isDigit/isP/isJ等)、主程序逻辑、关键词表定义、单词识别流程及错误检测机制(如非法标识符'2A'的判别),并附有规范格式的实验报告正文与可直接运行的源码片段。已有97人学习下载,读者可直接复用代码结构、理解字符分类与单词构建逻辑、掌握词法分析二元组输出规范,并借鉴其文件读取(pl0.txt)、状态迁移与边界处理等工程细节,为后续语法分析实验打下坚实基础。
1. 编译原理实验报告:不是交作业的PDF,而是你亲手把 PL/0 源码喂进黑匣子、看着它吐出四元式和目标代码的完整证据链
“编译原理实验报告”这八个字,在山东科技大学、燕山大学、中国矿业大学等高校的课程表里,从来不是一份格式规范的Word文档——它是你第一次亲手把begin a:=1; b:=a+2; end.这行 PL/0 代码,从字符流开始,一层层剥开词法、语法、语义的壳,最终在内存里生成可执行的 P-code 指令序列的全程录像。它不考背诵龙书第三章定理,而考你能不能让yacc报错时精准定位到.y文件第 47 行的归约冲突,能不能在g++ -g调试时单步跟踪到emit()函数往code[]数组写入LOD 0 1的那一帧。如果你正被“vscode配置c/c++环境”卡在#include <iostream>红波浪线,或被“c++随机数”干扰了PL0Lexer::nextToken()的状态机逻辑——这篇笔记就是为你写的:它不讲理论推导,只讲怎么用 C++ 在本地跑通一个真实可调试、可断点、可修改的 PL/0 编译器,并产出一份能说服助教“你真懂了”的实验报告。
2. 用 C++ 实现 PL/0 编译器:从 lexer 到 codegen 的最小可行闭环
PL/0 是编译原理教学的经典靶机:语法极简(仅支持整数、赋值、顺序、条件、循环、过程),但五脏俱全(词法分析、语法分析、符号表、中间代码生成、解释执行)。我们不堆砌框架,直接构建一个可单步调试、可打印 AST、可导出四元式的 C++ 实现。整个工程控制在 2000 行以内,所有模块紧耦合、无外部依赖(不引入 Boost、不调用 LLVM),纯靠std::string、std::vector和裸指针管理符号表——这是为了让你看清每一处内存分配和生命周期,而不是被模板元编程绕晕。
2.1 词法分析器:手写状态机,拒绝正则黑盒
PL/0 关键字只有begin,end,if,then,while,do,call,const,var,procedure,odd共 11 个,标识符以字母开头、后接字母或数字,整数为十进制无符号数。很多同学用 Flex 生成 lexer,结果调试时根本不知道yylex()内部怎么跳转。我们手写PL0Lexer类,核心是nextToken():
// lexer.h enum TokenType { TK_BEGIN, TK_END, TK_IF, TK_THEN, TK_WHILE, TK_DO, TK_CALL, TK_CONST, TK_VAR, TK_PROCEDURE, TK_ODD, TK_IDENTIFIER, TK_NUMBER, TK_PLUS, TK_MINUS, TK_TIMES, TK_SLASH, TK_LPAREN, TK_RPAREN, TK_EQ, TK_NEQ, TK_LT, TK_LE, TK_GT, TK_GE, TK_ASSIGN, TK_SEMICOLON, TK_PERIOD, TK_EOF }; struct Token { TokenType type; std::string value; int line; }; class PL0Lexer { private: std::string input; size_t pos; int line; std::unordered_map<std::string, TokenType> keywords; public: PL0Lexer(const std::string& src) : input(src), pos(0), line(1) { keywords = {{"begin", TK_BEGIN}, {"end", TK_END}, {"if", TK_IF}, {"then", TK_THEN}, {"while", TK_WHILE}, {"do", TK_DO}, {"call", TK_CALL}, {"const", TK_CONST}, {"var", TK_VAR}, {"procedure", TK_PROCEDURE}, {"odd", TK_ODD}}; } Token nextToken() { skipWhitespace(); if (pos >= input.length()) return {TK_EOF, "", line}; char c = input[pos]; if (std::isalpha(c)) { return identifierOrKeyword(); } else if (std::isdigit(c)) { return number(); } else if (c == '+') { pos++; return {TK_PLUS, "+", line}; } else if (c == '-') { pos++; return {TK_MINUS, "-", line}; } else if (c == '*') { pos++; return {TK_TIMES, "*", line}; } else if (c == '/') { pos++; return {TK_SLASH, "/", line}; } else if (c == '(') { pos++; return {TK_LPAREN, "(", line}; } else if (c == ')') { pos++; return {TK_RPAREN, ")", line}; } else if (c == '=') { pos++; if (pos < input.length() && input[pos] == '=') { pos++; return {TK_EQ, "==", line}; } else { return {TK_ASSIGN, "=", line}; } } // ... 其他单字符/双字符运算符(<, <=, >, >=, <>, ;, .) else { pos++; return {TK_EOF, std::string(1, c), line}; // 错误字符 } } private: void skipWhitespace() { while (pos < input.length()) { char c = input[pos]; if (c == '\n') { line++; } if (std::isspace(c)) { pos++; } else break; } } Token identifierOrKeyword() { size_t start = pos; while (pos < input.length() && (std::isalnum(input[pos]) || input[pos] == '_')) { pos++; } std::string id = input.substr(start, pos - start); auto it = keywords.find(id); if (it != keywords.end()) { return {it->second, id, line}; } else { return {TK_IDENTIFIER, id, line}; } } Token number() { size_t start = pos; while (pos < input.length() && std::isdigit(input[pos])) { pos++; } std::string numStr = input.substr(start, pos - start); return {TK_NUMBER, numStr, line}; } };关键参数说明:
line字段必须全程维护,否则语法错误提示(如error at line 5)会失效;keywords哈希表查找是 O(1),比线性遍历快一个数量级;identifierOrKeyword中std::isalnum严格限定 PL/0 标识符规则(不支持下划线?按教材删掉_即可)。
这个 lexer 的价值在于:你能用gdb在nextToken()第一行下断点,观察input[pos]如何一步步变成TK_ASSIGN,而不是对着 Flex 生成的 3000 行yytables.c发呆。
2.2 语法分析器:递归下降 + 错误恢复,比 Yacc 更透明
PL/0 文法是 LL(1) 友好的,我们放弃 Yacc/Bison,手写递归下降分析器。这不是炫技,而是因为:Yacc 报shift/reduce conflict时,你得看y.output文件猜哪条产生式冲突;而手写分析器,parseStatement()里if (lookahead == TK_IF)分支走错,gdb一步就看到lookahead当前值是TK_WHILE——问题当场定位。
核心文法片段(对应《编译原理》清华大学出版社第三版第二章):
<程序> → <分程序> . <分程序> → [<常量说明部分>] [<变量说明部分>] [<过程说明部分>] <语句> <语句> → <赋值语句> | <调用语句> | <复合语句> | <条件语句> | <当型循环语句> <赋值语句> → <标识符> := <表达式> <表达式> → [+|-] <项> {<加减运算符> <项>} <项> → <因子> {<乘除运算符> <因子>} <因子> → <标识符> | <无符号整数> | (<表达式>)对应 C++ 实现(节选parseStatement()):
// parser.h class PL0Parser { private: PL0Lexer& lexer; Token lookahead; std::vector<Quad> quads; // 四元式序列 std::vector<SymTabEntry> symtab; // 符号表 int nextQuadAddr = 0; public: PL0Parser(PL0Lexer& l) : lexer(l) { lookahead = lexer.nextToken(); } void parseProgram() { parseBlock(); expect(TK_PERIOD); // 匹配结尾的 '.' // 输出四元式到文件,供后续解释器读取 dumpQuads("output.quad"); } private: void parseBlock() { // 处理 const/var/procedure 部分(略,见完整代码) parseStatement(); // 主语句 } void parseStatement() { switch (lookahead.type) { case TK_IDENTIFIER: { // 赋值语句:id := expr std::string id = lookahead.value; expect(TK_IDENTIFIER); expect(TK_ASSIGN); int exprAddr = parseExpression(); // 生成四元式:(:=, exprAddr, _, id) emit(Quad{OP_ASSIGN, exprAddr, -1, id}); break; } case TK_CALL: { expect(TK_CALL); expect(TK_IDENTIFIER); // 生成 call 指令(略) break; } case TK_BEGIN: { expect(TK_BEGIN); do { parseStatement(); if (lookahead.type == TK_SEMICOLON) { expect(TK_SEMICOLON); } } while (lookahead.type != TK_END); expect(TK_END); break; } case TK_IF: { expect(TK_IF); int condAddr = parseCondition(); // 返回条件计算结果存放地址 int jumpAddr = nextQuadAddr; // 记录 JPC 指令位置 emit(Quad{OP_JPC, condAddr, -1, ""}); // JPC condAddr, ? parseStatement(); // 修复跳转地址:将 ? 替换为当前 nextQuadAddr quads[jumpAddr].result = nextQuadAddr; break; } // ... 其他语句类型 default: error("expected statement, got " + tokenToString(lookahead)); } } int parseExpression() { int addr = parseTerm(); while (lookahead.type == TK_PLUS || lookahead.type == TK_MINUS) { TokenType op = lookahead.type; expect(op); int termAddr = parseTerm(); // 生成 ADD 或 SUB 四元式 int resultAddr = newTemp(); emit(Quad{op == TK_PLUS ? OP_ADD : OP_SUB, addr, termAddr, resultAddr}); addr = resultAddr; } return addr; } void expect(TokenType expected) { if (lookahead.type != expected) { error("expected " + tokenToString({expected, "", 0}) + ", got " + tokenToString(lookahead)); } lookahead = lexer.nextToken(); } void error(const std::string& msg) { std::cerr << "Parse Error at line " << lookahead.line << ": " << msg << std::endl; exit(1); } };为什么不用 Yacc?
- Yacc 默认错误恢复策略(
%error-verbose)在 PL/0 这种小文法里反而掩盖问题;parseExpression()中addr变量承载着中间结果地址,这是生成四元式的关键线索,Yacc 动作里变量作用域混乱;- 所有
expect()调用都带行号,错误提示直指源码位置,比syntax error at line 12精确十倍。
2.3 四元式生成与符号表:用 vector 管理作用域,拒绝智能指针玄学
PL/0 支持嵌套过程,符号表必须支持作用域嵌套。我们不用std::shared_ptr<SymTab>,而用std::vector<SymTabEntry>+int scopeDepth实现:
struct SymTabEntry { std::string name; int level; // 0=全局, 1=过程1, 2=过程2... int address; // 相对于该层基址的偏移 TokenType type; // TK_IDENTIFIER / TK_CONST / TK_VAR / TK_PROCEDURE int size; // 仅对数组有用,PL/0 不支持,设为 1 }; class SymbolTable { private: std::vector<SymTabEntry> entries; std::vector<int> scopeStart; // 每个作用域起始索引 int currentLevel = 0; public: void enterScope() { scopeStart.push_back(entries.size()); currentLevel++; } void exitScope() { if (scopeStart.empty()) return; int start = scopeStart.back(); scopeStart.pop_back(); // 清理本作用域所有符号(vector erase) entries.erase(entries.begin() + start, entries.end()); currentLevel--; } void insert(const std::string& name, TokenType type, int addr) { entries.push_back({name, currentLevel, addr, type, 1}); } SymTabEntry* lookup(const std::string& name) { // 逆序遍历,优先匹配最近作用域 for (int i = entries.size() - 1; i >= 0; i--) { if (entries[i].name == name && entries[i].level <= currentLevel) { return &entries[i]; } } return nullptr; } };血泪经验:
exitScope()用erase()而非clear(),否则跨作用域查找会失败;lookup()必须检查entries[i].level <= currentLevel,因为currentLevel可能因过程嵌套变化,而entries[i].level是插入时固定的。
3. VSCode 配置 C/C++ 环境:让 F5 调试 PL/0 编译器像调试 Hello World 一样丝滑
很多同学卡在“vscode配置c/c++环境”,以为要折腾c_cpp_properties.json里的includePath和defines。其实 PL/0 编译器项目不需要任何第三方头文件,唯一需要的是让 VSCode 知道g++在哪、如何编译、如何启动调试器。以下是经过山东科技大学、燕山大学学生实测的最小配置。
3.1 tasks.json:一键编译,不依赖 Makefile
// .vscode/tasks.json { "version": "2.0.0", "tasks": [ { "type": "cppbuild", "label": "C/C++: g++ build active file", "command": "/usr/bin/g++", "args": [ "-g", "-std=c++17", "-Wall", "-Wextra", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}" ], "options": { "cwd": "${fileDirname}" }, "problemMatcher": ["$gcc"], "group": "build", "detail": "Task generated by Debugger." } ] }参数说明:
-std=c++17:启用结构化绑定(用于auto [a,b,c] = quad;),避免c++八股里老式 tuple 解包;-Wall -Wextra:开启全部警告,PL0Lexer::number()里std::stoi(numStr)若未检查溢出,此处会报warning: ‘std::stoi’ called with ‘numStr’ that may be unsafe;"${file}":确保只编译当前打开的.cpp文件,避免多文件项目链接错误。
3.2 launch.json:F5 启动,支持命令行参数传入 PL/0 源码
// .vscode/launch.json { "version": "0.2.0", "configurations": [ { "name": "(gdb) Launch", "type": "cppdbg", "request": "launch", "program": "${fileDirname}/${fileBasenameNoExtension}", "args": ["./test.pl0"], // ← 关键!传入测试文件路径 "stopAtEntry": false, "cwd": "${fileDirname}", "environment": [], "externalConsole": false, "MIMode": "gdb", "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } ], "preLaunchTask": "C/C++: g++ build active file" } ] }为什么
args必须是["./test.pl0"]?
main()函数需接收argc/argv,解析argv[1]作为输入文件名;- 若写成
["test.pl0"],VSCode 在 Windows 下可能找不到路径(因工作目录是${fileDirname},而test.pl0在同级目录);externalConsole: false保证输出在 VSCode 终端,方便复制错误信息。
3.3 c_cpp_properties.json:零配置,删掉也行
PL/0 项目不依赖<boost/...>或<llvm/...>,#include <iostream>、<vector>、<string>全是 GCC 自带标准库。因此c_cpp_properties.json可完全删除,VSCode 的 IntelliSense 会自动识别系统头文件路径。若执意保留,只需最简配置:
// .vscode/c_cpp_properties.json { "configurations": [ { "name": "Linux", "includePath": [ "${workspaceFolder}/**" ], "defines": [], "compilerPath": "/usr/bin/g++", "cStandard": "c17", "cppStandard": "c++17", "intelliSenseMode": "linux-gcc-x64" } ], "version": 4 }避坑提示:不要添加
/usr/include/c++/11/等绝对路径——GCC 版本升级后路径失效;intelliSenseMode必须与compilerPath匹配(linux-gcc-x64对应g++,msvc-x64对应cl.exe)。
4. 编译原理实验报告的硬核产出:不只是截图,而是可验证的中间产物
一份合格的“编译原理实验报告”,绝不能只有main.cpp截图和g++ main.cpp -o pl0命令行。助教要看的是你是否真正理解了编译流程的每个环节。因此,报告必须包含以下 4 类可验证产物,且每类都需提供生成命令和预期输出:
| 产物类型 | 生成方式 | 验证方法 | 典型输出片段 |
|---|---|---|---|
| 词法分析结果 | ./pl0 --lex test.pl0 | 检查 token 序列是否与教材第二章例题一致 | TK_IDENTIFIER aTK_ASSIGN =TK_NUMBER 1 |
| 语法树(AST) | ./pl0 --ast test.pl0 | 观察缩进层级是否反映嵌套结构 | AssignStmt├─ Identifier: a└─ Number: 1 |
| 四元式列表 | ./pl0 --quad test.pl0 | 检查JPC、JMP地址是否连续、无跳转到不存在指令 | (ASSIGN, 1, -, a)(ADD, 1, 2, t1)(JPC, t1, -, 5) |
| P-code 解释执行结果 | ./pl0 test.pl0 | 对比输出与 PL/0 标准解释器结果 | a = 1b = 3 |
4.1 词法分析结果:用--lex参数驱动 lexer 独立运行
修改main(),支持命令行参数:
int main(int argc, char* argv[]) { if (argc < 2) { std::cerr << "Usage: " << argv[0] << " <pl0_file> [--lex|--ast|--quad]" << std::endl; return 1; } std::ifstream ifs(argv[1]); if (!ifs.is_open()) { std::cerr << "Cannot open " << argv[1] << std::endl; return 1; } std::string src((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>()); if (argc > 2 && std::string(argv[2]) == "--lex") { PL0Lexer lexer(src); Token tok; do { tok = lexer.nextToken(); std::cout << tokenToString(tok) << std::endl; } while (tok.type != TK_EOF); return 0; } // ... 其他模式 }验证技巧:拿教材 P23 的
factorial.pl0测试,对比输出中TK_PROCEDURE factorial是否在TK_BEGIN之前——这验证了 lexer 能正确识别关键字,而非当作TK_IDENTIFIER。
4.2 四元式生成:用--quad导出结构化文本,拒绝二进制黑匣子
四元式结构体定义:
enum OpType { OP_ASSIGN, OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_JMP, OP_JPC, OP_LOD, OP_STO }; struct Quad { OpType op; int arg1; // 地址或立即数 int arg2; // 地址或立即数 std::string result; // 变量名或临时变量名(如 t1) };导出函数:
void dumpQuads(const std::string& filename) { std::ofstream ofs(filename); for (size_t i = 0; i < quads.size(); i++) { const Quad& q = quads[i]; std::string opStr; switch (q.op) { case OP_ASSIGN: opStr = ":="; break; case OP_ADD: opStr = "+"; break; case OP_SUB: opStr = "-"; break; case OP_MUL: opStr = "*"; break; case OP_DIV: opStr = "/"; break; case OP_JMP: opStr = "JMP"; break; case OP_JPC: opStr = "JPC"; break; case OP_LOD: opStr = "LOD"; break; case OP_STO: opStr = "STO"; break; } ofs << "(" << opStr << ", "; if (q.arg1 >= 0) ofs << "t" << q.arg1; else ofs << q.arg1; ofs << ", "; if (q.arg2 >= 0) ofs << "t" << q.arg2; else ofs << q.arg2; ofs << ", " << q.result << ")" << std::endl; } }关键设计:
arg1/arg2为负数时表示立即数(如LOD 0 1中的1),为非负数时表示临时变量编号(t1,t2)——这与 PL/0 P-code 规范严格对齐,避免“c++字符串数组初始化”式随意编码。
5. 避坑指南:PL/0 编译器开发中 5 个高频翻车现场
PL/0 实验最典型的失败不是语法错误,而是底层机制理解偏差导致的静默错误。这些坑不会报错,但会让四元式地址错乱、跳转失效、变量覆盖——最终输出结果看似正常,实则逻辑已崩。以下是我在山东科技大学助教岗位上见过的 5 个最高频问题,按现象→原因→解决给出可操作方案。
5.1 现象:JPC指令跳转地址始终为 0,条件语句永远不跳
- 原因:
parseIfStatement()中emit(OP_JPC, condAddr, -1, "")后,未在parseStatement()执行完后更新quads[jumpAddr].result。常见错误是把quads[jumpAddr].result = nextQuadAddr;写在parseStatement()之前,或忘记保存jumpAddr。 - 解决:在
emit()后立即记录当前nextQuadAddr为jumpAddr,并在parseStatement()返回后立刻修复:int jumpAddr = nextQuadAddr; emit(Quad{OP_JPC, condAddr, -1, ""}); parseStatement(); quads[jumpAddr].result = nextQuadAddr; // 必须在此处赋值!
5.2 现象:begin a:=1; b:=a+2; end.编译后b的值是 0 而非 3
- 原因:符号表
lookup("a")返回nullptr,导致LOD指令加载了未初始化内存。根源是parseBlock()中变量声明未插入符号表,或insert()时address计算错误(如未按int占 4 字节对齐)。 - 解决:在
parseVarDeclaration()中,为每个变量分配地址并插入:void parseVarDeclaration() { expect(TK_VAR); do { expect(TK_IDENTIFIER); std::string name = lookahead.value; symtab.insert(name, TK_VAR, nextVarAddr); nextVarAddr += 4; // PL/0 中 int 占 4 字节 if (lookahead.type == TK_COMMA) expect(TK_COMMA); } while (lookahead.type == TK_IDENTIFIER); expect(TK_SEMICOLON); }
5.3 现象:vscode c++调试时nextToken()死循环,CPU 占用 100%
- 原因:
skipWhitespace()中未处理\r\n(Windows 换行符)或\t(制表符),导致pos卡在空白字符处无法前进。 - 解决:强化
skipWhitespace(),显式处理所有空白字符:void skipWhitespace() { while (pos < input.length()) { char c = input[pos]; if (c == '\n') { line++; } else if (c == '\r') { /* 忽略 \r,避免 \r\n 重复计行 */ } else if (std::isspace(static_cast<unsigned char>(c))) { pos++; } else break; } }
5.4 现象:c++小游戏项目能跑通,但 PL/0 编译器g++报undefined reference to 'std::regex_constants::error_collate'
- 原因:误在
PL0Lexer中使用了std::regex(如std::regex_match()),而 PL/0 词法极其简单,正则引擎反而引入链接错误(尤其在旧版 GCC)。 - 解决:彻底删除所有
#include <regex>和std::regex调用,回归手写状态机。PL/0 关键字仅 11 个,std::unordered_map查找比正则快 10 倍且无依赖。
5.5 现象:microsoft visual c++ redistributable安装后,./pl0仍报libstdc++.so.6: version 'GLIBCXX_3.4.29' not found
- 原因:在 Ubuntu 20.04 编译的二进制文件,被拷贝到 CentOS 7 运行——后者
libstdc++版本过低(GLIBCXX_3.4.29 是 GCC 11.1 引入)。 - 解决:静态链接标准库,在
tasks.json的args中加入-static-libstdc++:
生成的二进制体积增大 2MB,但可脱离宿主环境运行。"args": [ "-g", "-std=c++17", "-static-libstdc++", // ← 关键! "-Wall", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}" ]
6. 实验报告的终极验证:用 P-code 解释器反向执行四元式,确认编译器没“说谎”
编译原理实验最有力的自证,不是截图,而是用独立的 P-code 解释器,读取你生成的output.quad,执行并输出结果,再与 PL/0 标准解释器对比。这一步能暴露 90% 的中间代码生成错误——比如JPC地址写反、LOD加载了错误栈帧、临时变量重用冲突。
6.1 构建极简 P-code 解释器:50 行搞定
PL/0 P-code 指令集精简(仅 8 条),我们用std::vector<int>模拟栈,std::vector<Quad>加载四元式:
// interpreter.cpp #include "quad.h" #include <vector> #include <iostream> #include <map> int interpret(const std::vector<Quad>& quads) { std::vector<int> stack; std::map<std::string, int> memory; // 变量名→值 int pc = 0; while (pc < quads.size()) { const Quad& q = quads[pc]; switch (q.op) { case OP_ASSIGN: memory[q.result] = (q.arg1 >= 0) ? stack[q.arg1] : q.arg1; break; case OP_ADD: stack.push_back( ((q.arg1 >= 0) ? stack[q.arg1] : q.arg1) + ((q.arg2 >= 0) ? stack[q.arg2] : q.arg2) ); break; case OP_JPC: if ((q.arg1 >= 0) ? stack[q.arg1] : q.arg1 == 0) { pc = q.result.empty() ? -1 : std::stoi(q.result); // 简化:result 存跳转地址 if (pc < 0) break; } break; // ... 其他指令(OP_LOD, OP_STO 等) } pc++; } return 0; }注意:此解释器仅为验证,不实现完整 P-code(如过程调用栈帧),但足以验证
ASSIGN、ADD、JPC的正确性。
6.2 报告中的黄金对比表格:让数据说话
在实验报告末尾,插入如下表格,三列数据必须全部来自你的程序输出:
| 测试用例 | 你的编译器输出 | 标准 PL/0 解释器输出 | 是否一致 | 说明 |
|---|---|---|---|---|
a:=1; b:=a+2; | a = 1b = 3 | a = 1b = 3 | ✅ | 基础赋值与加法正确 |
if 1 then a:=2 else a:=3; | a = 2 | a = 2 | ✅ | JPC跳转地址正确 |
while a<5 do a:=a+1; | a = 5 | a = 5 | ⚠️ | 若超时,检查JMP地址是否指向条件判断前 |
为什么这是报告灵魂?
- 助教一眼看出你是否真跑通了全流程;
- “⚠️” 标记引导你定位
while循环的JMP地址错误(应跳回条件计算,而非LOD);- 所有数据可复现:提供
test1.pl0源码、output.quad文件、你的./pl0 --quad test1.pl0输出截图。
我带过的每一届学生,只要把这张表填满且全打 ✅,实验报告分数就没低于 90。因为这证明你不是在拼凑代码,而是让机器按你的意志运行——编译原理的本质,从来不是纸上谈兵,而是你亲手铸造的齿轮,咬合转动,输出确定的结果。
希望帮到你。
本文还有配套的精品资源,点击获取