简介:本资源是西安电子科技大学《编译原理》课程配套上机实践报告,面向计算机专业本科生及编译技术初学者,聚焦词法/语法分析器与轻量级数据库管理系统(DBMS)的协同设计与实现。报告完整覆盖项目概况、Lex/Yacc编译流程(含正规式定义与AST构建)、DBMS物理结构(数据文件与索引设计)及核心功能(建库/表、增删改查、事务管理),并附有VC++6.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归约为一个conditions
Yacc 默认采用“移进优先”,会导致(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个。问题在于:当输入是"DATABASE"(9字符)时,DATABASES规则因长度不够无法匹配,应匹配DATABASE。但如果DATABASES规则写在DATABASE前面,且输入是"DATABASES"(10字符),则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(yytext+1); yylval.sval[strlen(yylval.sval)-1] = '\0'; return STRING; } \'([^\\\']|\\.)*\' { yylval.sval = strdup(yytext+1); 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 0 | 0 $accept : . sts $end | 状态0:$accept(起始符号)的点在sts前,等待读入sts |
shift 3 | CREATE shift 3 | 当前状态遇到CREATE,执行移进,跳转到状态3 |
reduce 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中未声明%union,Yacc 默认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.h,Lex 也能安全访问yylval.sval。
从那以后我每次写.y文件,都强制走一遍yacc -v+grep "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)换来的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取