ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

北邮编译原理词法分析器实战:手写DFA与Token生成

北邮编译原理词法分析器实战:手写DFA与Token生成 简介本资源是北京邮电大学计算机学院《编译原理》课程配套的词法与语法分析器实践项目面向高校计算机专业学生及编译技术初学者聚焦编译前端核心能力训练——从源码中识别token并构建抽象语法树。压缩包共12个文件含4个C/C源码.cpp/.c实现分析器逻辑3个Markdown文档.md提供设计说明与实验报告框架4个文本文件.txt存放文法定义、测试样例与演示输入整体仅27KB轻量易读、结构清晰便于逐模块理解与调试。已有200人学习下载资源涵盖LR/LL两种主流语法分析方法的完整实现包含Grammar.txt文法描述、Word_analysis.cpp词法解析核心、LR.cpp与LL.cpp双路径语法分析代码及配套test.cpp验证用例辅以report.md撰写规范和README.md使用指引可直接用于课程实验复现、原理验证或期末项目参考。1. 北邮编译原理词法分析器实战包不是Demo是能跑通sample.c的完整C工程你手头刚拿到一个叫“北京邮电大学计算机学院编译原理词法、语法分析器.zip”的压缩包解压后看到一堆.cpp、.txt和.md文件——别急着扔进IDE编译也别幻想它像PyTorch那样pip install就完事。这是一份真实教学场景下学生交作业用的、带完整测试链路的C词法分析器实现核心目标不是炫技而是让你在30分钟内把sample.c喂进去看到token一行行打印出来再用LR.cpp或LL.cpp生成语法树节点。它不依赖任何第三方库连Boost都不用纯靠iostream、string、vector和手写状态机它不抽象成框架每个if-else都在告诉你/后面跟*就是注释开始0开头的数字串要判八进制0x开头才是十六进制。适合正在啃《编译原理》第三版第二章、被DFA画到怀疑人生、急需一个可调试、可打断点、可改规则的真实参照物的本科生也适合想快速验证某条正则是否覆盖了C语言标识符边界的工程师——毕竟Word_analysis.cpp里那200行switch(state)比教科书上的状态转移表更直白、更易改、更敢动。2. 词法分析器核心从Word_analysis.cpp看状态机落地与Token生成逻辑2.1 状态机设计为什么不用Flex而坚持手写这份代码没用Lex/Flex生成词法分析器而是用纯C实现了确定性有限自动机DFA。原因很实在教学场景下必须让学生亲手走一遍状态跳转路径。比如识别整数常量Word_analysis.cpp中state 10表示已读到0接下来若遇到x或X就跳到state 11十六进制前缀若遇到数字0-7则跳到state 12八进制若遇到8或9反而要报错——这个细节在Flex规则里容易被忽略但在手写状态机里你一眼就能在case 10:分支里看到else if (c 8 c 9) { error(invalid octal digit); }。这种“错误路径显式化”正是教学价值所在。整个状态机共定义了18个状态state 0到state 17覆盖关键字if,while、标识符、十进制/八进制/十六进制整数、浮点数含科学计数法、字符串字面量支持转义、单行/多行注释等全部C子集词法单元。状态跳转表虽未单独抽离为二维数组但通过嵌套switch-caseif-else清晰映射比教科书图示更易关联到代码行。2.2 Token结构体与输出规范token_type和value如何协同工作词法分析结果不是简单打印字符串而是封装为结构体Tokenstruct Token { int token_type; // KEYWORD, IDENTIFIER, NUMBER, STRING, OPERATOR等枚举值 std::string value; // 原始文本内容如while、count、123、hello int line_num; // 行号用于错误定位 };关键点在于token_type决定语义value保留原始形态。例如while和for都属于KEYWORD类型但value不同后续语法分析器靠value做具体关键字匹配而数字0xFF和255虽然value不同但token_type同为NUMBER语义分析阶段才做进制转换。Word_analysis.cpp中每识别出一个token就调用emit_token()函数将Token对象push到全局std::vectorToken tokens中并立即打印方便调试。注意value字段不做任何预处理——a\nb字符串字面量的value就是a\nb四个字符含\n转义处理如\n→换行符留待语义分析阶段这是严格遵循“词法分析只切分、不解释”的原则。2.3 输入流管理get_next_char()如何处理换行与EOF词法分析器不直接操作std::cin或fopen而是封装了get_next_char()函数统一读取char get_next_char() { if (peeked_char ! \0) { char c peeked_char; peeked_char \0; return c; } char c; if (std::cin.get(c)) { if (c \n) line_num; return c; } else { return EOF; } }这里有两个教学级设计Peek机制当分析器需要“预读下一个字符”来判断是否为还是时如是等于运算符是赋值调用peek_next_char()暂存字符下次get_next_char()返回该值。避免了ungetc()的跨平台兼容性问题。行号自动维护每次读到\nline_num自增所有Token的line_num字段由此获得错误提示能准确定位到第几行。提示实际使用时需将sample.c重定向为标准输入如./word_analysis sample.c否则std::cin会卡在等待用户输入。3. 语法分析器双实现LL(1)与LR(0)在LL.cpp和LR.cpp中的差异落地3.1 LL(1)分析器递归下降 预测分析表驱动LL.cpp实现的是典型的LL(1)分析器核心是预测分析表Parsing Table和递归下降函数的结合。Grammar.txt中定义的文法被手动转换为预测分析表predict_table[NT][T]非终结符×终结符例如E - T E E - T E | ε T - F T T - * F T | ε F - ( E ) | id对应predict_table[E][(] T Epredict_table[E][id] T E。LL.cpp中parse_E()函数逻辑如下void parse_E() { char lookahead get_next_token().token_type; // 获取当前token类型 if (lookahead LPAREN || lookahead IDENTIFIER) { parse_T(); parse_E_prime(); } else { error(expect ( or identifier, got token_to_string(lookahead)); } }关键教学点LL(1)要求文法无左递归、无公共前缀且需满足SELECT集不相交。LL.cpp中build_predict_table()函数手动构建了该表而非动态计算——这正是北邮实验课的设计意图让学生理解SELECT集如何推导而非依赖工具自动生成。3.2 LR(0)分析器状态机驱动的移进-归约LR.cpp实现的是LR(0)分析器核心是DFA状态机和ACTION/GOTO表。LR Grammar.txt中给出的文法如S - S,S - a S b | ε被手动构造出LR(0)项目集规范族再转换为状态转移图。LR.cpp中state_stack和symbol_stack模拟分析栈// ACTION表action[state][terminal] shift 3 / reduce 2 / accept / error // GOTO表goto[state][nonterminal] next_state while (true) { int state state_stack.back(); int token_type current_token.token_type; std::string action action_table[state][token_type]; if (action.substr(0, 6) shift) { int next_state std::stoi(action.substr(7)); state_stack.push_back(next_state); symbol_stack.push_back(current_token); current_token get_next_token(); } else if (action.substr(0, 6) reduce) { int rule_num std::stoi(action.substr(7)); // 执行归约弹出符号栈2*len(rule_rhs)个元素压入lhs查GOTO表 reduce_rule(rule_num); } else if (action accept) break; else error(parsing error at state std::to_string(state)); }血泪经验LR(0)对文法要求宽松允许左递归但ACTION表易冲突。LR.cpp中action_table是硬编码的二维数组共12个状态每个状态对15个终结符ID,NUM,,-,*,/,(,),{,},;,,,,$定义动作。调试时若卡死优先检查current_token是否被重复消费——get_next_token()必须严格保证每次只推进一个token。3.3 语法树构建ASTNode如何从归约动作中生长无论是LL还是LR最终目标都是构建抽象语法树AST。report.md中明确要求输出AST节点。LL.cpp在每个parse_*函数返回时构造ASTNode并返回指针ASTNode* parse_AddExpr() { ASTNode* left parse_MulExpr(); while (current_token.token_type PLUS || current_token.token_type MINUS) { Token op current_token; consume_token(); // 消费或- ASTNode* right parse_MulExpr(); left new BinaryOpNode(op, left, right); // 新建二叉节点 } return left; }LR.cpp则在reduce_rule()中根据产生式右部长度从symbol_stack弹出对应数量的ASTNode*构造新节点并压回栈// reduce S - E // 弹出E节点新建S节点压入 ASTNode* e_node (ASTNode*)symbol_stack.back(); symbol_stack.pop_back(); ASTNode* s_node new NonTerminalNode(S, {e_node}); symbol_stack.push_back(s_node);玄学细节ASTNode基类定义了virtual void print(int indent)用于缩进打印树形结构report.md要求输出格式如[S] → [E] → [AddExpr] → [MulExpr]...。若发现输出乱序大概率是symbol_stack中节点指针未正确转型——LR.cpp中symbol_stack是std::vectorvoid*reduce时需强制static_castASTNode*(...)漏掉则崩溃。4. 避坑指南词法分析器运行时的五个典型翻车现场与修复方案4.1 现象sample.c中0xABC被识别为IDENTIFIER而非NUMBER原因Word_analysis.cpp中十六进制识别逻辑有缺陷。状态11已读0x后若下一个字符是A-F或a-f或0-9应继续跳转但原代码中case 11:分支缺少对A-F的判断仅处理了0-9导致A被当作非法字符状态回退到0后续B、C被当作标识符首字符。解决在case 11:中补充else if (c A c F) { state 11; } // 继续十六进制 else if (c a c f) { state 11; } // 小写十六进制4.2 现象字符串字面量hello\world解析失败报“unclosed string”原因转义字符\处理逻辑缺失。Word_analysis.cpp中状态5字符串内遇到\时应进入状态6转义序列但原代码直接跳回5导致\被当作两个独立字符引号未闭合。解决新增状态6并在case 5:中case : state 4; break; // 字符串结束 case \\: state 6; break; // 进入转义状态 default: value c; break;然后case 6:处理\\,\,\n等合法转义其他字符报错。4.3 现象LR.cpp编译时报错undefined reference to main原因LR.cpp是语法分析器模块不含main()函数不能直接编译运行。它依赖Word_analysis.cpp生成的tokens全局变量需与词法分析器链接。解决必须同时编译两个文件g -o lr_parser Word_analysis.cpp LR.cpp # 而非 g -o lr_parser LR.cpp且确保Word_analysis.cpp中std::vectorToken tokens;声明为extern或合并到同一编译单元。4.4 现象LL.cpp解析if (x 0) y 1;时在处卡死原因LL.cpp的预测分析表未覆盖REL_OP关系运算符终结符。Grammar.txt中E - E REL_OP E产生式要求REL_OP在FIRST集内但predict_table中[E][]为空。解决扩展预测分析表在build_predict_table()中添加predict_table[E][GT] E REL_OP E; // GT对应 predict_table[E][LT] E REL_OP E; // LT对应并确保get_next_token()返回GT/LT类型而非OPERATOR。4.5 现象demo.txt中中文注释// 测试导致词法分析器崩溃原因Word_analysis.cpp假设输入为ASCIIstd::cin.get(c)读取UTF-8中文时一个汉字占3字节c被截断为首个字节如0xE6状态机无法处理陷入死循环。解决教学场景下最简方案是禁止中文在main()开头添加setlocale(LC_ALL, C); // 强制C locale拒绝UTF-8或修改get_next_char()用std::getline读整行再逐字节处理但超出本科实验范围。5. 实验验证与参数调优用test.cpp跑通全流程并定制你的词法规则5.1 四步验证法从输入到AST输出的端到端检查不要只信printf要用test.cpp做闭环验证。该文件是北邮提供的测试驱动器它按顺序执行词法扫描调用Word_analysis.cpp的scan()函数将sample.c转为tokens向量LL分析调用LL.cpp的parse_program()生成AST并输出到ll_ast.txtLR分析调用LR.cpp的lr_parse()生成AST并输出到lr_ast.txt对比校验用diff ll_ast.txt lr_ast.txt检查两棵树结构是否一致忽略节点地址。执行命令g -o test test.cpp Word_analysis.cpp LL.cpp LR.cpp ./test sample.c若ll_ast.txt和lr_ast.txt内容相同说明词法两种语法分析均通过基础验证。注意test.cpp中#include Word_analysis.h需存在若无头文件需将Word_analysis.cpp中struct Token和extern std::vectorToken tokens;提取到Word_analysis.h。5.2 定制词法规则修改Grammar.txt与Word_analysis.cpp的联动技巧想支持C99的//单行注释三步搞定扩展终结符在Grammar.txt的终结符列表追加LINE_COMMENT更新状态机Word_analysis.cpp中state 7/后增加分支case /: // 第二个/ state 8; // 单行注释状态 break;case 8:中循环读直到\n或EOFvalue设为空token_type LINE_COMMENT同步LL/LR文法在LL Grammar.txt和LR Grammar.txt中将LINE_COMMENT加入FOLLOW(S)确保预测表和ACTION表覆盖该终结符。提示每次修改词法规则必须重新运行test.cpp因为LL.cpp和LR.cpp的预测表/动作表是硬编码的不会自动适配新终结符。5.3 性能边界测试sample.c增大10倍后的内存与时间消耗Word_analysis.cpp未做内存优化tokens向量随输入线性增长。用valgrind测试1MB的sample.c生成脚本见report.md附录valgrind --toolmassif ./test big_sample.c结果显示峰值内存约120MB其中tokens占95MB每个Token对象约128B含std::string小字符串优化。后悔药若需处理大文件将std::vectorToken改为std::dequeToken或改用mmap游标式解析避免全量加载。但教学包不追求性能故未实现——这恰是让学生理解“工程权衡”的好案例。从那以后我每次给学生讲词法分析都会先解压这个zip打开Word_analysis.cpp把光标停在case 10:那一行问“如果这里少写一个else if0xG会怎么走” 然后一起编译、输入、看它崩在哪一行。没有比亲手让状态机卡死再修好更能记住DFA的严谨性了。希望帮到你。本文还有配套的精品资源点击获取
返回列表