资讯动态

基于Pascal文法的编译器前端实战:从词法分析到解释执行

发布时间:2026/10/3 9:02:01 来源:尧图企业网站定制
简介这份资源是面向计算机专业学生与编译原理学习者的Pascal文法编译器课程设计完整实现围绕词法分析、语法分析、语义检查与代码生成等核心环节展开适合正在做课程设计或希望动手理解编译器构造流程的中高级学习者。压缩包共140个文件约8.18MB以cpp与h源码、txt与md说明文档、cmake与make构建脚本为主另含o、exe、bin等编译产物及少量xml、pptx、xls辅助材料覆盖从源码到可执行文件的完整工程结构。资源重点实现了if条件判断、while循环、类型定义、过程与函数调用以及嵌套定义等扩展功能并涉及符号表管理与目标代码生成可帮助读者对照Pascal文法梳理词法规则、上下文无关文法与解析算法理解类型检查与错误检测机制。目前已有288人学习下载适合作为课程设计参考与编译器入门实践素材。1. 基于Pascal文法的编译器从文法规则到可运行前端的完整路径很多人第一次接触「基于Pascal文法的编译器」这个题目是在编译原理课设或者自研脚本引擎的场景里。需求很具体给定一套Pascal子集的文法规则要做出词法分析、语法分析最好还能生成中间代码或直接解释执行。Pascal 的文法结构清晰、关键字固定、类型声明规整是练手编译器前端的理想素材比直接啃 C 语言文法要友好得多。这篇文章面向的是想真正把「文法」变成「能跑的编译器」的工程师和学生我会按词法、语法、语义、代码生成这条主线把每一步的参数、工具选型和踩坑点讲清楚。读完你应该能自己搭出一个支持赋值、表达式、if、while 和过程调用的 Pascal 子集编译器前端并知道后面该往哪扩。2. 先把文法立住Pascal 子集该砍掉什么、保留什么2.1 为什么不能直接照搬完整 Pascal 文法完整 Pascal 文法包含嵌套过程、变体记录、集合类型、文件类型、指针、标签跳转等大量特性如果一开始全盘接收语法分析器的状态机会爆炸调试成本极高。我一般会先定义一个「教学子集」保留最能体现编译器核心机制的部分程序头、常量与变量声明、赋值语句、算术与逻辑表达式、if-else、while、write/writeln 输出、简单过程声明与调用。砍掉的是嵌套过程、指针、集合、文件、goto、with 语句。这样文法规模控制在 40 条产生式左右LL(1) 或 LR(1) 都能处理。选型上递归下降适合手写、可读性好、报错信息容易定制Yacc/Bison 适合产生式多、想快速验证的情况。如果你是要理解「文法如何驱动编译器」我建议手写递归下降因为每一步都能看到文法符号是怎么被消费的。2.2 用 EBNF 把子集文法写清楚下面是我常用的 Pascal 子集 EBNF直接可以转成递归下降代码或者喂给 ANTLRprogram : program ID ; block . block : declPart statementPart declPart : (constDecl | varDecl | procDecl)* constDecl : const (ID constValue ;) varDecl : var (ID (, ID)* : type ;) type : integer | real | boolean procDecl : procedure ID ( paramList? ) ; block ; paramList : ID (, ID)* : type (; ID (, ID)* : type)* statementPart : begin statement (; statement)* end statement : assignStmt | ifStmt | whileStmt | compoundStmt | procCall | ioStmt assignStmt : ID : expression ifStmt : if expression then statement (else statement)? whileStmt : while expression do statement compoundStmt : begin statement (; statement)* end procCall : ID ( argList? ) ioStmt : write ( expression ) | writeln ( expression ) expression : simpleExpr (relOp simpleExpr)? simpleExpr : term (addOp term)* term : factor (mulOp factor)* factor : ID | NUMBER | ( expression ) | not factor relOp : | | | | | addOp : | - | or mulOp : * | / | div | mod | and这份文法的关键点在于expression用经典的优先级分层expression → simpleExpr → term → factor保证12*3解析成1(2*3)statement里if的 else 悬挂问题通过「else 就近匹配」在递归下降里自然解决procDecl的参数只支持值传递砍掉 var 参数降低符号表复杂度。提示EBNF 里的?和*在转递归下降时要手动展开成 if 和 while不要指望工具自动帮你处理所有边界。2.3 文法验证先跑通 anbn 这类最小集合在写完整编译器之前我习惯先用一个最小文法验证解析框架是否正确。热搜里常出现「构造文法 anbn 集合 n 大于一」这其实就是用a^n b^n检验解析器能否处理「数量匹配」的上下文无关结构。Pascal 里的begin...end配对、括号配对本质是同一类问题。你可以先写一个只认a和b的递归下降函数确认栈式匹配逻辑没问题再套到 Pascal 的 block 上。# 最小验证解析 a^n b^n (n1) def parse_anbn(s): pos 0 def match_a(): nonlocal pos count 0 while pos len(s) and s[pos] a: pos 1 count 1 return count def match_b(n): nonlocal pos for _ in range(n): if pos len(s) or s[pos] ! b: raise SyntaxError(f位置 {pos} 期望 b) pos 1 n match_a() if n 1: raise SyntaxError(至少需要一个 a) match_b(n) if pos ! len(s): raise SyntaxError(多余字符) return True print(parse_anbn(aaabbb)) # True这段代码里match_a返回 a 的个数match_b按这个个数消费 b位置指针pos是唯一的全局状态。参数n就是匹配数量改成 Pascal 的begin/end计数逻辑完全一样。跑通这个你对「文法驱动的栈式匹配」就有手感了。3. 词法分析器把字符流切成有类型的 Token3.1 Token 类型设计与关键字表词法分析的目标是把源程序字符串变成 Token 序列每个 Token 带类型、值、行号列号。Pascal 的关键字是大小写不敏感的Program、PROGRAM、program等价这一点和 C 不同必须在词法层统一转小写再查表。我一般把 Token 类型定义成枚举PROGRAM、ID、NUMBER、PLUS、MINUS、STAR、SLASH、ASSIGN、SEMI、COLON、LPAREN、RPAREN、DOT、COMMA、EQ、NEQ、LT、LE、GT、GE、BEGIN、END、IF、THEN、ELSE、WHILE、DO、VAR、CONST、PROCEDURE、WRITE、WRITELN、EOF。关键字表用一个字典映射字符串到 Token 类型标识符识别出来后先转小写查这个表命中就是关键字否则是普通 ID。3.2 手写词法分析器的核心循环KEYWORDS { program: PROGRAM, begin: BEGIN, end: END, if: IF, then: THEN, else: ELSE, while: WHILE, do: DO, var: VAR, const: CONST, procedure: PROCEDURE, write: WRITE, writeln: WRITELN, div: DIV, mod: MOD, and: AND, or: OR, not: NOT, integer: INTEGER, real: REAL, boolean: BOOLEAN } class Lexer: def __init__(self, text): self.text text self.pos 0 self.line 1 self.col 1 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_ws_and_comments(self): while self.peek() is not None: if self.peek().isspace(): self.advance() elif self.peek() {: while self.peek() is not None and self.peek() ! }: self.advance() if self.peek() }: self.advance() else: break def next_token(self): self.skip_ws_and_comments() if self.peek() is None: return (EOF, None, self.line, self.col) ch self.peek() if ch.isalpha() or ch _: return self.read_ident() if ch.isdigit(): return self.read_number() return self.read_operator() def read_ident(self): start_line, start_col self.line, self.col buf [] while self.peek() is not None and (self.peek().isalnum() or self.peek() _): buf.append(self.advance()) word .join(buf).lower() ttype KEYWORDS.get(word, ID) return (ttype, word, start_line, start_col) def read_number(self): start_line, start_col self.line, self.col buf [] while self.peek() is not None and self.peek().isdigit(): buf.append(self.advance()) if self.peek() .: buf.append(self.advance()) while self.peek() is not None and self.peek().isdigit(): buf.append(self.advance()) return (REAL_NUM, float(.join(buf)), start_line, start_col) return (INT_NUM, int(.join(buf)), start_line, start_col) def read_operator(self): start_line, start_col self.line, self.col ch self.advance() two ch (self.peek() or ) if two :: self.advance(); return (ASSIGN, :, start_line, start_col) if two : self.advance(); return (NEQ, , start_line, start_col) if two : self.advance(); return (LE, , start_line, start_col) if two : self.advance(); return (GE, , start_line, start_col) single { : PLUS, -: MINUS, *: STAR, /: SLASH, : EQ, : LT, : GT, ;: SEMI, :: COLON, ,: COMMA, (: LPAREN, ): RPAREN, .: DOT } if ch in single: return (single[ch], ch, start_line, start_col) raise SyntaxError(f第 {start_line} 行第 {start_col} 列出现非法字符: {ch})skip_ws_and_comments处理空白和{ }注释Pascal 的注释就是花括号不支持嵌套。read_ident里word.lower()是关键保证关键字大小写不敏感。read_number区分整数和实数遇到小数点就转 float。read_operator先看双字符运算符:、、、再看单字符。每个 Token 都带行号和列号后面报错定位全靠它。注意Pascal 里:是赋值是相等比较这两个在词法层必须分开否则语法分析会把赋值当表达式。3.3 词法阶段的常见翻车点第一个坑是注释未闭合。{开始后如果到文件尾都没遇到}skip_ws_and_comments会静默吃掉后面所有代码导致 Token 流突然 EOF。解决方法是加一个标志循环结束后检查是否真的遇到了}没遇到就抛「注释未闭合」错误。第二个坑是数字后紧跟字母比如123abc。Pascal 标准里这是非法的但有些实现会切成123和abc。我一般选择报错因为静默切分会让后续语法错误更难定位。第三个坑是行号列号在跨行字符串或注释里更新不及时。上面代码里advance统一处理了\n只要所有字符消费都走advance行列号就不会错。如果你在某个分支里直接self.pos 1行列号就会漂移这是血泪经验。4. 递归下降语法分析把 Token 流变成 AST4.1 AST 节点设计与优先级处理语法分析的产物是抽象语法树AST。我一般定义这些节点ProgramNode、BlockNode、VarDeclNode、ConstDeclNode、ProcDeclNode、AssignNode、IfNode、WhileNode、CallNode、WriteNode、BinOpNode、UnaryOpNode、NumNode、VarNode。每个节点用 Python 的类或者字典表示带line、col方便报错。优先级处理靠文法分层parse_expression调parse_simple_expr后者调parse_termparse_term调parse_factor。这样12*3在parse_term里先把2*3合成一个 BinOpNode再回到parse_simple_expr和1相加。递归下降的优先级是「越深的函数绑定越紧」。4.2 核心解析函数与错误恢复class Parser: def __init__(self, tokens): self.tokens tokens self.pos 0 def cur(self): return self.tokens[self.pos] def eat(self, ttype): tok self.cur() if tok[0] ! ttype: raise SyntaxError( f第 {tok[2]} 行第 {tok[3]} 列期望 {ttype}实际 {tok[0]} ({tok[1]}) ) self.pos 1 return tok def parse_program(self): self.eat(PROGRAM) name self.eat(ID)[1] self.eat(SEMI) block self.parse_block() self.eat(DOT) return (Program, name, block) def parse_block(self): decls [] while self.cur()[0] in (CONST, VAR, PROCEDURE): if self.cur()[0] CONST: decls.append(self.parse_const_decl()) elif self.cur()[0] VAR: decls.append(self.parse_var_decl()) else: decls.append(self.parse_proc_decl()) stmts self.parse_statement_list() return (Block, decls, stmts) def parse_statement_list(self): self.eat(BEGIN) stmts [self.parse_statement()] while self.cur()[0] SEMI: self.eat(SEMI) if self.cur()[0] END: break stmts.append(self.parse_statement()) self.eat(END) return stmts def parse_statement(self): t self.cur()[0] if t ID: return self.parse_assign_or_call() if t IF: return self.parse_if() if t WHILE: return self.parse_while() if t BEGIN: return (Compound, self.parse_statement_list()) if t in (WRITE, WRITELN): return self.parse_io() raise SyntaxError(f第 {self.cur()[2]} 行无法识别的语句起始: {t}) def parse_expression(self): left self.parse_simple_expr() if self.cur()[0] in (EQ, NEQ, LT, LE, GT, GE): op self.cur()[0] self.pos 1 right self.parse_simple_expr() return (BinOp, op, left, right) return left def parse_simple_expr(self): node self.parse_term() while self.cur()[0] in (PLUS, MINUS, OR): op self.cur()[0] self.pos 1 right self.parse_term() node (BinOp, op, node, right) return node def parse_term(self): node self.parse_factor() while self.cur()[0] in (STAR, SLASH, DIV, MOD, AND): op self.cur()[0] self.pos 1 right self.parse_factor() node (BinOp, op, node, right) return node def parse_factor(self): tok self.cur() if tok[0] INT_NUM: self.pos 1 return (Num, tok[1]) if tok[0] REAL_NUM: self.pos 1 return (Num, tok[1]) if tok[0] ID: self.pos 1 return (Var, tok[1]) if tok[0] LPAREN: self.pos 1 node self.parse_expression() self.eat(RPAREN) return node if tok[0] NOT: self.pos 1 return (UnaryOp, NOT, self.parse_factor()) raise SyntaxError(f第 {tok[2]} 行第 {tok[3]} 列期望表达式实际 {tok[0]})eat是核心断言函数类型不匹配立刻抛错并带上行列号。parse_statement_list处理begin...end里的分号分隔注意最后一个语句后面可以没有分号所以遇到END要 break。parse_expression到parse_factor四层函数对应文法里的优先级分层这是递归下降最标准的写法。parse_factor里NOT是右结合的一元运算符直接递归调自己。4.3 悬挂 else 与左递归消除Pascal 的if...then...else存在悬挂 else 问题if a then if b then s1 else s2里 else 到底配哪个 if。递归下降的天然行为是「else 就近匹配」也就是配内层 if这和 Pascal 标准一致所以不用额外处理。但如果你用 Yacc 写就要用优先级声明%nonassoc来强制。左递归方面表达式文法expression : expression term是左递归递归下降会无限递归。解决办法就是上面那样改写成expression : term ( term)*用 while 循环代替左递归。这是手写解析器必须过的坎。5. 语义分析与符号表类型检查、作用域和过程调用5.1 符号表结构栈式作用域符号表用栈式结构每进入一个 block 压一层退出弹一层。每层是一个字典键是变量名值是{type, kind, slot}。kind区分变量、常量、过程。slot是后面代码生成时的栈偏移或寄存器编号。Pascal 的作用域规则是内层可以遮蔽外层同名变量但过程内部不能访问调用者的局部变量除非是嵌套过程我们砍掉了。所以查找变量时从栈顶往下找找到第一个就返回。class SymbolTable: def __init__(self): self.scopes [{}] def push(self): self.scopes.append({}) def pop(self): self.scopes.pop() def declare(self, name, info): if name in self.scopes[-1]: raise SyntaxError(f重复声明: {name}) self.scopes[-1][name] info def lookup(self, name): for scope in reversed(self.scopes): if name in scope: return scope[name] return Nonedeclare只在当前层查重允许内层遮蔽外层。lookup从最内层往外找。这个结构简单但够用支持过程调用时的参数作用域。5.2 类型检查与隐式转换规则Pascal 是强类型语言integer和real不能直接混用但允许 integer 隐式提升为 real。布尔类型不能参与算术运算。类型检查在遍历 AST 时做每个表达式节点返回一个类型父节点检查子节点类型是否合法。规则表运算左类型右类型结果类型说明 - *integerintegerinteger整数运算 - *realrealreal实数运算 - *integerrealreal隐式提升/integer/realinteger/realreal除法总是 realdiv modintegerintegerinteger仅整数and or notbooleanbooleanboolean仅布尔 同类型同类型boolean可比较赋值语句检查右边类型能否赋给左边integer 可以赋给 real反之不行。if 和 while 的条件必须是 boolean。write/writeln 接受 integer、real、boolean。5.3 过程调用的参数匹配过程声明时记录参数个数和类型列表。调用时检查实参个数和类型是否匹配。我们只支持值传递所以实参可以是任意表达式类型按上面的规则检查。返回值方面教学子集里过程不返回值函数可以后面扩展。def check_call(self, node, symtab): name node[1] info symtab.lookup(name) if info is None or info[kind] ! procedure: raise SyntaxError(f未定义的过程: {name}) params info[params] args node[2] if len(args) ! len(params): raise SyntaxError(f过程 {name} 期望 {len(params)} 个参数实际 {len(args)} 个) for arg, ptype in zip(args, params): atype self.check_expr(arg, symtab) if atype ! ptype and not (atype integer and ptype real): raise SyntaxError(f参数类型不匹配: 期望 {ptype}实际 {atype})这段逻辑先查符号表确认过程存在再比对参数个数最后逐个检查类型允许 integer 到 real 的提升。报错信息带上过程名和期望/实际类型调试时能省很多时间。6. 避坑与排查编译器前端最容易翻车的 5 个地方6.1 现象解析到一半报「期望 SEMI 实际 END」原因parse_statement_list里分号处理逻辑没考虑最后一个语句后无分号的情况。Pascal 允许begin a:1; b:2 end也允许begin a:1; b:2; end但begin a:1; b:2; end里最后一个分号后直接 END如果循环里无条件 eat SEMI 再 parse_statement就会在 END 上报错。解决在 eat SEMI 之后先检查当前 Token 是不是 END是就 break不再解析语句。上面parse_statement_list里的if self.cur()[0] END: break就是干这个的。6.2 现象变量查找返回 None但明明声明了原因符号表 push/pop 时机不对。常见错误是在 parse_block 开始时 push但 parse_block 里先解析声明再解析语句声明阶段 declare 到当前层没问题可如果 parse_proc_decl 里又 push 了一层却没 pop退出过程后外层变量就被遮蔽了。解决push 和 pop 必须严格配对用 try/finally 保证异常时也能 pop。我一般把 push/pop 放在 parse_block 的入口和出口过程声明的参数在 push 之后 declare 到新层。6.3 现象1/2结果是 0 而不是 0.5原因代码生成或解释执行时用了整数除法。Pascal 里/永远返回 realdiv才是整数除法。如果解释器里/直接用了 Python 的//就会截断。解决在 eval 或代码生成时SLASH对应浮点除法DIV对应整数除法。类型检查阶段也要把/的结果标成 real这样赋值给 integer 变量时会报类型错误。6.4 现象if 嵌套时 else 配错 if原因如果手写解析器里 parse_if 没有正确处理 else 的可选性或者用了错误的优先级if a then if b then s1 else s2可能把 else 配给外层 if。解决递归下降里 parse_if 解析完 then 分支后检查当前 Token 是不是 ELSE是就消费并解析 else 分支。因为内层 if 先返回else 自然配内层。如果你用 Yacc用%nonassoc ELSE并调整优先级。6.5 现象报错行号总是 1原因Token 的行列号在词法阶段没更新或者语法分析报错时用了错误的 Token 索引。常见的是self.tokens[self.pos]越界后取了默认值或者 Lexer 里某些分支直接操作self.pos没走 advance。解决所有字符消费必须走 advance所有报错必须用self.cur()返回的 Token 里的行列号。可以在 Parser 里加一个error(msg)方法统一格式化第 X 行第 Y 列: msg避免各处拼字符串。7. 从 AST 到可执行解释执行与代码生成的两条路走到 AST 之后你有两条路一是写一个树遍历解释器直接 eval AST二是生成中间代码三地址码或栈式字节码再写虚拟机执行。教学场景我推荐先写解释器因为快、好调试想深入编译器后端再上代码生成。解释器核心是一个eval_node(node, env)函数env 是变量名到值的字典。BinOp 节点递归求左右值再按 op 计算Assign 节点求右值写 envIf 节点求条件决定走哪个分支While 节点循环直到条件为假。过程调用稍微麻烦需要把参数值绑定到新 env执行过程体再恢复调用者 env。我一般用 env 链或者栈式 env 实现。def eval_node(node, env): kind node[0] if kind Num: return node[1] if kind Var: if node[1] not in env: raise RuntimeError(f未定义变量: {node[1]}) return env[node[1]] if kind BinOp: op node[1] l eval_node(node[2], env) r eval_node(node[3], env) if op PLUS: return l r if op MINUS: return l - r if op STAR: return l * r if op SLASH: return l / r if op DIV: return l // r if op MOD: return l % r if op EQ: return l r if op NEQ: return l ! r if op LT: return l r if op LE: return l r if op GT: return l r if op GE: return l r if op AND: return l and r if op OR: return l or r if kind Assign: val eval_node(node[2], env) env[node[1]] val return val if kind If: if eval_node(node[1], env): return eval_node(node[2], env) elif node[3] is not None: return eval_node(node[3], env) return None if kind While: while eval_node(node[1], env): eval_node(node[2], env) return None if kind Compound: result None for stmt in node[1]: result eval_node(stmt, env) return result if kind Write: val eval_node(node[1], env) print(val, end) return None if kind Writeln: val eval_node(node[1], env) print(val) return None raise RuntimeError(f未知节点类型: {kind})这段解释器覆盖了赋值、表达式、if、while、复合语句和输出。SLASH用/返回浮点DIV用//返回整数和类型检查规则一致。过程调用需要额外处理 env 的保存和恢复可以用一个 env 栈调用时 push 新 env返回时 pop。如果你想走代码生成三地址码是常见选择每条指令形如t1 a b、if t1 goto L1、param x、call f, n。生成时遍历 AST为每个中间结果分配临时变量控制流用标签和跳转。栈式字节码则更接近 JVM用操作数栈代替临时变量指令如LOAD x、PUSH 1、ADD、STORE y。两条路都值得走一遍但先跑通解释器能让你快速验证前端正确性。最后说一个我自己的习惯每加一个文法特性先写三个测试用例——一个正常、一个边界、一个错误。正常用例确认功能边界用例确认不崩错误用例确认报错信息可读。这个习惯帮我省了无数调试时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑