Pascal子集编译器实现:词法、LL(1)分析与三地址码生成
发布时间:2026/10/31 20:35:25 锦皓数字建站
分析与三地址码生成`)
简介这份资源是面向计算机专业学生与编译原理初学者的Pascal子集编译器课程设计报告围绕词法分析、语法分析、语义分析与中间代码生成等核心环节展开帮助读者理解并实践一个小型编译程序的完整构造流程。压缩包内共1个doc文档约952KB以课程设计报告为主体涵盖需求分析、总体结构、接口描述与各阶段详细设计说明并附有团队分工与成绩评定标准。内容预览显示报告来自北京邮电大学课程设计按Sub_P文法组织具体涉及符号表构建、first与follow集求解、分析表与分析栈设计、三地址码或四元式中间表示及类型检查等知识点可作为课程设计撰写与实现的参考范本。目前已有240人学习适合需要完成编译原理实验、梳理设计文档结构或对照检查各阶段实现思路的读者参考借鉴。1. 一份能跑通的 Pascal 子集编译器从课程设计报告到可复现的编译流水线手上这份 Pascal 子集编译器设计报告第一眼看上去像典型的课程作业但翻到词法接口和翻译方案那几页会发现它把「编译 pascal 子集」这条链路拆得相当完整词法分析产出记号流和符号表语法分析用 LL(1) 预测分析表建分析树语义分析挂翻译方案做类型检查中间代码用三地址码落地。对正在啃编译原理、想找一个能对着抄结构的小型编译器实现的人来说这份报告的价值不在论文腔而在它把每个阶段的接口、数据结构和内部编码都摊开了。适合两类人一是课程设计要交编译器、卡在词法符号表或预测分析表构造上的学生二是想快速理解一个无工具、纯手工实现的编译前端长什么样的从业者。下面按「它怎么组织 → 怎么复现 → 哪里会翻车」的顺序拆。2. 词法与符号表记号流数组和行列号是怎么落地的2.1 为什么先啃词法接口而不是直接看语法很多人拿到编译器项目会直奔语法分析觉得那才是「核心」。但这份报告里词法分析器lexout(void)是整个流水线的地基它没有参数没有返回值靠往两个全局数组Symbol_stream[1000]和symbol_table[100]里写数据来和语法分析器通信。这种设计在工程上不算优雅但对课程设计场景反而友好——接口极简语法分析器只要读数组就行不用处理复杂的返回值协议。我一般建议复现时先把这个接口跑通因为词法一旦有 bug后面语法分析报的错全是假的排查起来就是黑匣子。词法分析器要处理的事情比想象中多过滤{}和/**/两种注释、识别间隔符号、做行列计数、区分关键字和标识符、把常数塞进常数表。报告里给的内部编码表是关键它定义了每个记号对应的属性值比如and是 1、array是 2、begin是 3一直到id是 36、num是 35。这套编码是词法和语法之间的契约语法分析器的预测分析表就是按这套编码来查的编码对不上分析表全废。2.2 符号表的数据结构为什么 name 只留 9 个字符报告里符号表的条目结构值得单独说typedef struct table_item { char name[9]; // 变量名有效字符数为 8 int address; // 目标地址 int type; // 类型 int demension; // 维数 int declare_row; // 声明行 int use_row[5]; // 引用行 int block; // 块索引号 } Tuple; Tuple symbol_table[100];name[9]意味着有效标识符最长 8 个字符加一个结束符。这是课程设计里常见的取舍——Pascal 标准其实允许更长但截断到 8 字符能简化哈希和比较。复现时如果你照抄这个长度遇到长变量名会被静默截断两个前 8 字符相同的变量会撞在一起这是后面避坑章节要重点说的。use_row[5]只记 5 次引用超过就丢报告没写溢出处理实际用的时候要么扩容要么加计数。Word_insert(char[])的返回值语义也要注意标识符已存在返回它在WordList[]里的下标否则返回 0。这里有个隐患——下标 0 既是「新插入」又是「第一个元素的位置」如果第一个标识符恰好存在下标 0返回值就歧义了。常见做法是把符号表从下标 1 开始用或者返回 -1 表示不存在。2.3 词法分析器的可执行步骤复现词法部分我一般按这个顺序走第一步把源文件整体读进缓冲区buffer[Max]报告就是这么做的一次性读入再逐字符扫描比流式读取好调试。第二步实现字符分类函数。IsLetter(char ch[], int n)判断字母、IsDigit(char ch)判断数字这两个是扫描循环的分支依据。注意IsLetter的参数是char ch[]加长度n不是单字符说明它可能被用来判断一个 token 是否全字母复现时别写成单字符版本。第三步关键字识别。Iskeyword(char[])返回 1 到 4 分别对应mod、or、and、div其他关键字返回 -1标识符返回 0。这个返回值设计有点绕——为什么只有这四个运算符关键字有正数编码其他关键字走 -1因为mod/or/and/div在语法里当运算符处理需要单独的属性值而begin/end/var这类结构关键字只需要「是关键字」这个信息。理解这一点语法分析器里对运算符和结构字的处理分支就顺了。第四步写输出。报告要求生成五个文件符号表、常数表、统计信息、记号流、终结符。这一步别省单独测试词法时这五个文件就是你的眼睛。统计信息里的行数、单词数、错误数能快速告诉你扫描有没有跑偏。// 词法扫描主循环的骨架按报告的状态机思路简化 while (buffer[i] ! \0) { if (buffer[i] \n) { line_count; i; continue; } if (IsLetter(buffer[i], 1)) { // 收集完整标识符到 token j 0; while (IsLetter(buffer[i], 1) || IsDigit(buffer[i])) { token[j] buffer[i]; } token[j] \0; int kw Iskeyword(token); if (kw 0) { int idx Word_insert(token); // 插入或查符号表 Symbol_stream[k].atr 36; // id 的编码 strcpy(Symbol_stream[k].sname, token); } else { Symbol_stream[k].atr kw; // 关键字/运算符编码 } k; } else if (IsDigit(buffer[i])) { // 数字处理注意小数点和 state1 标志 // ... } else { // 运算符、界符、注释过滤 // ... } }这段骨架里state1是小数的判断标志报告里用它区分整数和实数复现时小数点后必须跟数字否则要报错回退。digit_l和digit_r记录{}注释的配对不配对就是注释未闭合错误。提示词法分析器作为语法分析器的子程序被调用报告明确要求「将词法分析器作为语法分析器调用的函数」所以lexout不要写成独立 main要能被语法模块直接调。3. LL(1) 预测分析FIRST/FOLLOW 集与分析表的构造细节3.1 选 LL(1) 而不是 LR 的理由报告语法部分明确走 LL 方法要求「有求 first、follow 集的函数」「对每个非终结符有相应的函数相对应」「有明确的数据结构表示分析表」。为什么课程设计偏爱 LL(1)因为它的分析过程和分析树是同构的——你递归下降写一遍分析树自然就出来了直观输出分析树这个要求几乎白送。LR 虽然能力强但分析栈和移进归约表对初学者是额外的认知负担报告里也提到「可以用手工的方法也可以用 YACC 方法但不建议采用」手工 LL 是最稳的路线。代价是 LL(1) 对文法有要求不能有左递归不能有公共左因子。Pascal 子集的表达式文法天然带左递归E - E T复现时必须先做左递归消除改成E - T E、E - T E | ε这种形式。报告里的翻译方案能看到simple_expression、term、expression_list这些带撇的非终结符就是消除左递归后的产物。3.2 FIRST 集和 FOLLOW 集的实现要点报告给了FirstGroup和FollowGroup两个类核心方法是GetFirstGroup(int sym)和GetFollowGroup()都在语法分析器初始化时调用一次结果存内存供构造分析表用。FIRST 的算法就是教材三条规则终结符的 FIRST 是自身X - ε则 ε 进 FIRST(X)非终结符按产生式右部逐个符号并入遇到能推出 ε 的继续往后并。FOLLOW 同理开始符号放$然后按A - αBβ把 FIRST(β){ε} 并入 FOLLOW(B)β 能推 ε 时把 FOLLOW(A) 并入 FOLLOW(B)。这里有个实现细节报告没展开但很关键FIRST 集的计算有依赖顺序X - Y1 Y2里 X 的 FIRST 依赖 Y1 的 FIRST如果 Y1 还没算完就取会拿到空集。常见做法是迭代到不动点——反复扫所有产生式直到没有集合再变化为止。existed(int sym)这个判断函数就是为去重和不动点检测服务的别写成简单的线性查找就完事集合大了会慢但课程设计规模下线性查找够用。3.3 预测分析表的构造与查表CreateAnalysisTable()按教材算法 4.2 来先把analysistable[][]全初始化为 -1逐个扫产生式A - α标号 i对 FIRST(α) 里每个终结符 a把analysistable[A][a]赋值为 i如果 ε 在 FIRST(α) 里再对 FOLLOW(A) 里每个 b 赋值为 i。剩下还是 -1 的格子就是 error。查表时的数据结构是int analysistable[][]行是非终结符编码列是终结符编码值是产生式编号。这里编码一致性再次成为命门非终结符和终结符的编码必须和词法输出的记号编码对齐matchsymbol(char* str)就是干这个的——从终结符表和非终结符表里查符号对应的内部编码。复现时建议把这张编码对照表单独打出来贴在显示器边上调分析表的时候一眼能对上。// 预测分析表构造的核心逻辑 for (int i 0; i generator_count; i) { int A generator[i][0]; // 产生式左部 // 计算右部 FIRST for (each a in FirstOfRHS(i)) { analysistable[A][a] i; // 填产生式编号 } if (EpsilonInFirstOfRHS(i)) { for (each b in FollowGroup[A]) { analysistable[A][b] i; } } } // 未填充的格子保持 -1查表时 -1 即语法错误分析树用递归下降自然生成每个非终结符一个函数函数里查analysistable[当前非终结符][当前输入记号]拿到产生式编号按产生式右部依次调用子函数或匹配终结符。报告要求「能够以直观的方式输出分析树」最简单是缩进打印每进一层加两个空格改源程序后树结构变化一眼可见。注意LL(1) 分析表如果有冲突同一格被赋两个产生式编号说明文法不是 LL(1) 的得回去改文法或提取左因子别硬填覆盖。4. 语义分析与三地址码翻译方案怎么落到代码4.1 语法制导翻译的组织方式语义部分报告给了完整的翻译方案从program一路到factor每个产生式后面跟语义动作。这套方案的本质是语法制导定义SDD属性分两类综合属性往上传递比如expression.t类型、expression.name临时变量名继承属性往下传比如identifier_list.t变量类型。复现时最容易乱的就是属性传递方向——simple_expression的.t和.name是从右部子节点综合上来的而identifier_list的.t是从父节点继承下去的搞反了类型检查全错。报告里enter(SqStack * tableptr, char * name, int type, int addr)往符号表插变量mktable()产生新符号表lookahead(int k)查节点 k 的最左子节点。符号表用堆栈组织SqStack因为有不嵌套过程的要求其实栈的深度有限但用栈能自然处理作用域进出。4.2 类型检查与类型转换的四个分支simple_expression的语义动作是类型检查的主战场报告写得很细四个分支覆盖了整数和实数的所有组合左操作数类型右操作数类型处理方式integerinteger直接生成三地址码结果 integerrealreal直接生成结果 realintegerreal先生成inttoreal转换左操作数再运算结果 realrealinteger先生成inttoreal转换右操作数再运算结果 real这个表就是「类型转化」要求的具体落地。inttoreal是中间代码里的一条指令用newtemp产生临时变量承接转换结果。复现时注意转换只发生在混合类型运算同类型不插转换指令否则中间代码会冗余得没法看。赋值语句的类型检查在statement - variable assignop expression里检查variable和expression的类型是否相同不同报错。判断语句if expression then要求expression.t Boolean否则type_error。这两条是报告明确点出的检查点别漏。4.3 三地址码的生成与临时变量管理中间代码用三地址码报告要求覆盖赋值、数组、指针、函数调用。emit()是发射指令的函数newtemp产生临时变量t1, t2...enterq(q, expression.name)把实参压进参数队列 q。控制流的翻译是重点// if 语句的三地址码生成 // if expression then statement1 else statement2 // 生成 // if expression.name goto L1 // goto L2 // L1: statement1 的代码 // goto L3 // L2: statement2 的代码 // L3:while语句类似报告里给了Li、Li1、Li2三个标号的分配方式i是标号计数器每次用三个。复现时标号管理要小心嵌套语句里标号不能重用i必须是全局递增的否则内层循环的 goto 会跳到外层去这种 bug 跑起来就是死循环或者跳飞血泪经验。数组访问的中间代码报告没给完整翻译方案但要求覆盖常见做法是x : a[i]翻成先算地址t1 : i * width、t2 : a t1、x : *t2三条。指针类似解引用用*前缀。函数调用的参数传递机制报告要求文档描述常见是值传递实参按顺序enterq进队列调用时按队列顺序取。提示newtemp产生的临时变量名要全局唯一别用局部计数器否则不同语句的 t1 会撞中间代码读起来全是歧义。5. 避坑与排查五个真实会翻车的地方5.1 符号表 name[9] 截断导致变量撞名现象两个不同变量前 8 个字符相同编译后行为异常后一个变量的赋值影响了前一个。原因char name[9]只存 8 个有效字符strcpy或strncpy时超长部分被截符号表里两条记录 name 相同Word_insert查重时误判为已存在返回同一个下标。解决要么把name扩到 32 字节要么在插入前做长度检查超长直接报「标识符过长」错误。课程设计场景下扩长度最省事别为了省内存留这个雷。5.2 FIRST 集迭代顺序不对导致分析表缺项现象预测分析表某些格子该有产生式编号却是 -1语法分析器对合法输入报语法错误。原因FIRST 集计算时按产生式顺序单遍扫描X - Y1 Y2里 Y1 的 FIRST 还没算完就取了拿到空集导致 X 的 FIRST 不全分析表漏填。解决改成不动点迭代反复扫所有产生式直到没有集合变化。加个changed标志一轮下来没变就退出。规模小的话多扫几轮无所谓。5.3 左递归没消干净导致递归下降栈溢出现象语法分析器一跑就栈溢出或死循环分析树无限深。原因表达式文法E - E T的左递归没消除递归下降函数E()第一件事就是调E()直接无限递归。解决严格按左递归消除公式改写E - T E、E - T E | ε。改完检查每个非终结符函数确保没有函数在产生式第一个符号就调自己。5.4 标号计数器作用域错误导致 goto 跳飞现象嵌套 if/while 的中间代码执行时跳到错误位置结果错乱或死循环。原因标号计数器i写成了局部变量每层语句重新从 0 开始分配标号内外层标号重复goto 跳到最近的定义。解决i必须是全局或贯穿整个语义分析的单一计数器每次分配只增不减。生成完中间代码后打印标号分配表核对一遍。5.5 词法行列计数在注释和换行处错位现象报错信息里的行号列号和源文件对不上定位错误困难。原因注释{}和/**/内部有换行扫描注释时没更新line_count或者\r\n被当成两个字符列计数多算。解决注释扫描循环里遇到\n也要line_count读文件时统一处理\r\n为\n。词法单独测试时拿一个带多行注释的源文件验证行号。6. 进阶验证用三个测试程序把编译器逼到边界复现完别急着交拿三个层次的测试程序过一遍比看十遍代码管用。第一个是纯赋值和四则运算验证词法、语法、三地址码生成的主链路通不通第二个带 if/while 嵌套和混合类型运算专门逼类型转换和标号分配第三个故意塞错误——未声明变量、类型不匹配的赋值、注释不闭合——看报错信息能不能定位到行列。验证中间代码对不对我有个笨但有效的办法手工把三地址码翻译回等价的 C 代码跑一遍对比结果。比如t1 : a b、t2 : t1 * c翻成t2 (ab)*c结果对不上就说明某条 emit 的参数顺序错了。这个方法在调simple_expression的四个类型分支时特别管用因为混合类型的inttoreal插错位置手工翻译一眼能看出来。分析树的验证靠改源程序看结构变化把a b * c改成(a b) * c分析树里term和simple_expression的嵌套层次应该跟着变如果两棵树一样说明优先级处理有问题多半是文法改写时把结合性搞反了。还有个小技巧把符号表、常数表、记号流三个输出文件按行对齐打印词法阶段的问题基本无处遁形。记号流的属性值和符号表的下标能对上说明Word_insert的返回值语义没写错对不上就是 5.1 那个截断或者下标 0 歧义的老问题。从那以后我每次碰这种手工编译器项目都强制先把词法五个输出文件跑出来逐个核对再动语法和语义——词法这层地基要是虚的上面盖多高都是玄学调试。希望这份拆解帮到你少走几个我踩过的坑。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。