ARTICLE DETAIL

资讯详情

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

SNL编译器前端实战:词法分析、递归下降与LL1语法分析完整实现

SNL编译器前端实战:词法分析、递归下降与LL1语法分析完整实现 简介这份资源是面向高校计算机专业学生的编译原理课程设计完整源码基于C实现SNL语言的词法分析、递归下降语法分析与LL1语法分析三大核心模块适合正在做课设或希望把编译理论落地为代码的学习者。压缩包共36个文件约1.43MB以9个h头文件与9个cpp源文件为主体分别承载词法扫描、递归下降解析、LL1分析表构建等逻辑另有6个txt测试用例、4个xml配置、2个gif演示动图及py、snl、pro、ui等辅助文件目录按src、res、snl_example等模块划分结构清晰。目前已有767人学习下载。读者可从中获得一套可直接编译运行的SNL编译器实现涵盖Token识别、First集与Follow集计算、LL1分析表构造等关键环节并借助示例输入文件与图形界面代码理解自顶向下解析的完整流程与左递归处理思路为后续编译器设计与优化打下实践基础。1. SNL 编译器前端三件套词法、递归下降与 LL1 到底怎么串起来如果你正在做编译原理课程设计选题是 SNL 语言要求用 C 实现词法分析、递归下降语法分析和 LL1 语法分析那你大概率已经翻过龙书和清华大学出版社第三版的前两章也看过一些零散的实验代码。但真正动手时你会发现网上能搜到的 SNL 实现要么只给词法分析要么语法分析写了一半要么 LL1 分析表是手写的、根本对不上自己的文法。这篇笔记就是把我自己从零搭这套前端时踩过的路重新走一遍SNL 的词法怎么切、递归下降怎么和词法分析器对接、LL1 分析表怎么从文法自动生成而不是手填。适合正在做编译原理实验、需要交课程设计、或者想用 C 把编译器前端跑通的人。读完你至少能拿到一套可编译、可扩展、能对着测试用例逐条验证的代码骨架。2. SNL 词法分析器从字符流到 Token 序列的完整实现2.1 SNL 的 Token 分类与正则定义SNL 语言的词法单元不算多但有几个地方容易翻车。常见做法是先把 Token 分成几大类关键字、标识符、整型常量、字符常量、运算符、界符、注释和空白。关键字包括program、procedure、type、var、if、then、else、while、do、read、write、begin、end、array、record、of、return、integer、char这些。标识符的正则是letter(letter|digit)*整型常量是digit字符常量是单引号包一个字符。运算符和界符要覆盖 - * / : ; , . ( ) [ ] ..其中..是数组下标范围:是赋值是不等这三个双字符符号如果按单字符切就会直接导致语法分析崩掉。我一般会用一个枚举TokenType把所有类型列出来再用一个结构体Token存类型、原始字符串和行号。行号一定要存后面报错定位全靠它。下面是最小可用的词法分析器核心代码#include string #include vector #include cctype #include unordered_map enum class TokenType { KW_PROGRAM, KW_PROCEDURE, KW_TYPE, KW_VAR, KW_IF, KW_THEN, KW_ELSE, KW_WHILE, KW_DO, KW_READ, KW_WRITE, KW_BEGIN, KW_END, KW_ARRAY, KW_RECORD, KW_OF, KW_RETURN, KW_INTEGER, KW_CHAR, ID, INTC, CHARC, PLUS, MINUS, STAR, DIV, LT, LE, GT, GE, EQ, NE, ASSIGN, SEMI, COMMA, DOT, DOTDOT, LPAREN, RPAREN, LBRACK, RBRACK, EOF_TOKEN, ERROR }; struct Token { TokenType type; std::string lexeme; int line; }; class Lexer { public: Lexer(const std::string src) : src_(src), pos_(0), line_(1) { keywords_ { {program, TokenType::KW_PROGRAM}, {procedure, TokenType::KW_PROCEDURE}, {type, TokenType::KW_TYPE}, {var, TokenType::KW_VAR}, {if, TokenType::KW_IF}, {then, TokenType::KW_THEN}, {else, TokenType::KW_ELSE}, {while, TokenType::KW_WHILE}, {do, TokenType::KW_DO}, {read, TokenType::KW_READ}, {write, TokenType::KW_WRITE}, {begin, TokenType::KW_BEGIN}, {end, TokenType::KW_END}, {array, TokenType::KW_ARRAY}, {record, TokenType::KW_RECORD}, {of, TokenType::KW_OF}, {return, TokenType::KW_RETURN}, {integer, TokenType::KW_INTEGER}, {char, TokenType::KW_CHAR} }; } std::vectorToken tokenize() { std::vectorToken tokens; while (pos_ src_.size()) { char c src_[pos_]; if (std::isspace(c)) { if (c \n) line_; pos_; continue; } if (std::isalpha(c)) { tokens.push_back(readIdentifierOrKeyword()); continue; } if (std::isdigit(c)) { tokens.push_back(readInteger()); continue; } if (c \) { tokens.push_back(readCharConst()); continue; } tokens.push_back(readOperatorOrDelimiter()); } tokens.push_back({TokenType::EOF_TOKEN, , line_}); return tokens; } private: std::string src_; size_t pos_; int line_; std::unordered_mapstd::string, TokenType keywords_; Token readIdentifierOrKeyword() { size_t start pos_; while (pos_ src_.size() (std::isalnum(src_[pos_]) || src_[pos_] _)) pos_; std::string word src_.substr(start, pos_ - start); auto it keywords_.find(word); if (it ! keywords_.end()) return {it-second, word, line_}; return {TokenType::ID, word, line_}; } Token readInteger() { size_t start pos_; while (pos_ src_.size() std::isdigit(src_[pos_])) pos_; return {TokenType::INTC, src_.substr(start, pos_ - start), line_}; } Token readCharConst() { pos_; // skip opening quote if (pos_ src_.size()) return {TokenType::ERROR, unterminated char, line_}; std::string val(1, src_[pos_]); pos_; if (pos_ src_.size() src_[pos_] \) pos_; else return {TokenType::ERROR, unterminated char, line_}; return {TokenType::CHARC, val, line_}; } Token readOperatorOrDelimiter() { char c src_[pos_]; auto peek [](size_t off) - char { return (pos_ off src_.size()) ? src_[pos_ off] : \0; }; switch (c) { case : pos_; return {TokenType::PLUS, , line_}; case -: pos_; return {TokenType::MINUS, -, line_}; case *: pos_; return {TokenType::STAR, *, line_}; case /: pos_; return {TokenType::DIV, /, line_}; case : if (peek(1) ) { pos_ 2; return {TokenType::LE, , line_}; } if (peek(1) ) { pos_ 2; return {TokenType::NE, , line_}; } pos_; return {TokenType::LT, , line_}; case : if (peek(1) ) { pos_ 2; return {TokenType::GE, , line_}; } pos_; return {TokenType::GT, , line_}; case : pos_; return {TokenType::EQ, , line_}; case :: if (peek(1) ) { pos_ 2; return {TokenType::ASSIGN, :, line_}; } pos_; return {TokenType::ERROR, :, line_}; case ;: pos_; return {TokenType::SEMI, ;, line_}; case ,: pos_; return {TokenType::COMMA, ,, line_}; case .: if (peek(1) .) { pos_ 2; return {TokenType::DOTDOT, .., line_}; } pos_; return {TokenType::DOT, ., line_}; case (: pos_; return {TokenType::LPAREN, (, line_}; case ): pos_; return {TokenType::RPAREN, ), line_}; case [: pos_; return {TokenType::LBRACK, [, line_}; case ]: pos_; return {TokenType::RBRACK, ], line_}; default: pos_; return {TokenType::ERROR, std::string(1, c), line_}; } } };这段代码的逻辑很直白主循环按字符类型分派字母开头走标识符/关键字数字开头走整型常量单引号走字符常量其余走运算符和界符。关键字用哈希表查比一长串 if-else 干净得多。readOperatorOrDelimiter里对、、:、.做了双字符前瞻这是 SNL 词法里最容易漏的地方。参数方面line_在遇到换行时自增每个 Token 都带行号后面语法分析报错时可以直接指出第几行。如果你用 Dev C 或 VS Code 配 C 环境这段代码直接扔进一个.cpp就能编译不需要额外依赖。2.2 注释处理与错误恢复SNL 的注释是/* ... */形式可以跨行。很多课程设计的词法分析器一遇到注释就死循环原因是在主循环里没有跳过注释体。正确做法是在tokenize()的空白判断之后加一个注释分支如果当前字符是/且下一个是*就进入注释跳过逻辑一直读到*/为止期间遇到换行要维护line_。如果读到文件尾还没遇到*/就产生一个 ERROR Token 并记录行号。错误恢复方面词法阶段最常见的错误是非法字符和未闭合的字符常量。我的习惯是非法字符不直接抛异常而是生成一个ERRORToken 塞进序列让后续语法分析决定怎么报。这样一次编译能报出多个错误而不是遇到第一个就退出。字符常量未闭合同理生成 ERROR Token 后把位置推到行尾或文件尾避免死循环。这里有个血泪经验如果你在readCharConst里遇到未闭合时只pos_而不跳过剩余内容下一个循环还会读到同一个单引号直接无限循环。所以要么跳到行尾要么跳到下一个单引号。2.3 用测试用例验证词法输出写完词法分析器后别急着写语法分析。先拿一段 SNL 源码跑一遍把 Token 序列打印出来看。我一般会准备三个测试文件一个只含关键字和标识符的声明段一个含各种运算符和界符的表达式段一个含注释和字符常量的混合段。打印格式用行号: 类型 词素一眼就能看出哪里切错了。int main() { std::string src R( program p var x, y: integer; begin x : 10; y : x 20 * 3; if x y then write(x) else write(y) end. ); Lexer lexer(src); auto tokens lexer.tokenize(); for (auto t : tokens) { std::cout t.line : static_castint(t.type) t.lexeme \n; } return 0; }跑完之后重点检查三处:有没有被切成:和..有没有被切成两个.有没有被切成和。如果这三处对了词法分析器基本就稳了。另外注意end.里的点号SNL 程序以end.结尾点号是单独的 DOT Token不能和end粘在一起。3. 递归下降语法分析把 SNL 文法直接翻译成 C 函数3.1 SNL 文法子集与递归下降的对应关系递归下降的核心思想是文法里每个非终结符对应一个函数函数体按照产生式右部的顺序调用其他函数或匹配终结符。SNL 的文法不算大但产生式不少。我一般会先整理出一个适合递归下降的子集去掉左递归和公共左因子。比如表达式部分原始文法可能是E - E T | T直接写会无限递归必须改写成E - T EE - T E | ε。SNL 里的语句序列、声明序列也有类似问题常见做法是用while循环处理重复部分而不是硬套右递归。下面这张表是我整理的非终结符和对应函数的映射写代码前先对着表把函数签名列出来能省很多返工非终结符对应函数主要产生式ProgramparseProgramprogram ID DeclareList begin StmtList end .DeclareListparseDeclareListtype/var 声明序列StmtListparseStmtList语句序列StmtparseStmt赋值/if/while/read/writeExpparseExp表达式TermparseTerm项FactorparseFactor因子3.2 递归下降分析器的代码骨架递归下降分析器需要一个 Token 流和一个当前位置指针。我一般封装一个Parser类持有std::vectorToken和size_t pos_提供peek()、advance()、match()、expect()四个基础操作。match用于尝试匹配某个类型成功就前进并返回 trueexpect用于必须匹配的场景失败就报错并记录行号。class Parser { public: Parser(std::vectorToken tokens) : tokens_(std::move(tokens)), pos_(0) {} void parse() { parseProgram(); if (peek().type ! TokenType::EOF_TOKEN) { error(unexpected token after program end); } } private: std::vectorToken tokens_; size_t pos_; Token peek() const { return tokens_[pos_]; } Token advance() { return tokens_[pos_]; } bool match(TokenType t) { if (peek().type t) { pos_; return true; } return false; } Token expect(TokenType t, const std::string what) { if (peek().type t) return advance(); error(expected what but got peek().lexeme ); return peek(); } void error(const std::string msg) { std::cerr Line peek().line : msg \n; // 简单恢复跳到分号或 end while (peek().type ! TokenType::SEMI peek().type ! TokenType::KW_END peek().type ! TokenType::EOF_TOKEN) { pos_; } if (peek().type TokenType::SEMI) pos_; } void parseProgram() { expect(TokenType::KW_PROGRAM, program); expect(TokenType::ID, program name); parseDeclareList(); expect(TokenType::KW_BEGIN, begin); parseStmtList(); expect(TokenType::KW_END, end); expect(TokenType::DOT, .); } void parseDeclareList() { while (peek().type TokenType::KW_TYPE || peek().type TokenType::KW_VAR) { if (match(TokenType::KW_TYPE)) parseTypeDecl(); else if (match(TokenType::KW_VAR)) parseVarDecl(); } } void parseTypeDecl() { // type ID ... ; expect(TokenType::ID, type name); expect(TokenType::EQ, ); // 简化处理跳过到分号 while (peek().type ! TokenType::SEMI peek().type ! TokenType::EOF_TOKEN) pos_; expect(TokenType::SEMI, ;); } void parseVarDecl() { // var IDList : Type ; do { expect(TokenType::ID, variable name); } while (match(TokenType::COMMA)); expect(TokenType::EQ, :); // 注意SNL 里冒号单独出现时词法可能报错 // 类型名 if (peek().type TokenType::KW_INTEGER || peek().type TokenType::KW_CHAR) { advance(); } else { expect(TokenType::ID, type name); } expect(TokenType::SEMI, ;); } void parseStmtList() { while (peek().type ! TokenType::KW_END peek().type ! TokenType::EOF_TOKEN) { parseStmt(); } } void parseStmt() { switch (peek().type) { case TokenType::ID: { advance(); if (match(TokenType::ASSIGN)) { parseExp(); } else if (peek().type TokenType::LPAREN) { // 过程调用 advance(); if (peek().type ! TokenType::RPAREN) { parseExp(); while (match(TokenType::COMMA)) parseExp(); } expect(TokenType::RPAREN, )); } expect(TokenType::SEMI, ;); break; } case TokenType::KW_IF: { advance(); parseExp(); expect(TokenType::KW_THEN, then); parseStmt(); if (match(TokenType::KW_ELSE)) parseStmt(); break; } case TokenType::KW_WHILE: { advance(); parseExp(); expect(TokenType::KW_DO, do); parseStmt(); break; } case TokenType::KW_READ: { advance(); expect(TokenType::LPAREN, (); expect(TokenType::ID, variable); expect(TokenType::RPAREN, )); expect(TokenType::SEMI, ;); break; } case TokenType::KW_WRITE: { advance(); expect(TokenType::LPAREN, (); parseExp(); expect(TokenType::RPAREN, )); expect(TokenType::SEMI, ;); break; } default: error(unexpected token in statement); } } void parseExp() { parseTerm(); while (peek().type TokenType::PLUS || peek().type TokenType::MINUS) { advance(); parseTerm(); } } void parseTerm() { parseFactor(); while (peek().type TokenType::STAR || peek().type TokenType::DIV) { advance(); parseFactor(); } } void parseFactor() { switch (peek().type) { case TokenType::ID: case TokenType::INTC: case TokenType::CHARC: advance(); break; case TokenType::LPAREN: advance(); parseExp(); expect(TokenType::RPAREN, )); break; default: error(unexpected token in factor); } } };这段代码里parseVarDecl有个细节SNL 的变量声明是var x, y: integer;冒号在词法阶段如果单独出现会报 ERROR因为我在词法里只把:识别为 ASSIGN单独的:返回 ERROR。所以这里expect(TokenType::EQ, :)其实是有问题的正确做法是在词法里加一个 COLON 类型或者把变量声明的冒号也识别出来。这是我在实际写的时候翻过的一个车后来在词法里补了case :: if (peek(1) ) ... else return {TokenType::COLON, :, line_};才解决。如果你照着写记得先把 COLON 加上。3.3 递归下降的报错与恢复策略递归下降的报错策略直接影响课程设计的演示效果。我的习惯是expect失败时打印行号和期望的 Token 类型然后调用error做简单恢复。恢复策略是跳到下一个分号或end这样一次能报多个错误。但要注意如果错误发生在表达式内部跳到分号可能会把整个语句吞掉导致后续语句解析错位。更稳的做法是在parseStmt层面做恢复表达式内部出错就抛一个异常由parseStmt捕获后跳到分号。不过课程设计一般不需要工业级恢复简单跳到分号就够用了。另外递归下降对左递归文法是天然不支持的如果你拿到的 SNL 文法里有A - A α | β这种形式必须先消除左递归。消除方法是引入新非终结符A改成A - β AA - α A | ε。这一步如果偷懒不做程序一跑就栈溢出而且报错信息很难看。4. LL1 语法分析从 FIRST/FOLLOW 集到分析表自动生成4.1 FIRST 集和 FOLLOW 集的计算LL1 分析的核心是预测分析表而预测分析表依赖 FIRST 集和 FOLLOW 集。FIRST(α) 是从 α 能推导出的所有终结符开头的集合如果 α 能推导出 ε则 ε 也在 FIRST(α) 中。FOLLOW(A) 是紧跟在 A 后面的终结符集合如果 A 是开始符号则$在 FOLLOW(A) 中。计算 FIRST 集的算法是迭代到不动点对于每个产生式A - X1 X2 ... Xn先把 FIRST(X1) 中非 ε 的元素加入 FIRST(A)如果 X1 能推导出 ε就继续看 X2以此类推。如果所有 Xi 都能推导出 ε则 ε 加入 FIRST(A)。FOLLOW 集的计算类似对于产生式A - α B β把 FIRST(β) 中非 ε 的元素加入 FOLLOW(B)如果 β 能推导出 ε则把 FOLLOW(A) 加入 FOLLOW(B)。我一般用std::setstd::string存 FIRST 和 FOLLOW用std::mapstd::string, std::vectorstd::vectorstd::string存文法产生式。下面是一个计算 FIRST 集的核心片段using Symbol std::string; using Production std::vectorSymbol; using Grammar std::mapSymbol, std::vectorProduction; std::mapSymbol, std::setSymbol computeFirst(const Grammar g, const std::setSymbol terminals) { std::mapSymbol, std::setSymbol first; // 终结符的 FIRST 是自身 for (auto t : terminals) first[t] {t}; // 非终结符初始为空 for (auto [nt, _] : g) first[nt] {}; bool changed true; while (changed) { changed false; for (auto [lhs, prods] : g) { for (auto prod : prods) { bool allNullable true; for (auto sym : prod) { for (auto f : first[sym]) { if (f ! ε first[lhs].insert(f).second) changed true; } if (first[sym].count(ε) 0) { allNullable false; break; } } if (allNullable first[lhs].insert(ε).second) changed true; } } } return first; }这段代码的关键点是allNullable标志只有当前面所有符号都能推导出 ε 时才继续看下一个符号的 FIRST 集。如果中途某个符号不能推导出 ε就停止后面的符号不再影响当前产生式的 FIRST 集。changed标志控制迭代直到某一轮没有任何新元素加入才停止。参数方面terminals集合需要提前定义好SNL 的终结符就是词法分析器里那些 Token 类型对应的字符串。4.2 预测分析表的构造与冲突处理有了 FIRST 和 FOLLOW预测分析表M[A, a]的构造规则是对每个产生式A - α对 FIRST(α) 中每个终结符 a把A - α填入M[A, a]如果 ε 在 FIRST(α) 中则对 FOLLOW(A) 中每个终结符 b把A - α填入M[A, b]。如果同一个格子被填入两个不同产生式就说明文法不是 LL1 的存在冲突。SNL 文法里常见的冲突点有两个一个是if-then-else的悬挂 else 问题另一个是表达式里和-的优先级。悬挂 else 在 LL1 里表现为M[Stmt, else]可能同时有if Exp then Stmt和if Exp then Stmt else Stmt两个候选。解决办法是改写文法把Stmt拆成MatchedStmt和UnmatchedStmt或者规定 else 总是匹配最近的 if。表达式优先级冲突则通过分层文法解决把Exp、Term、Factor分开每层只处理一种优先级。下面这张表是 SNL 表达式部分的预测分析表片段展示Exp、Term、Factor在遇到不同输入时的选择非终结符IDINTC(*);ExpExp-Term ExpExp-Term ExpExp-Term ExpExpExp-Term ExpExp-εExp-εTermTerm-Factor TermTerm-Factor TermTerm-Factor TermTermTerm-εTerm-*Factor TermTerm-εTerm-εFactorFactor-IDFactor-INTCFactor-(Exp)这张表是手写出来做对照的实际代码里应该从文法自动生成。生成之后拿它和手写表对比如果一致说明 FIRST/FOLLOW 计算正确如果不一致优先检查 ε 产生式的处理。4.3 用栈驱动 LL1 分析器跑通测试用例LL1 分析器是一个下推自动机栈里初始放$和开始符号输入缓冲区放 Token 序列加$。每一步看栈顶和当前输入符号查预测分析表决定动作。如果栈顶是终结符且和输入匹配就弹出并前进如果栈顶是非终结符就查表把产生式右部逆序压栈如果查表为空就报错。void ll1Parse(const Grammar g, const std::mapstd::pairSymbol, Symbol, Production table, const std::vectorSymbol input, const Symbol start) { std::vectorSymbol stack; stack.push_back($); stack.push_back(start); size_t ip 0; while (!stack.empty()) { Symbol top stack.back(); Symbol cur input[ip]; if (top $ cur $) { std::cout Accept\n; return; } if (top cur) { stack.pop_back(); ip; continue; } auto key std::make_pair(top, cur); auto it table.find(key); if (it table.end()) { std::cerr Error at input symbol: cur \n; return; } stack.pop_back(); const Production prod it-second; // 逆序压栈跳过 ε for (auto rit prod.rbegin(); rit ! prod.rend(); rit) { if (*rit ! ε) stack.push_back(*rit); } } }这段代码里input是 Token 类型对应的字符串序列末尾要手动加$。table的键是(栈顶非终结符, 当前输入符号)值是产生式右部。压栈时逆序是因为栈是后进先出产生式右部第一个符号应该最先被处理。如果产生式是A - ε右部为空直接弹出栈顶即可不需要压任何东西。跑测试用例时重点看if-then-else和嵌套表达式这两处最容易暴露分析表冲突。5. 避坑与排查SNL 前端实现里最容易翻车的五个地方5.1 词法阶段把:切成两个 Token现象语法分析报错提示在赋值语句处遇到:而不是:。原因词法分析器在读到:时没有前瞻下一个字符直接返回了 COLON 或 ERROR。解决在readOperatorOrDelimiter里对:做双字符判断如果下一个是就返回 ASSIGN否则返回 COLON。同理、、.都要做前瞻。5.2 递归下降遇到左递归直接栈溢出现象程序一跑就崩溃调用栈里全是同一个函数。原因文法里有直接左递归或间接左递归递归下降无法处理。解决先消除左递归把A - A α | β改成A - β AA - α A | ε。消除之后再用循环处理重复部分比如while (match(PLUS)) { parseTerm(); }。5.3 FIRST/FOLLOW 集迭代不收敛现象计算 FIRST 集的while循环停不下来或者结果明显不对。原因changed标志没有在正确的地方更新或者 ε 产生式的处理逻辑有误。解决每次插入新元素时检查insert的返回值只有真正插入了才置changed true。另外终结符的 FIRST 集要初始化为自身非终结符初始为空不能搞反。5.4 预测分析表出现多重入口现象LL1 分析器在某个输入符号上查表查到两个产生式不知道选哪个。原因文法不是 LL1 的存在公共左因子或二义性。解决提取公共左因子把A - α β | α γ改成A - α AA - β | γ。如果是悬挂 else改写文法或规定 else 匹配最近的 if。改完之后重新计算 FIRST/FOLLOW重新生成分析表。5.5 行号在注释和字符串里丢失现象报错行号不对明明错误在第 10 行却报第 8 行。原因词法分析器在跳过注释和字符常量时没有维护line_。解决在注释跳过逻辑里每遇到\n就line_在字符常量读取里如果字符是\n也要处理。另外\r\n换行要统一处理只认\n自增避免 Windows 和 Linux 换行符差异导致行号偏移。6. 把三件套串成可演示的课程设计验证方法与扩展技巧课程设计最后要演示演示的核心是让老师看到你的词法、递归下降、LL1 三条路径都能跑通而且结果一致。我的习惯是写一个统一的测试驱动读入同一个 SNL 源文件先跑词法分析打印 Token 序列再跑递归下降打印语法树或至少打印「Parse OK」最后跑 LL1 打印每一步的栈和输入。三条路径的输出对得上基本就能拿高分。验证方法上我一般准备三组用例第一组是合法程序覆盖声明、赋值、if-else、while、read、write第二组是词法错误比如非法字符、未闭合字符常量第三组是语法错误比如缺少分号、括号不匹配。每组用例都要检查报错行号是否正确这是老师最容易挑毛病的地方。扩展技巧方面如果你想让课程设计更出彩可以在递归下降里加一个简单的符号表记录变量名和类型这样在赋值时能检查类型是否匹配。LL1 那边可以把预测分析表的生成过程打印出来展示 FIRST 和 FOLLOW 的计算结果老师一看就知道你是真懂了而不是抄的。另外把 Token 类型和文法符号用同一套字符串表示能省掉很多转换代码LL1 分析器直接吃词法输出的字符串序列就行。我自己做这套东西的时候最大的教训是别一上来就写 LL1先把词法和递归下降跑通拿到正确的 Token 序列和语法树再去搞 FIRST/FOLLOW。因为 LL1 的分析表如果对不上你根本分不清是文法写错了还是 FIRST 算错了。先有递归下降做对照LL1 出问题的时候至少有个参照。希望帮到你。本文还有配套的精品资源点击获取
返回列表