
简介这份资源是面向计算机专业学生与编译原理学习者的Pascal文法编译器课程设计完整实现围绕词法分析、语法分析、语义检查与代码生成等核心环节展开适合正在做课程设计或希望动手理解编译器构造流程的中高级学习者。压缩包共140个文件约8.18MB以cpp、h、c等源码文件为主体配合cmake、make、makefile等构建脚本另有txt、md说明文档、pptx演示材料及exe可执行文件覆盖从源码到可运行程序的完整链路。资源中实现了if条件判断、while循环、类型定义、过程与函数调用以及嵌套定义等扩展功能并包含符号表、抽象语法树与目标汇编输出等模块可帮助读者对照Pascal文法梳理词法规则、上下文无关文法与语义处理思路。目前已有288人学习下载适合作为课程设计参考与编译器入门实践素材。1. 从一段 Pascal 代码到可执行文件编译器到底在做什么很多人第一次接触「基于 Pascal 文法的编译器」脑子里浮现的是龙书里那套晦涩的自动机理论觉得这东西离实际工程很远。但真实情况恰恰相反你每天用的 JSON 解析、SQL 执行计划、甚至 VS Code 里的语法高亮底层都是同一套「词法分析 → 语法分析 → 语义检查 → 中间代码 → 目标代码」的流水线。Pascal 之所以是编译器入门的经典载体是因为它的文法足够规整——没有 C 语言那种声明与表达式纠缠的歧义也没有 Python 缩进带来的词法层复杂度用它来跑通一条完整的编译链路你能把注意力放在「文法怎么驱动代码生成」这件事本身而不是被语言特性拖住。这篇文章面向两类人一类是想从零手写一个能跑通的最小编译器、但被各种理论书劝退的开发者另一类是想把「文法」这个抽象概念落到具体代码上、搞清楚递归下降和 LL(1) 到底怎么配合的工程师。我会用 Pascal 的一个子集作为输入语言带你走完从文法定义到生成可执行结果的全过程中间该踩的坑、该调的参数、该看的中间产物一个都不省。读完你至少能自己扩展出一门小语言的编译器骨架而不是停留在「知道有这回事」。2. 用 Pascal 文法定义一门可编译的子集语言2.1 为什么选 Pascal 子集而不是完整 Pascal完整 Pascal 的文法体量不小光标准类型就有整型、实型、字符、布尔、枚举、子界、集合、数组、记录、文件、指针再加上过程与函数的嵌套定义、forward声明、with语句一套下来词法规则上百条语法产生式几百条。对第一次写编译器的人来说这不是难度问题是耐心问题——你会在处理with语句的作用域嵌套时崩溃而不是在理解编译原理时崩溃。我的做法是砍到一个「能表达完整计算逻辑」的最小子集保留这些构造整型和布尔类型变量声明与赋值if-then-else和while-dowrite输出语句四则运算与比较运算复合语句begin...end砍掉的东西数组、记录、指针、过程与函数、case、repeat、for。这些不是不重要而是它们每一个都会引入新的语义分析维度比如数组要处理下标越界检查、函数要处理调用约定在你还没把主流程跑通之前加进来只会让调试变成玄学。这个子集的文法用 EBNF 写出来大概是这样program program ident ; block . ; block declPart compoundStmt ; declPart { var ident { , ident } : type ; } ; type integer | boolean ; compoundStmt begin stmtList end ; stmtList stmt { ; stmt } ; stmt assignStmt | ifStmt | whileStmt | compoundStmt | writeStmt | empty ; assignStmt ident : expr ; ifStmt if expr then stmt [ else stmt ] ; whileStmt while expr do stmt ; writeStmt write ( expr ) ; expr simpleExpr [ relOp simpleExpr ] ; simpleExpr term { addOp term } ; term factor { mulOp factor } ; factor ident | number | ( expr ) | true | false ; relOp | | | | | ; addOp | - | or ; mulOp * | / | div | mod | and ;这份文法有一个关键性质它是 LL(1) 的。也就是说对每个非终结符你只需要看下一个 token 就能决定用哪条产生式不需要回溯。这是递归下降分析器能直接手写的前提。如果你选的文法不是 LL(1)比如把stmt的产生式写成ident : expr | ident ( args )那遇到ident开头的语句你就得往前看两个 token 才能区分赋值和过程调用——这时候要么改写文法做左公因子提取要么上回溯要么换 LL(k)。我一般会在一开始就把文法调成 LL(1)省得后面改分析器改到怀疑人生。2.2 词法分析器把字符流切成 token 流词法分析是整个编译器的入口它的职责很单一吃字符吐 token。但这里有个新手常翻车的地方——把词法分析和语法分析混在一起写边读字符边判断语法结构。这样写出来的代码在遇到:和:这种前缀重叠的符号时会非常难受而且没法复用。正确的做法是词法分析器独立成一个模块对外只暴露一个nextToken()接口。下面是一个用 Python 实现的词法分析器核心部分import re # token 类型常量 INTEGER, BOOLEAN, IDENT, NUMBER INTEGER, BOOLEAN, IDENT, NUMBER PLUS, MINUS, MUL, DIV, MOD PLUS, MINUS, MUL, DIV, MOD ASSIGN, EQ, NEQ, LT, LE, GT, GE ASSIGN, EQ, NEQ, LT, LE, GT, GE LPAREN, RPAREN, SEMI, COLON, COMMA, DOT LPAREN, RPAREN, SEMI, COLON, COMMA, DOT PROGRAM, VAR, BEGIN, END, IF, THEN, ELSE, WHILE, DO, WRITE ( PROGRAM, VAR, BEGIN, END, IF, THEN, ELSE, WHILE, DO, WRITE) TRUE, FALSE, AND, OR, NOT TRUE, FALSE, AND, OR, NOT EOF EOF KEYWORDS { program: PROGRAM, var: VAR, begin: BEGIN, end: END, if: IF, then: THEN, else: ELSE, while: WHILE, do: DO, write: WRITE, integer: INTEGER, boolean: BOOLEAN, true: TRUE, false: FALSE, and: AND, or: OR, not: NOT, } class Token: def __init__(self, type_, value, line, col): self.type type_ self.value value self.line line self.col col def __repr__(self): return fToken({self.type}, {self.value!r}, L{self.line}:C{self.col}) class Lexer: def __init__(self, text): self.text text self.pos 0 self.line 1 self.col 1 def error(self, msg): raise SyntaxError(f词法错误 L{self.line}:C{self.col}: {msg}) def peek(self): return self.text[self.pos] if self.pos len(self.text) else None def advance(self): ch self.text[self.pos] self.pos 1 if ch \n: self.line 1 self.col 1 else: self.col 1 return ch def skip_whitespace_and_comments(self): # Pascal 注释用 { } 或 (* *)这里只处理 { } while self.pos len(self.text): ch self.peek() if ch.isspace(): self.advance() elif ch {: while self.pos len(self.text) and self.peek() ! }: self.advance() if self.pos len(self.text): self.error(注释未闭合) self.advance() # 吃掉 } else: break def next_token(self): self.skip_whitespace_and_comments() if self.pos len(self.text): return Token(EOF, None, self.line, self.col) ch self.peek() start_line, start_col self.line, self.col # 标识符或关键字 if ch.isalpha() or ch _: buf [] while self.pos len(self.text) and (self.peek().isalnum() or self.peek() _): buf.append(self.advance()) word .join(buf) ttype KEYWORDS.get(word.lower(), IDENT) return Token(ttype, word, start_line, start_col) # 数字 if ch.isdigit(): buf [] while self.pos len(self.text) and self.peek().isdigit(): buf.append(self.advance()) return Token(NUMBER, int(.join(buf)), start_line, start_col) # 双字符运算符必须先于单字符判断 two self.text[self.pos:self.pos2] if two :: self.advance(); self.advance() return Token(ASSIGN, :, start_line, start_col) if two : self.advance(); self.advance() return Token(NEQ, , start_line, start_col) if two : self.advance(); self.advance() return Token(LE, , start_line, start_col) if two : self.advance(); self.advance() return Token(GE, , start_line, start_col) # 单字符 single_map { : PLUS, -: MINUS, *: MUL, /: DIV, : EQ, : LT, : GT, (: LPAREN, ): RPAREN, ;: SEMI, :: COLON, ,: COMMA, .: DOT, } if ch in single_map: self.advance() return Token(single_map[ch], ch, start_line, start_col) self.error(f无法识别的字符 {ch!r})这段代码有几个参数和逻辑值得说清楚。KEYWORDS字典把关键字映射到 token 类型注意我用word.lower()做匹配这样 Pascal 的大小写不敏感特性就自然支持了。双字符运算符的判断必须放在单字符之前否则:会被拆成:和两个 token语法分析器拿到之后会一脸懵。skip_whitespace_and_comments里对未闭合注释做了报错这是新手最容易忽略的边界——如果注释没闭合while循环会一直读到文件尾然后self.peek()返回NoneNone ! }永远成立死循环。我加了self.pos len(self.text)的判断来兜底。Token里带line和col不是装饰是后面报错定位的命根子。你写编译器写到后面最痛苦的不是逻辑错是「语法错误」四个字后面什么都没有。有了行列号报错能精确到L3:C12调试效率差一个数量级。2.3 递归下降语法分析器把 token 流变成语法树有了 token 流接下来是语法分析。递归下降的核心思想是文法里每个非终结符对应一个函数函数内部按照产生式的右部依次调用其他函数或匹配 token。因为我们的文法是 LL(1) 的每个函数只需要看当前 token 就能决定走哪条分支。先定义 AST 节点class ASTNode: pass class Program(ASTNode): def __init__(self, name, block): self.name name self.block block class Block(ASTNode): def __init__(self, decls, body): self.decls decls self.body body class VarDecl(ASTNode): def __init__(self, names, type_name): self.names names self.type_name type_name class Compound(ASTNode): def __init__(self, stmts): self.stmts stmts class Assign(ASTNode): def __init__(self, name, expr): self.name name self.expr expr class If(ASTNode): def __init__(self, cond, then_branch, else_branchNone): self.cond cond self.then_branch then_branch self.else_branch else_branch class While(ASTNode): def __init__(self, cond, body): self.cond cond self.body body class Write(ASTNode): def __init__(self, expr): self.expr expr class BinOp(ASTNode): def __init__(self, op, left, right): self.op op self.left left self.right right class UnaryOp(ASTNode): def __init__(self, op, operand): self.op op self.operand operand class Num(ASTNode): def __init__(self, value): self.value value class Bool(ASTNode): def __init__(self, value): self.value value class Var(ASTNode): def __init__(self, name): self.name name class NoOp(ASTNode): pass然后是分析器本体class Parser: def __init__(self, lexer): self.lexer lexer self.current self.lexer.next_token() def error(self, msg): raise SyntaxError(f语法错误 L{self.current.line}:C{self.current.col}: {msg}当前 token{self.current.type}) def eat(self, token_type): if self.current.type token_type: tok self.current self.current self.lexer.next_token() return tok self.error(f期望 {token_type}) def parse(self): self.eat(PROGRAM) name self.eat(IDENT).value block self.parse_block() self.eat(DOT) self.eat(EOF) return Program(name, block) def parse_block(self): decls self.parse_decl_part() body self.parse_compound() return Block(decls, body) def parse_decl_part(self): decls [] while self.current.type VAR: self.eat(VAR) names [self.eat(IDENT).value] while self.current.type COMMA: self.eat(COMMA) names.append(self.eat(IDENT).value) self.eat(COLON) type_name self.current.value if self.current.type not in (INTEGER, BOOLEAN): self.error(类型只能是 integer 或 boolean) self.eat(self.current.type) self.eat(SEMI) decls.append(VarDecl(names, type_name)) return decls def parse_compound(self): self.eat(BEGIN) stmts self.parse_stmt_list() self.eat(END) return Compound(stmts) def parse_stmt_list(self): stmts [self.parse_stmt()] while self.current.type SEMI: self.eat(SEMI) stmts.append(self.parse_stmt()) return stmts def parse_stmt(self): t self.current.type if t IDENT: name self.eat(IDENT).value self.eat(ASSIGN) expr self.parse_expr() return Assign(name, expr) elif t IF: self.eat(IF) cond self.parse_expr() self.eat(THEN) then_branch self.parse_stmt() else_branch None if self.current.type ELSE: self.eat(ELSE) else_branch self.parse_stmt() return If(cond, then_branch, else_branch) elif t WHILE: self.eat(WHILE) cond self.parse_expr() self.eat(DO) body self.parse_stmt() return While(cond, body) elif t BEGIN: return self.parse_compound() elif t WRITE: self.eat(WRITE) self.eat(LPAREN) expr self.parse_expr() self.eat(RPAREN) return Write(expr) else: return NoOp() def parse_expr(self): left self.parse_simple_expr() if self.current.type in (EQ, NEQ, LT, LE, GT, GE): op self.current.type self.eat(op) right self.parse_simple_expr() return BinOp(op, left, right) return left def parse_simple_expr(self): node self.parse_term() while self.current.type in (PLUS, MINUS, OR): op self.current.type self.eat(op) right self.parse_term() node BinOp(op, node, right) return node def parse_term(self): node self.parse_factor() while self.current.type in (MUL, DIV, MOD, AND): op self.current.type self.eat(op) right self.parse_factor() node BinOp(op, node, right) return node def parse_factor(self): t self.current.type if t NUMBER: return Num(self.eat(NUMBER).value) elif t TRUE: self.eat(TRUE) return Bool(True) elif t FALSE: self.eat(FALSE) return Bool(False) elif t IDENT: return Var(self.eat(IDENT).value) elif t LPAREN: self.eat(LPAREN) node self.parse_expr() self.eat(RPAREN) return node elif t MINUS: self.eat(MINUS) return UnaryOp(MINUS, self.parse_factor()) elif t NOT: self.eat(NOT) return UnaryOp(NOT, self.parse_factor()) self.error(期望表达式)这里的关键设计是eat函数——它既做匹配又做前进是整个分析器的原子操作。parse_expr处理比较运算parse_simple_expr处理加减parse_term处理乘除parse_factor处理括号和原子。这个分层不是随便定的它直接对应文法里的优先级层次比较 加减 乘除 原子。如果你把优先级搞反了1 2 * 3会被解析成(1 2) * 3结果从 7 变成 9这种 bug 在测试用例少的时候根本发现不了。parse_stmt里对NoOp的处理是空语句对应文法里的empty。Pascal 允许begin end里什么都不写也允许;;这种连续分号所以空语句是必要的。2.4 语义分析与符号表变量必须先声明后使用语法树建好之后编译器需要做语义检查。对 Pascal 子集来说核心检查就三件事变量是否声明、变量是否重复声明、类型是否匹配。这三件事都依赖符号表。class SymbolTable: def __init__(self): self.scopes [{}] # 栈式作用域当前子集只有全局作用域 def declare(self, name, type_name, lineNone): scope self.scopes[-1] if name in scope: raise NameError(f变量 {name} 重复声明) scope[name] type_name def lookup(self, name): for scope in reversed(self.scopes): if name in scope: return scope[name] raise NameError(f变量 {name} 未声明) class SemanticAnalyzer: def __init__(self): self.symtab SymbolTable() def analyze(self, node): method visit_ type(node).__name__ visitor getattr(self, method, self.generic_visit) return visitor(node) def generic_visit(self, node): raise NotImplementedError(f没有为 {type(node).__name__} 定义 visit 方法) def visit_Program(self, node): self.analyze(node.block) def visit_Block(self, node): for decl in node.decls: self.analyze(decl) self.analyze(node.body) def visit_VarDecl(self, node): for name in node.names: self.symtab.declare(name, node.type_name) def visit_Compound(self, node): for stmt in node.stmts: self.analyze(stmt) def visit_Assign(self, node): var_type self.symtab.lookup(node.name) expr_type self.analyze(node.expr) if var_type ! expr_type: raise TypeError(f赋值类型不匹配{node.name} 是 {var_type}表达式是 {expr_type}) def visit_If(self, node): cond_type self.analyze(node.cond) if cond_type ! boolean: raise TypeError(fif 条件必须是 boolean实际是 {cond_type}) self.analyze(node.then_branch) if node.else_branch: self.analyze(node.else_branch) def visit_While(self, node): cond_type self.analyze(node.cond) if cond_type ! boolean: raise TypeError(fwhile 条件必须是 boolean实际是 {cond_type}) self.analyze(node.body) def visit_Write(self, node): self.analyze(node.expr) def visit_BinOp(self, node): left_type self.analyze(node.left) right_type self.analyze(node.right) if node.op in (PLUS, MINUS, MUL, DIV, MOD): if left_type ! integer or right_type ! integer: raise TypeError(f算术运算要求 integer实际 {left_type} 和 {right_type}) return integer if node.op in (EQ, NEQ, LT, LE, GT, GE): if left_type ! right_type: raise TypeError(f比较运算两侧类型不一致{left_type} vs {right_type}) return boolean if node.op in (AND, OR): if left_type ! boolean or right_type ! boolean: raise TypeError(f逻辑运算要求 boolean) return boolean raise TypeError(f未知运算符 {node.op}) def visit_UnaryOp(self, node): operand_type self.analyze(node.operand) if node.op MINUS and operand_type ! integer: raise TypeError(一元负号要求 integer) if node.op NOT and operand_type ! boolean: raise TypeError(not 要求 boolean) return operand_type def visit_Num(self, node): return integer def visit_Bool(self, node): return boolean def visit_Var(self, node): return self.symtab.lookup(node.name) def visit_NoOp(self, node): return None符号表用栈式结构是为了后面扩展作用域用的当前子集只有全局作用域但如果你要加过程或函数scopes直接append一个新字典就行lookup从栈顶往下找天然支持嵌套作用域。visit_Assign里做了类型匹配检查visit_BinOp里区分了算术、比较、逻辑三类运算的类型规则。这些检查看起来琐碎但它们是编译器「能给出有意义报错」和「只会崩」的分水岭。3. 从语法树到可执行结果代码生成与解释执行3.1 两种落地路径生成字节码还是直接解释语法树和语义检查都过了之后你有两条路可以走。第一条是生成中间代码比如三地址码或字节码再写一个虚拟机去执行第二条是直接写一个树遍历解释器边走 AST 边算结果。两条路各有取舍。生成字节码的好处是执行效率高、便于做优化、也便于后面接真正的目标代码生成比如生成 x86 汇编。代价是你得多写一个虚拟机指令集设计、栈管理、跳转回填这些都得处理。树遍历解释器则简单得多一个evaluate函数递归下去就完事适合快速验证编译前端是否正确。我的建议是第一次写编译器先做树遍历解释器把前端跑通、测试用例全绿之后再考虑加字节码后端。因为前端词法、语法、语义才是编译器真正难的部分后端只是体力活。你如果一上来就搞字节码遇到 bug 的时候根本分不清是前端 AST 建错了还是后端指令生成错了。3.2 树遍历解释器的实现class Interpreter: def __init__(self): self.variables {} def interpret(self, node): method visit_ type(node).__name__ visitor getattr(self, method, self.generic_visit) return visitor(node) def generic_visit(self, node): raise NotImplementedError(f没有为 {type(node).__name__} 定义 visit 方法) def visit_Program(self, node): self.visit(node.block) def visit_Block(self, node): for decl in node.decls: self.visit(decl) self.visit(node.body) def visit_VarDecl(self, node): for name in node.names: if node.type_name integer: self.variables[name] 0 elif node.type_name boolean: self.variables[name] False def visit_Compound(self, node): for stmt in node.stmts: self.visit(stmt) def visit_Assign(self, node): self.variables[node.name] self.visit(node.expr) def visit_If(self, node): if self.visit(node.cond): self.visit(node.then_branch) elif node.else_branch: self.visit(node.else_branch) def visit_While(self, node): while self.visit(node.cond): self.visit(node.body) def visit_Write(self, node): print(self.visit(node.expr)) def visit_BinOp(self, node): op node.op if op PLUS: return self.visit(node.left) self.visit(node.right) if op MINUS: return self.visit(node.left) - self.visit(node.right) if op MUL: return self.visit(node.left) * self.visit(node.right) if op DIV: right self.visit(node.right) if right 0: raise ZeroDivisionError(除数为零) return self.visit(node.left) // right if op MOD: right self.visit(node.right) if right 0: raise ZeroDivisionError(模运算除数为零) return self.visit(node.left) % right if op EQ: return self.visit(node.left) self.visit(node.right) if op NEQ: return self.visit(node.left) ! self.visit(node.right) if op LT: return self.visit(node.left) self.visit(node.right) if op LE: return self.visit(node.left) self.visit(node.right) if op GT: return self.visit(node.left) self.visit(node.right) if op GE: return self.visit(node.left) self.visit(node.right) if op AND: return self.visit(node.left) and self.visit(node.right) if op OR: return self.visit(node.left) or self.visit(node.right) raise RuntimeError(f未知运算符 {op}) def visit_UnaryOp(self, node): if node.op MINUS: return -self.visit(node.operand) if node.op NOT: return not self.visit(node.operand) raise RuntimeError(f未知一元运算符 {node.op}) def visit_Num(self, node): return node.value def visit_Bool(self, node): return node.value def visit_Var(self, node): if node.name not in self.variables: raise NameError(f变量 {node.name} 未初始化) return self.variables[node.name] def visit_NoOp(self, node): passvisit_While是唯一有循环风险的地方。如果条件永远为真解释器会死循环。这不是 bug是语言特性——Pascal 本身就允许while true do。但如果你在测试时不小心写了个死循环程序会卡住这时候你需要的是 CtrlC而不是改代码。visit_BinOp里对除法和取模做了零检查这是语义分析阶段没法做的——因为除数可能是运行时才确定的变量。这类「运行时错误」和「编译期错误」要分开处理前者抛异常后者在语义分析阶段就拦下来。3.3 把整条链路串起来跑一个完整例子现在把词法、语法、语义、解释四步串起来def run(source): lexer Lexer(source) parser Parser(lexer) ast parser.parse() analyzer SemanticAnalyzer() analyzer.analyze(ast) interpreter Interpreter() interpreter.interpret(ast) if __name__ __main__: source program test; var x, y : integer; flag : boolean; begin x : 10; y : 3; flag : x y; if flag then write(x y * 2) else write(0); while x 0 do begin write(x); x : x - 3 end end. run(source)这段代码的输出是16然后依次打印10、7、4、1。注意write(x y * 2)的结果是 16 而不是 26因为乘法优先级高于加法y * 2先算得 6再加 10。如果你前面的parse_term和parse_simple_expr分层写反了这里会输出 26这就是优先级处理错误的典型症状。while循环里x : x - 3没有分号结尾因为它是begin...end块里的最后一条语句Pascal 允许最后一条语句不带分号。但如果你在end前多写一个分号parse_stmt_list会尝试解析一个空语句然后parse_stmt返回NoOp不会报错。这是 Pascal 文法的一个宽容之处也是为什么NoOp节点必须存在。4. 避坑与排查写 Pascal 编译器最容易翻车的五个地方4.1 现象:被解析成:和赋值语句报语法错误原因词法分析器里单字符运算符的判断放在了双字符之前。next_token先看到:直接返回COLON然后下一个 token 是语法分析器在parse_stmt里期望ASSIGN拿到COLON就报错了。解决把所有双字符运算符:、、、的判断放在单字符映射之前。判断方式是取self.text[self.pos:self.pos2]做字符串比较匹配成功就连续advance()两次。这个顺序问题在写词法分析器时是血泪教训我见过不止一个人在这里卡半天。4.2 现象1 2 * 3算出来是 9 而不是 7原因语法分析器的优先级层次写错了。如果parse_expr直接处理所有二元运算符没有分成simple_expr和term两层那和*就是左结合同级运算(1 2) * 3自然得 9。解决严格按照文法分层。parse_expr只处理比较运算符parse_simple_expr处理加减和orparse_term处理乘除、mod和andparse_factor处理括号和原子。每一层只吃自己那层的运算符遇到不属于自己的就返回上一层。这样*在parse_term里被消化掉在parse_simple_expr里才处理优先级自然正确。4.3 现象变量未声明却通过了语义检查运行时才报 KeyError原因语义分析器没有在visit_Var里查符号表或者符号表在声明时没有正确写入。常见的是visit_VarDecl里用了self.symtab.declare但declare内部逻辑写成了if name not in scope才报错结果重复声明被放过了或者lookup没有遍历所有作用域。解决declare必须检查重复声明并抛异常lookup必须从最内层作用域往外找。另外语义分析必须在解释执行之前完整跑一遍不能边解释边检查——因为if的else分支可能永远不执行如果语义检查放在解释器里else分支里的未声明变量就漏掉了。4.4 现象while循环条件为假时仍然执行了一次循环体原因visit_While写成了do-while语义先执行一次body再判断条件。Pascal 的while-do是标准的先判断后执行和 C 的while一样。解决visit_While必须是while self.visit(node.cond): self.visit(node.body)条件判断在循环体之前。如果你想要repeat-until语义那是另一个文法产生式不能混在while里。4.5 现象嵌套if-then-else的else绑定了错误的if原因经典的「悬空 else」问题。文法ifStmt if expr then stmt [ else stmt ]里else是可选的当出现if a then if b then s1 else s2时else应该绑定到内层的if b但递归下降分析器如果处理不当可能绑定到外层。解决递归下降天然按最近匹配原则处理——parse_stmt在解析if时then_branch递归调用parse_stmt内层if会先消费掉else。所以只要你的parse_stmt对if的处理是「解析完then分支后立即检查else」悬空 else 就会正确绑定到内层。如果你把else的处理放到外层循环里就会绑错。5. 进阶技巧用文法驱动的方式扩展语言特性前面四章走完你已经有了一个能跑通的最小 Pascal 编译器。但真实工程里语言特性是不断加的每次加特性如果都要改词法、语法、语义、解释四层那维护成本会爆炸。我一般会用「文法驱动」的方式来做扩展先在 EBNF 里加产生式再按产生式逐层落地代码。以加一个for循环为例。先在文法里加forStmt for ident : expr (to | downto) expr do stmt ;然后词法层加FOR、TO、DOWNTO三个关键字到KEYWORDS字典。语法层加parse_for_stmt在parse_stmt的分发里加elif t FOR分支。AST 加For节点。语义层加visit_For检查循环变量已声明且为integer起止表达式也是integer。解释层加visit_For根据to或downto决定步进方向。这套流程走下来加一个特性大概 30 分钟而且不容易漏。关键是每一步都有明确的输入输出文法产生式决定语法函数的结构AST 节点决定语义和解释的访问方法符号表决定类型检查的规则。验证编译器正确性我习惯用「差分测试」同一段 Pascal 代码分别用我的编译器和 Free Pascal 的fpc编译运行对比输出。如果输出一致说明前端至少在这条路径上是对的。不一致的地方就是 bug 的高发区。这个技巧比手写测试用例高效得多因为你可以拿现成的 Pascal 代码库来跑。最后一个习惯每次改完文法先把 EBNF 重新过一遍确认它还是 LL(1) 的。如果引入了左递归或者公共前缀递归下降分析器会直接崩掉。我一般会在文法文件旁边放一张 FIRST/FOLLOW 集的手算表加产生式的时候顺手更新这样能在写代码之前就发现冲突。这个习惯帮我省了无数次重构分析器的时间。希望帮到你。本文还有配套的精品资源点击获取