ARTICLE DETAIL

资讯详情

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

北邮课设实战:手工实现Pascal子集编译器全流程

北邮课设实战:手工实现Pascal子集编译器全流程 简介这份资源是面向计算机专业学生与编译原理学习者的Pascal子集编译器课程设计报告以C手工实现不使用YACC等工具适合正在做编译原理课设或希望系统梳理词法、语法、语义与中间代码生成流程的读者。压缩包内共1个doc文档约952KB内容为完整的课程设计报告涵盖需求分析、总体结构、详细设计与各阶段接口说明。报告围绕Sub_P文法展开词法部分给出记号流数组、符号表及行号列号记录语法部分采用自顶向下分析并建立分析树语义部分结合翻译方案进行类型检查与转化中间代码以三地址码或四元式表示并附有函数调用参数传递机制与寄存器分配策略的讨论。文档还包含团队分工、成绩评定标准与错误处理设计可帮助读者理解编译器各模块的接口定义与数据结构组织方式。目前已有240人学习下载适合作为课设参考与编译流程复盘材料。1. 从一份北邮课设报告说起Pascal 子集编译器到底能跑通什么很多人第一次接触编译原理都是被龙书里那套理论绕晕的——FIRST 集、FOLLOW 集、预测分析表考试会做真让你写一个能跑的编译器就懵了。这份北京邮电大学计算机学院的课程设计报告做的就是一件很实在的事用 C 语言手工实现一个 Pascal 子集编译器不借助 YACC、ANTLR 这类工具从词法分析一路做到中间代码生成。报告里分工明确王才丰负责词法分析和符号表李文星做语法分析李金雨做中间代码生成刘国莅做语义分析和类型计算刘光辰负责测试和文档。整个项目覆盖了词法分析、语法分析、语义分析、中间代码生成四个阶段目标代码部分因为不采用工具所以不强制实现。如果你正在做类似的编译原理课设或者想找一个能照着复现的编译器实战参考这份报告的价值在于它把每个阶段的接口、数据结构、算法描述都写清楚了不是泛泛而谈。它适合刚学完编译原理、需要动手落地一个完整编译器流程的学生也适合想回顾手工构造编译器细节的从业者。2. 词法分析器拆解从字符流到记号流数组的完整链路2.1 为什么选择手工编写词法分析器这份课设明确要求不使用工具词法分析器完全用 C 语言手工实现。常见做法是用 flex 自动生成但手工写的好处是你能真正理解状态转移的每一步。词法分析器的核心任务很明确读入 Pascal 源程序识别出标识符、数字、关键字、运算符过滤注释和间隔符号最终输出一个记号流数组Symbol_stream[1000]和一个符号表数组symbol_table[100]。报告里定义的接口是void lexout(void)没有参数和返回值通过全局数组传递结果。这种设计在课设场景下够用但实际工程中更推荐把输入输出显式化避免全局状态带来的调试困难。词法分析器需要处理的字符类型包括字母、数字、运算符、界符、注释。Pascal 的注释有两种形式{}和/**/。报告里用digit_l和digit_r记录{}的个数来配对用state标志判断当前是否在注释中。关键字识别通过Iskeyword(char[])函数完成返回内部编码mod返回 1or返回 2and返回 3div返回 4其他关键字返回 -1标识符返回 0。这个设计把关键字和标识符的区分放在词法阶段语法阶段就不用再查表了。2.2 符号表与常数表的数据结构设计符号表是词法分析和语义分析的桥梁。报告里定义的结构体如下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];这个结构体记录了变量名、地址、类型、维数、声明行、引用行和块索引。use_row[5]最多记录 5 次引用超过 5 次就覆盖或丢弃这在课设里够用但真实编译器会用链表或动态数组。常数表用char NumList[1000][20]存储count2记录项数。符号表插入函数Word_insert(char[])返回标识符在WordList[]中的下标如果已存在则返回已有下标否则返回 0。这里有个细节返回 0 既表示“插入成功且下标为 0”又表示“插入失败”存在歧义。常见做法是返回 -1 表示失败或者用单独的布尔变量标记。2.3 词法分析器的执行流程与输出文件词法分析器的执行流程可以拆成以下步骤打开源文件将内容全部读入缓冲区buffer[Max]。逐个字符扫描根据当前字符类型进入不同分支。如果是字母继续读取直到非字母非数字得到token调用Iskeyword判断是否为关键字。如果是数字继续读取直到非数字处理整数和小数调用Num_insert插入常数表。如果是运算符或界符直接生成对应记号。如果是{或/*进入注释过滤逻辑跳过注释内容。每识别一个记号写入Symbol_stream数组同时更新行计数、列计数、字符计数。遇到错误时记录错误行号和错误类型继续处理后续字符。输出文件包括符号表.txt、常数表.txt、统计信息.txt、记号流.txt、终结符.txt。这些文件在调试阶段非常有用尤其是记号流.txt可以让你直观看到每个记号的类别编码和属性值。报告里给出的内部编码表如下记号属性值记号属性值记号属性值and1array2begin3boolean4do5else6end7false8function9if10integer11not12of13or14procedure15program15read17real18record19then20true21var22while23write24(25)26:27[28]29;30,31.32Relop33assignop34mulop35addop14digits3840-41id36num35注意program和procedure都编码为 15addop和or都编码为 14num和mulop都编码为 35。这种编码冲突在课设里可能不会暴露问题因为语法分析器会根据上下文区分但严格来说应该保证编码唯一。如果你要复现这个项目建议把编码表重新整理一遍确保每个记号有唯一编码。提示词法分析器单独测试时可以准备几个边界用例空文件、只有注释的文件、包含非法字符的文件、超长标识符、小数和整数混合。这些用例能帮你快速定位状态转移的遗漏。3. 语法分析器实战FIRST 集、FOLLOW 集与预测分析表的构造3.1 FIRST 集和 FOLLOW 集的算法实现语法分析采用自顶向下的 LL(1) 方法。报告里用 C 类封装了 FIRST 集和 FOLLOW 集的计算。FirstGroup类的核心数据结构是int firstgroup[15]和int countinsertFsg函数把某个非终结符的 FIRST 集合并到当前符号的 FIRST 集中。算法描述完全按照课本规则终结符的 FIRST 集是自身如果X-ε把 ε 加入 FIRST(X)如果X-Y1Y2...Yk把 FIRST(Y1) 中除 ε 外的符号加入 FIRST(X)如果 Y1 能推导出 ε继续看 Y2以此类推。FOLLOW 集的计算规则同样来自课本把$放入 FOLLOW(S)如果存在A-αBβ把 FIRST(β) 中除 ε 外的符号放入 FOLLOW(B)如果存在A-αB或A-αBβ且 β 能推导出 ε把 FOLLOW(A) 放入 FOLLOW(B)。报告里用FollowGroup类实现GetFollowGroup()函数在语法分析器初始化时调用一次结果存在内存中供构造分析表使用。这里有个容易翻车的地方FIRST 集和 FOLLOW 集的计算顺序。如果文法有左递归必须先消除左递归再计算否则会死循环。报告里的文法已经处理过但如果你自己改文法一定要先检查左递归。另外ε 的处理要特别小心existed函数判断符号是否已在集合中避免重复插入。3.2 预测分析表的构造与查表逻辑预测分析表的构造算法在报告里描述为先把analysistable初始化为全 -1逐个扫描产生式A-α标号为 i求 FIRST(α)对全部a∈FIRST(α)将analysistable[A][a]赋值为 i若ε∈FIRST(α)则求 FOLLOW(A)对任何b∈FOLLOW(A)将analysistable[A][b]赋值为 i。没有被赋值的区域为 -1表示 error。数据结构方面非终结符表用struct NTsymbol { char symbol[25]; int value; }终结符表用struct Tsymbol { char symbol[25]; int value; }。matchsymbol(char* str)函数从这两张表中查找符号对应的内部编码。产生式存储在int generator[][]中每一行存一个产生式generator[i][0]存放左端字符generator[i][1..10]存放右端字符。查表逻辑是语法分析器的核心循环从输入串中取当前记号查analysistable[栈顶非终结符][当前记号]如果值为 -1 则报错否则用对应产生式右部替换栈顶非终结符。这个过程会构建一棵分析树报告里要求“能够以直观的方式输出分析树”。常见做法是用缩进打印或者括号表示法比如program(id, declarations, subprogram_declarations, compound_statement)。3.3 分析树的输出与源程序变更测试报告里明确要求“改变源程序分析树将发生变化”。这意味着你需要准备至少两个不同的 Pascal 源程序分别跑一遍语法分析对比输出的分析树。一个简单的测试程序可以只包含赋值语句另一个包含 if-then-else 和 while 循环。分析树的输出格式建议用括号嵌套表示每个节点占一行用缩进表示层级。// 分析树节点打印示例 void print_tree(Node* node, int depth) { if (node NULL) return; for (int i 0; i depth; i) printf( ); printf(%s\n, node-symbol); for (int i 0; i node-child_count; i) { print_tree(node-children[i], depth 1); } }这段代码的逻辑很简单先打印当前节点缩进深度由depth控制然后递归打印所有子节点。参数node是当前节点指针depth是当前深度。如果你用 C 的vector存子节点遍历方式类似。输出到文件时把printf换成fprintf即可。注意LL(1) 文法的局限性在于它不能处理左递归和公共左因子。如果你的 Pascal 子集文法包含这些必须先做文法变换。报告里的文法已经处理过但如果你要扩展文法记得先检查这两点。4. 语义分析与中间代码生成翻译模式与类型检查的落地细节4.1 语法制导翻译模式的设计语义分析的核心是翻译模式。报告里给出了完整的翻译方案覆盖了 program、identifier_list、declarations、type、subprogram_declarations、compound_statement、statement、expression 等非终结符。每个产生式后面跟着语义动作用花括号括起来。比如program → {t:mktable(nil); push(t,tableptr); push(0,offset); f:mkfile(nil); push(f,fileptr); q:mstack()} program id ( identifier_list ) ; declarations subprogram_declarations compound_statement这段语义动作做了几件事创建符号表、压入栈、初始化偏移量为 0、创建文件表、初始化队列。mktable产生新符号表push把符号表指针和偏移量压栈mkfile创建文件表mstack初始化队列。这些操作在进入 program 时执行一次为后续的声明和语句处理做准备。identifier_list的语义动作负责把标识符插入符号表并更新偏移量identifier_list → id {enter(top(tableptr), id.iPos, identifier_list.t, top(offset)); top(offset):top(offset)identifier_list.width; identifier_list.width:identifier_list.width; identifier_list.t:identifier_list.t} identifier_listenter函数把变量名、类型、偏移量插入符号表。top(tableptr)取当前符号表栈顶top(offset)取当前偏移量。每插入一个变量偏移量增加该变量的宽度。identifier_list.t和identifier_list.width是从type传递过来的类型和宽度。4.2 类型检查与类型转化类型检查是语义分析的重要任务。报告里列出了三条规则赋值语句检查等号左右两边类型是否相同判断语句检查表达式是否为 boolean 型基本运算检查运算对象是否为同一类型必要时做类型转化。类型转化的处理在simple_expression的语义动作里simple_expression → addop term simple_expression1 { if simple_expression1.tinteger and term.tinteger then begin simple_expression.name:newtemp; emit(simple_expression.name : simple_expression1.name int addloplexeme term.place); simple_expression.t:integer end else if simple_expression1.treal and term.treal then begin simple_expression.name:newtemp; emit(simple_expression.name : simple_expression1.name real addloplexeme term.name); simple_expression.t:real end else if simple_expression1.tinteger and term.treal then begin u:newtemp; emit(u : inttoreal simple_expression1.name); emit(simple_expression.name : u real addloplexeme term.name); simple_expression.t:real end else if simple_expression1.treal and term.tinteger then begin u:newtemp; emit(u : inttoreal term.name); emit(term.name : simple_expression1.name real addloplexeme u); simple_expression.t:real end else simple_expression.name:type_error }这段代码处理了四种情况整数加整数、实数加实数、整数加实数、实数加整数。整数加实数时先用inttoreal把整数转成实数再做实数加法。newtemp生成临时变量emit输出三地址码。type_error表示类型不匹配报错。4.3 中间代码生成与四元式输出中间代码采用三地址码或四元式。报告里用emit函数输出中间代码格式类似x : y op z。中间代码需要覆盖赋值、数组、指针、函数调用。赋值语句的语义动作是emit(variable.name : expression.name)。数组访问需要计算下标偏移指针需要解引用。函数调用的参数传递机制在报告里没有详细展开但常见做法是用栈传递参数调用前把实参压栈被调用函数从栈中取参。中间代码的输出文件建议单独保存方便后续目标代码生成阶段使用。如果你要实现目标代码生成可以把三地址码逐条翻译成汇编指令。寄存器分配策略可以用简单的图着色或线性扫描课设场景下用固定寄存器分配也够用。提示类型检查时数组下标必须是整数指针解引用必须是指针类型函数调用的实参个数和类型必须与形参匹配。这些检查在语义分析阶段完成不要留到运行时。5. 避坑与排查手工实现编译器时最容易翻车的五个地方5.1 词法分析器把关键字识别成标识符现象源程序里的program、begin、end被当成普通标识符语法分析器报“意外的标识符”。原因Iskeyword函数的比较逻辑有问题或者关键字数组的初始化不完整。报告里关键字数组有 24 个但 Pascal 的关键字不止这些比如const、type、label等没有包含在内。解决先打印Iskeyword的返回值确认每个关键字的编码是否正确。如果关键字数组不全根据 Pascal 标准补充完整。另外注意大小写Pascal 关键字不区分大小写比较前统一转小写或大写。5.2 FIRST 集和 FOLLOW 集计算死循环现象程序在计算 FIRST 集或 FOLLOW 集时卡死CPU 占用率飙升。原因文法存在左递归或者 FIRST 集计算时没有正确终止条件。比如A-Aα这样的产生式会导致无限递归。解决先检查文法是否有左递归如果有用标准算法消除左递归。然后在 FIRST 集计算中加入终止条件如果当前符号的 FIRST 集没有变化停止迭代。报告里的existed函数就是用来判断符号是否已在集合中避免重复插入。5.3 预测分析表冲突现象构造预测分析表时同一个单元格被赋值多次后赋的值覆盖了先赋的值。原因文法不是 LL(1) 文法存在公共左因子或左递归导致 FIRST 集有交集。解决提取公共左因子消除左递归。如果无法消除考虑改用 LR 分析方法。报告里建议“可以用手工的方法也可以用 YACC 方法但是不建议大家采用”说明老师希望你们手工处理文法冲突。5.4 符号表插入返回 0 的歧义现象Word_insert返回 0 时无法判断是“插入成功且下标为 0”还是“插入失败”。原因函数设计时用 0 表示失败但下标 0 也是合法值。解决把返回值改为 -1 表示失败或者增加一个输出参数int* index返回下标函数返回值只表示成功或失败。报告里的设计在课设场景下可能不会暴露问题但如果你要扩展功能建议改掉。5.5 中间代码生成时临时变量命名冲突现象生成的中间代码里多个临时变量用了同一个名字导致后续优化或目标代码生成时出错。原因newtemp函数没有正确递增计数器或者计数器被重置。解决newtemp应该用一个全局计数器每次调用递增生成t1、t2、t3这样的唯一名字。不要用局部变量做计数器否则每次进入函数都会重置。报告里没有给出newtemp的实现但这是中间代码生成的关键函数必须保证唯一性。6. 从课设到可运行验证编译器正确性的三个进阶技巧6.1 用边界用例覆盖词法分析的盲区词法分析器的正确性不能只靠一个 Hello World 程序验证。我一般会准备一组边界用例覆盖以下场景用例类型输入示例预期行为空文件空输出空记号流不报错只有注释{ comment }输出空记号流不报错嵌套注释{ outer { inner } }Pascal 不支持嵌套注释应报错或按最外层匹配超长标识符30 个字符的标识符截断到 8 个字符或报错非法字符报错记录行号和列号小数3.14识别为实数插入常数表小数点开头.5根据文法决定是否合法把这些用例跑一遍对比记号流.txt和符号表.txt的输出能发现大部分词法分析的问题。6.2 用语法分析树验证文法覆盖度语法分析树的输出是验证文法覆盖度的最好工具。准备三个源程序第一个只包含赋值语句第二个包含 if-then-else第三个包含 while 循环和函数调用。分别跑语法分析检查分析树是否完整覆盖了所有语法结构。如果某个结构没有出现在分析树中说明文法或分析表有问题。// 语法分析树节点结构示例 typedef struct TreeNode { char symbol[25]; struct TreeNode* children[10]; int child_count; } TreeNode; // 递归打印分析树 void print_parse_tree(TreeNode* root, int depth) { if (root NULL) return; for (int i 0; i depth; i) printf( ); printf(%s\n, root-symbol); for (int i 0; i root-child_count; i) { print_parse_tree(root-children[i], depth 1); } }这段代码的逻辑是深度优先遍历先打印当前节点再递归打印子节点。depth控制缩进child_count记录子节点数量。输出到文件时把printf换成fprintf文件指针作为参数传入。6.3 用中间代码反推语义正确性中间代码是语义分析的直接产物。检查中间代码时重点看以下几点赋值语句的左右类型是否一致算术运算是否插入了必要的inttorealif 语句是否生成了正确的跳转标签while 循环是否生成了回跳指令。如果中间代码里出现了type_error说明类型检查发现了问题需要回到语义分析阶段排查。我自己的习惯是每次改完语义动作先跑一个最简单的赋值语句看中间代码是不是x : y这种形式。然后逐步增加复杂度加算术运算、加 if、加 while。每加一个结构检查中间代码的标签编号是否连续临时变量是否唯一。从那以后我每次改语义动作都强制走一遍这个流程能省下大量调试时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表