
简介本资源是西安电子科技大学《编译原理》课程配套上机实践报告面向计算机专业本科生及编译技术初学者聚焦词法/语法分析器与轻量级数据库管理系统DBMS的协同设计与实现。报告完整覆盖项目概况、Lex/Yacc编译流程含正规式定义与AST构建、DBMS物理结构数据文件与索引设计及核心功能建库/表、增删改查、事务管理并附有VC6.0环境下的代码实现思路与上机心得反思。压缩包为单个82KB Word文档.doc结构清晰含目录、分工说明、分模块技术方案与总结建议便于对照学习与课程作业参考。已有1331人学习下载适合巩固编译原理理论、理解前端分析与后端系统集成、开展课程设计复现与拓展的学生使用。1. 编译原理上机报告为什么一份“未完成”的词法语法实现反而暴露了教科书里从不讲的解析器真实行为边界你见过最接近生产级的编译原理课设往往不是功能最全的那个而是在ID和NUMBER冲突时崩溃、在单引号转义上卡死、在Name规则里把数字吞掉还报错的那份——就像这份来自2011年西安电子科技大学的《DBMS 的设计与实现》上机报告。它没实现执行引擎没写查询优化甚至 DBMS 物理结构只留了标题和空行但它用mylexer.l里一行被注释掉的规则、用myparser.y中sts: sts st {printf(Operation is ok\n);}| /* empty */ ;末尾那个孤零零的问号赤裸裸地展示了Lex/Yacc 工具链在真实语境下的脆弱性与诚实性。这不是一份“作业交差”文档而是一份编译器构建过程中的故障日志汇编它记录了当正规式优先级没对齐、终结符声明遗漏、空产生式语义未定义时整个解析流程如何像多米诺骨牌一样逐层失效。搞懂它不是为了复现一个玩具 DBMS而是为了看清——为什么我们写的 parser 总在看似无关的位置报错为什么yylval传参会静默失败为什么AND和OR的结合性声明必须写在%left而不能靠代码顺序这些问题的答案不在龙书第4章的推导表里而在yyparse()返回值为1时yyerror()打印出的那一行模糊提示背后。这份报告的价值正在于它用血泪经验把编译原理从黑匣子拆成了可触摸、可调试、可重演的机械结构。2. 词法分析器Lex 规则不是正则式列表而是一张有向优先级图Lex 的.l文件表面看是正则式 动作的线性罗列但它的实际执行模型远比这复杂。mylexer.l中的规则顺序、模式覆盖关系、以及yytext的生命周期管理共同构成了一张隐式的匹配优先级有向图。一旦这张图出现环、断连或歧义词法分析器就会进入不可预测状态——而这正是报告中“Name 将 NUMBER 截获”问题的根源。2.1 正规式冲突的本质最长匹配 先到先得的双重约束看这段关键规则[0-9] {return NUMBER;} [a-zA-Z][_0-9a-zA-Z()\.\*]* {printf(%s,yytext);return Name;} [a-zA-Z][_a-zA-Z0-9] {return ID;}表面看[0-9]应该匹配所有纯数字串ID匹配字母开头的标识符。但问题出在第二条规则[a-zA-Z][_0-9a-zA-Z()\.\*]*。它允许下划线后接数字_123也允许点号和星号table.field、func.*但更致命的是——它没有锚定结尾。当输入是123abc时Lex 会尝试匹配[0-9]→ 匹配123长度3[a-zA-Z][...]→ 首字符1不满足[a-zA-Z]跳过[a-zA-Z][...]→ 同样跳过→ 正常返回NUMBER但当输入是abc123时[0-9]→ 首字符a不匹配跳过[a-zA-Z][_0-9a-zA-Z()\.\*]*→a匹配接着b、c、1、2、3全部在字符集中 → 匹配整个abc123返回Name[a-zA-Z][_a-zA-Z0-9]→ 因为 Lex按规则顺序尝试且一旦某条规则成功匹配就立即返回不再检查后续规则所以这条永远不会触发提示Lex 的匹配策略是「在所有能匹配当前输入前缀的规则中选最长的那个若多个规则匹配长度相同则选源码中位置最靠前的那个」。这里Name规则不仅长度更长覆盖了ID的全部字符集而且位置在ID之前彻底封死了ID的生效路径。2.2 单引号处理为什么不是字面量而是语法陷阱报告中明确提到“在识别单引号时由于没有使用斜线标示导致无法识别”。看原始规则 {}这行代码在 Lex 中根本不会被编译通过。原因在于Lex 的规则部分要求每个模式必须是有效的正则表达式而连续三个单引号在 Lex 语法中会被解释为第一个开始一个字符字面量第二个是字面量内容即单引号字符本身第三个被当作字面量结束符 → 合法匹配单个但问题来了Lex 要求字面量内的字符必须是 ASCII 可见字符而单引号本身是分隔符直接写会导致解析器将第二个误认为字面量结束第三个成为孤立符号引发语法错误。正确写法必须转义\ {return SINGLE_QUOTE;} // 匹配单个单引号 {return SINGLE_QUOTE;} // 用双引号包裹避免转义而报告中那行 {}实际上是开发者试图匹配三个单引号如 SQL 中的转义单引号但 Lex 根本不支持这种写法。真正要匹配两个连续单引号应写为 {return DOUBLE_SINGLE_QUOTE;}2.3yylval传递机制为什么printf(%s,yytext)能工作但yylval却常为空mylexer.l中有这样一句%{ #include myparser.h #include define.h %} ... [a-zA-Z][_0-9a-zA-Z()\.\*]* {printf(%s,yytext);return Name;}这里printf能打印yytext说明词法分析器确实捕获到了字符串内容。但Name对应的 token 值比如#define Name 257被返回后在语法分析器中如何获取这个字符串答案是yylval—— 一个由 Yacc 定义、Lex 填充的联合体union。但在本报告的代码中yylval的填充完全缺失。标准 Lex/Yacc 协作流程要求在myparser.h或%union声明中定义yylval类型例如%union { int ival; char *sval; } %token sval Name ID %token ival NUMBER在 Lex 动作中显式赋值[a-zA-Z][_0-9a-zA-Z()\.\*]* { yylval.sval strdup(yytext); return Name; } [0-9] { yylval.ival atoi(yytext); return NUMBER; }而本报告的mylexer.l中没有任何yylval赋值语句。这意味着当parser收到Nametoken 时yylval.sval是未初始化的野指针后续strcpy或printf极易导致段错误。这也是为什么报告中select_st规则里table_field: field | table.field;无法正确拼接字段名——field的字符串值从未被安全传递过来。3. 语法分析器Yacc 的产生式不是数学推导而是状态机驱动的归约协议Yacc或 Bison生成的语法分析器本质是一个确定性下推自动机DPDA其核心是state状态、stack符号栈、lookahead向前看符号三者协同的归约决策。myparser.y中那些看似随意的| /* empty */ ;和sts: sts st实则是这个状态机在处理空语句、多语句序列时的关键控制点。忽略它们的语义等于让自动机在无指令状态下乱走。3.1 空产生式/* empty */不是可有可无的占位符而是递归归约的启动开关看sts的定义sts: sts st {printf(Operation is ok\n);} | /* empty */ ;这里的/* empty */绝非注释而是 Yacc 中声明 ε-产生式空产生式的标准写法。它告诉解析器“sts可以推导出空串”。这在处理多条 SQL 语句如CREATE TABLE t(id INT); SELECT * FROM t;时至关重要。如果没有| /* empty */sts就只能是sts st即必须至少有一个st。那么当输入为空比如用户只敲回车时yyparse()会立即失败因为无法归约出sts。而有了空产生式解析器就能初始状态栈底为sts输入为空 → 直接用sts → ε归约接受空输入输入st1; st2;先sts → ε再sts → sts st归约st1再sts → sts st归约st2最终sts为栈顶输入结束 → 接受那个孤零零的很可能是开发者调试时误留的字符但它恰好暴露了一个事实Yacc 对空产生式的语法极其敏感多一个字符、少一个分号都会导致.y文件编译失败。真正的写法必须是sts: sts st { printf(Operation is ok\n); } | /* empty */ ;3.2 结合性声明%left AND OR不是语义修饰而是解决移进-归约冲突的硬编码指令myparser.y中有%left AND OR这行代码的物理意义是向 Yacc 的冲突解决器注入一条强制归约策略。考虑条件表达式a AND b OR c。其文法可能为conditions: conditions AND conditions | conditions OR conditions | condition此时当解析器看到a AND b OR时面临经典冲突移进OR将OR压栈等待下一个conditions归约a AND b将已读的a AND b归约为一个conditionsYacc 默认采用“移进优先”会导致(a AND b) OR c被解析为a AND (b OR c)错误。而%left AND OR告诉 Yacc“当AND和OR出现在同一层级时按左结合性归约并且AND和OR优先级相同”。于是解析器强制选择归约确保a AND b先被归约为一个单元再与OR c组合。注意如果写成%right AND OR则会得到右结合结果极少用如果漏掉此声明Yacc 会在编译时报告conflicts: 1 shift/reduce并静默选择移进——这就是为什么很多初学者写的 parser 在复杂条件上逻辑错乱却找不到原因。3.3yyerror()的沉默为什么语法错误不报具体位置myparser.y的main()函数中int main(void){ int n 1; mylexer lexer; myparser parser; if (parser.yycreate(lexer)) { if (lexer.yycreate(parser)) { n parser.yyparse(); } } return n; }这里完全没有自定义yyerror()函数。Yacc 默认的yyerror(const char *s)仅打印s字符串通常是syntax error不提供出错的行号、列号、当前 token 或输入上下文。这意味着当INSERT INTO t VALUES (1, abc缺右括号时你只会看到syntax error然后被迫用printf在每个产生式动作里打日志来定位。要获得有效错误信息必须显式定义extern int yylineno; // Lex 通常提供此变量 void yyerror(const char *s) { fprintf(stderr, Line %d: %s\n, yylineno, s); }并在mylexer.l中启用行号计数%{ #include stdio.h int yylineno 1; %} %% \n { yylineno; }否则调试语法分析器就是一场盲人摸象。4. 避坑指南Lex/Yacc 项目中最容易踩的 5 个深坑及现场抢救方案这些不是理论假设而是从本报告的“存在问题及解决方法”和代码细节中反向工程出的真实翻车现场。每一条都对应一次make失败、一次段错误、一次无限循环或一次yyparse()永远返回1的绝望。4.1 现象yyparse()返回1但yyerror()什么也不输出原因yyerror函数未定义或定义在.y文件外但未声明extern导致链接时使用默认弱符号空实现。解决在.y文件顶部%{ %}块中添加%{ #include stdio.h extern int yylineno; void yyerror(const char *s) { fprintf(stderr, Parse error at line %d: %s\n, yylineno, s); } %}并在mylexer.l中确保yylineno被正确维护。4.2 现象ID永远匹配不到所有标识符都被Name吞掉原因Name规则[a-zA-Z][_0-9a-zA-Z()\.\*]*的字符集包含数字和特殊符号且位置在ID规则之前导致 Lex 优先匹配更长的Name。解决重构规则顺序与范围// 严格按优先级降序排列 [a-zA-Z][a-zA-Z0-9_]* { yylval.sval strdup(yytext); return ID; } [a-zA-Z][_0-9a-zA-Z()*.]* { yylval.sval strdup(yytext); return Name; } [0-9] { yylval.ival atoi(yytext); return NUMBER; }并确保ID规则在Name之前。4.3 现象CREATE DATABASE db1;被识别为CREATE DATABASES而非CREATE DATABASE Name原因DATABASEStoken 声明在DATABASE之前且DATABASES是DATABASE的前缀。Lex 匹配最长前缀DATABASES长度7 DATABASE长度8等等DATABASE是9个字符但DATABASES是10个。问题在于当输入是DATABASE9字符时DATABASES规则因长度不够无法匹配应匹配DATABASE。但如果DATABASES规则写在DATABASE前面且输入是DATABASES10字符则DATABASES匹配成功。真正的风险在于DATABASE和DATABASES的共用前缀。解决将更长的关键词放在前面确保精确匹配DATABASES {return DATABASES;} DATABASE {return DATABASE;} TABLES {return TABLES;} TABLE {return TABLE;}4.4 现象SELECT * FROM t WHERE id 1;中id 1被解析为condition: comp_left comp_op comp_right但comp_right的NUMBER值始终为 0原因NUMBERtoken 的yylval.ival未被赋值yylval是未初始化的栈变量值为随机内存垃圾。解决在mylexer.l中为所有需传值的 token 显式赋值[0-9] { yylval.ival atoi(yytext); return NUMBER; }并在.y文件中声明%token ival NUMBER。4.5 现象INSERT INTO t VALUES (1, abc);中单引号字符串abc被切分成、abc、三个 token原因Lex 规则中缺少对字符串字面量的完整匹配abc被.任意字符或ID规则分别匹配。解决添加字符串匹配规则用状态机处理引号内转义\([^\\\]|\\.)*\ { yylval.sval strdup(yytext1); yylval.sval[strlen(yylval.sval)-1] \0; return STRING; } \([^\\\]|\\.)*\ { yylval.sval strdup(yytext1); yylval.sval[strlen(yylval.sval)-1] \0; return STRING; }并确保该规则在ID和Name之前。5. 从 yacc.debug 到 gdb如何把一份“半成品报告”变成可调试的编译器沙盒这份报告最珍贵的遗产不是它实现了什么而是它暴露了 Lex/Yacc 工具链的全部可观测接口.l和.y是源码y.tab.c和lex.yy.c是生成的 C 代码y.output是状态机图谱yacc -v生成的y.output文件就是打开这个黑匣子的第一把钥匙。我至今保留着一个习惯任何新的.y文件写完第一件事不是make而是yacc -v myparser.y然后用less y.output查看状态转换表。5.1y.output读懂状态机的 3 个关键字段运行yacc -v myparser.y后生成的y.output文件本质是 LR(1) 分析表的文本化描述。重点关注字段示例含义state 00 $accept : . sts $end状态0$accept起始符号的点在sts前等待读入stsshift 3CREATE shift 3当前状态遇到CREATE执行移进跳转到状态3reduce 5; reduce 5当前状态遇到;执行归约使用第5条产生式在本报告的myparser.y中如果sts的空产生式未正确定义y.output里会出现类似state 1: $accept : sts . $end后无reduce动作只有error—— 这直接告诉你语法无法接受空输入。5.2y.tab.c在 C 层调试yyparse()的归约路径yacc生成的y.tab.c是纯 C 代码可直接用gdb调试。设置断点观察关键变量gcc -g y.tab.c lex.yy.c -o parser gdb ./parser (gdb) break yyparse (gdb) run test.sql (gdb) print yyn (gdb) print yychar (gdb) print yyss # 状态栈 (gdb) print yyvs # 值栈yyn是当前执行的产生式编号对照y.output中的编号就能知道哪条规则正在归约。当yyn 7假设st: create_st ;是第7条而yychar ;说明归约成功若yychar 则说明空格未被 Lex 过滤导致yyparse卡在等待;。5.3#define YYSTYPE的陷阱为什么int类型的yylval必须全局一致报告中.y文件有%include { #ifndef YYSTYPE #define YYSTYPE int #endif }这行代码埋下巨大隐患YYSTYPE在.y中定义为int但在.l中yylval被当作union使用因#include myparser.h而myparser.h由 Yacc 生成其YYSTYPE定义取决于%union。若.y中未声明%unionYacc 默认YYSTYPE为int但.l中若写了yylval.sval ...则编译失败sval不存在。正确做法是删除#define YYSTYPE int改用显式%union%union { int ival; char *sval; } %token sval Name ID %token ival NUMBER %type sval field table这样 Yacc 会生成带union的myparser.hLex 也能安全访问yylval.sval。从那以后我每次写.y文件都强制走一遍yacc -vgrep state [0-9] y.output | head -20确认起始状态和空产生式存在写.l时第一行必加%option yylineno并在每个 token 动作里加fprintf(stderr, TOKEN: %s [%s]\n, yytext, yytname[yytoken]);make之前先nm y.tab.o | grep yyerror确认符号已定义。这些不是教条而是从这份 2011 年的报告里用Segmentation fault (core dumped)换来的后悔药。希望帮到你。本文还有配套的精品资源点击获取