从bison报错到可运行语言:Crowbar与Diksam工程拆解

发布时间:2026/10/27 19:04:30

从bison报错到可运行语言:Crowbar与Diksam工程拆解 简介这份资源围绕「自制编程语言」展开是一份面向编译原理学习者、语言爱好者以及想动手实现解释器与编译器的开发者的PDF资料适合具备一定C语言与数据结构基础、希望从设计到实现完整走通一遍的中高级学习者。压缩包内共1个PDF文件整体约2.38MB便于在电脑或平板上直接阅读与检索无需配置额外运行环境。目前已有870人学习下载具备一定的参考热度。内容从编程语言的设计原则切入讨论语法、语义与可读性、效率之间的权衡再延伸到实现环节涉及yacc/lex、bison/flex等解析器与词法分析器生成工具以及MinGW在Windows下搭建类Unix编译环境的做法。资源还以Crowbar和Diksam两个自制语言实现为线索给出从0.1到0.4版本逐步演进的示例涵盖垃圾回收、运行时环境与资源管理并说明Linux下make.sh、Windows下make.bat的构建差异以及Cygwin与MinGW的取舍。读者可借此理解编译器与解释器的分工掌握从词法、语法分析到代码生成的基本链路为自行设计小型语言打下基础。1. 从一句 m4 报错说起这套资料到底把「自制编程语言」拆成了什么几乎每个想动手做一门编程语言的人都在 bison 的报错里翻过车。我见过最常见的开场白不是「语法怎么写」而是屏幕上跳出一行m4: cannot open Files: No such file or directory然后人就在这条命令前面卡了两天。这套资料包解决的正是这个落差它不讲抽象的语言理论而是把「一门编程语言从零到能跑」拆成四个可直接编译的实物——两个热身项目、一个动态类型解释器 Crowbar 的 0.1 到 0.4 四个版本、一个静态类型编译型语言 Diksam 的 0.1 到 0.4 四个版本。适合两类人一类是学过编译原理但没真正编过一次译的新手照着目录就能把第一个.crb脚本跑起来另一类是想看「解释器和编译器在工程上差在哪」的熟手直接读 0.4 版源码里的 GC 和执行器。它不负责让你成为语言设计专家负责让你手里有一个能改、能加语法、能调试的完整工程。2. 目录结构拆解四条学习线对应四种实现路径2.1 压缩包里其实是四个难度梯度解压之后不要急着找入口先看目录命名规律。整个包不是一个大工程而是按「教学进度」切开的若干独立子目录每个子目录自带 Makefile、源码和测试脚本可以单独编译单独跑。这正是它比一整坨工程友好的地方——你不需要理解全部只需要理解你当前打开的那一层。目录语言形态实现方式依赖工具定位mycalc/mycalc_ex四则运算表达式yacc/lex 生成解析器bison、flex、gcc热身先跑通.y和.l的配合llparser/llparser_ex带语句块的简单语言手写递归下降LL只要 gcc不依赖生成器理解「解析器是人写的」crowbar_book_0_1~0_4动态类型脚本语言解释执行lex yacc 生成bison、flex、gcc主线从词法到执行栈到 GCdiksam_book_0_1~0_4静态类型语言编译成字节码虚拟机执行bison、flex、gcc、oniguruma进阶看类型检查和编译器后端mycalc和llparser的存在很关键很多人跳过直接上 Crowbar结果在.y文件里看到一堆$$、$1、$3就懵了。这两个目录就是给你练手的一个告诉你怎么让工具生成解析器一个告诉你解析器不生成也能手写两条路都走一遍再看 Crowbar 的源码就不会有黑匣子感。提示_ex后缀的目录通常是「带练习答案」或「扩展版」和同名无后缀目录对照着看diff 一下就能看出这一步到底改了什么比读文字描述有效得多。2.2 Crowbar 从 0.1 到 0.4一个解释器是怎么长出来的Crowbar 这条线是整个包里最值得逐版对着读的部分。四个版本不是四个不同项目而是同一个解释器的四个快照每个快照对应书上的一章功能是严格递增的。常见做法是0.1 只做最朴素的词法分析、语法分析和表达式求值变量表是一个简单的数组或链表到了 0.2最明显的变化是引入了 GC垃圾回收因为字符串和对象开始动态分配不回收就会一路吃内存0.3、0.4 继续往上加数组、原生函数、更完整的错误信息。版本号在这里不是装饰它决定了你打开源码时该关注哪个文件。0.1 的execute.c里基本只有表达式和几个语句的分支到了 0.2 之后string_pool.c和 GC 相关的标记清除逻辑会变成重点。我的建议是第一次读只读 0.1把「输入一行、解析成树、求值输出」这条链路走通第二次读 0.2专门盯 GC 的标记和清扫两个函数0.3 和 0.4 当成增量补丁看用 diff 工具直接对比 0.1 和 0.4 的目录看新增了哪些.c文件比从头读快得多。2.3 Diksam 的目录变化藏着一个架构决策Diksam 这条线有个很容易被忽略的细节0.1 和 0.2 版本的构建入口在compiler目录下而 0.3 和 0.4 版本的构建入口挪到了main目录下compiler变成了被引用的子模块。这个变化不是随便挪的它反映的是从「一个把源码编译成中间产物的程序」演进到「编译器 虚拟机 运行时」的完整形态。也就是说0.1 阶段的 Diksam 更像一个「翻译器」你给它源码它吐出字节码或者直接执行到了 0.3 之后主程序main负责驱动整个流程compiler只负责前端后面接的是独立的虚拟机执行循环。这个分层是静态类型语言绕不开的一步因为类型检查、符号表、字节码生成这些事必须和运行时解耦否则后面加优化根本没法下手。读 Diksam 的时候先看main下的入口函数怎么把「读文件 → 调编译器 → 交给虚拟机」串起来再回头看compiler内部的实现顺序反过来会很痛苦。3. 环境搭建与构建链路MinGW、bison、flex 三件套怎么配3.1 Windows 上装 MinGW 并把它变成 gmakeCrowbar 和 Diksam 的构建脚本里Linux 下用的是make.sh内部调用makeWindows 下用的是make.bat内部调用gmake。这里的gmake不是另一个软件就是 MinGW 自带的mingw32-make.exe只是项目里的 Makefile 写死了这个名字所以你得手动把它改成gmake.exe或者每次敲全名。MinGW 的安装走官方安装器下载mingw-get-setup.exe运行时它会问安装目录默认是C:\MinGW建议改到一个不含空格和中文的路径比如D:\MinGW。进入 MinGW Installation Manager 之后在包列表里找到mingw32-base右键 Mark for Installation然后点左上角 Installation → Apply Changes等它把 gcc 和基础工具链拉完。# 安装完成后进入 MinGW 的 bin 目录 cd D:\MinGW\bin # 把 mingw32-make.exe 复制一份改名为 gmake.exe copy mingw32-make.exe gmake.exe # 验证版本能打印出版本号就说明 PATH 生效了 gmake --version gcc --version这段操作的意义在于make.bat里写的是gmake如果你只装了 MinGW 没改名双击脚本会直接报「不是内部或外部命令」。改完之后还要确认D:\MinGW\bin已经进了系统 PATH否则在别的目录下敲gmake依然找不到。参数上没什么可调的--version能出版本号、gcc能编译一个 hello world就说明基础链路通了。3.2 bison 和 flex装到带空格的路径会直接翻车bison 和 flex 是这套资料的核心依赖.y文件靠 bison 生成y.tab.c和y.tab.h.l文件靠 flex 生成lex.yy.c。Windows 下常见做法是装 GnuWin32 提供的版本一路 next 就行。这里有一条血泪经验安装路径千万不要带空格。GnuWin32 的安装器默认会往C:\Program Files (x86)\GnuWin32里装而 bison 在运行时会调用m4宏处理器并且把自身路径拼进去。路径里有空格m4就会把Program和Files当成两个独立参数于是报出这种错m4: cannot open Files: No such file or directory m4: cannot open D:\Program: No such file or directory m4: cannot open (x86)\GnuWin32\Bison/share/bison/m4sugar/m4sugar.m4: No such file or directory看到这几行不要怀疑 bison 装坏了就是路径问题。解决办法是把 GnuWin32 装到C:\GnuWin32这种纯英文无空格的路径下然后把C:\GnuWin32\bin加到 PATH 最前面重启命令行窗口再试。装完之后用下面两条命令验证能出版本号即可bison --version flex --version # 如果系统里装了多个 bison用 where 确认实际调用的是哪一个 where bisonwhere这一步很关键很多人 PATH 里同时有 MinGW 自带的 bison 和 GnuWin32 的 bison报错信息却指向你没在用的那个排查半天。确认路径优先级之后.y文件才能正常生成 C 代码。3.3 Linux 和 Cygwin 两条路线怎么选Linux 下最省事bison、flex、make、gcc基本都在发行版仓库里一条包管理命令装完就能直接进目录执行./make.sh。真正麻烦的是「Windows 但想要 Linux 那套工具」的场景这时有两条路。第一条是 Cygwin。安装时选setup-x86_64.exe32 位选setup-x86.exeInstall from InternetRoot Directory 和 Local Package Directory 保持默认即可连接方式选 Direct Connection。包选择界面里必须手动勾上三个Devel分类下的make、Interpreters分类下的m4、以及bison和flex。装完之后从 Cygwin64 Terminal 里进去编译.y、.l这些文件的换行符和路径分隔符都能被正确处理。第二条是 MinGW GnuWin32 的组合也就是 3.1 和 3.2 里那套。两者的区别很实际Cygwin 编译出来的.exe依赖cygwin.dll你把它单独拷到别的机器上、或者在一个纯 cmd 窗口里跑就会报「找不到 cygwin.dll」MinGW 编译出来的是原生 Windows 程序不依赖这个 DLL但代价是它对 Unix 风格的路径和换行符支持没 Cygwin 那么顺。我的做法是先把 MinGW 路线跑通因为产出的 exe 更干净只有在某个版本死活编不过、又确认是工具链差异时才切 Cygwin 对照。3.4 Diksam 0.4 需要的 oniguruma 怎么编Diksam 的后期版本引入了正则表达式支持依赖 oniguruma 这个库。Linux 下的流程是标准的 autoconf 三步wget http://example.invalid/onig-5.9.4.tar.gz # 实际地址以资料包内说明为准 tar -xvf onig-5.9.4.tar.gz cd onig-5.9.4 ./configure make sudo make installWindows 下用 Cygwin 的 bash 执行同样的./configure make通常能过但make install经常在这一步断掉报ranlib.exe: /usr/local/lib/libonig.a: No such file。原因是 MinGW 的ranlib对 Cygwin 生成的路径不认账。绕过办法是不走make install手动把静态库拿出来# 在 onig-5.9.4 目录下执行完 make 之后 cd .libs ranlib libonig.a # 把 libonig.a 拷到 MinGW 的 lib 目录头文件拷到 include cp libonig.a /d/MinGW/lib/ cp ../oniguruma.h /d/MinGW/include/这样处理后Diksam 的 Makefile 里-lonig就能正常链接。这一步没做的话编译到链接阶段会报一堆undefined reference to onig_new之类的错误看起来像是源码问题其实只是库没就位。4. 核心模块拆解从 .y/.l 到执行器的一条完整链路4.1 Makefile 的生成顺序决定了你会不会遇到 y.tab.h 缺失打开 Crowbar 任意版本的 Makefile最值得先看的不是编译选项而是目标之间的依赖顺序。lex 生成的lex.yy.c里有一行#include y.tab.h而y.tab.h是 bison 处理.y文件时才产出的。如果 Makefile 没写清楚这个先后关系或者你手动敲gcc跳过了 make就会直接撞上mycalc.l:3:19: error: y.tab.h: No such file or directory。# 目标文件列表注意 y.tab.o 和 lex.yy.o 都在里面 OBJS main.o interface.o create.o execute.o eval.o \ string.o string_pool.o util.o native.o \ error.o error_message.o lex.yy.o y.tab.o # bison 先生成头文件和 C 文件两者是同一个规则的两个产物 y.tab.h y.tab.c: crowbar.y bison --yacc -dv crowbar.y # flex 依赖 y.tab.h因为它要 include 里面的 token 定义 lex.yy.c: crowbar.l y.tab.h flex crowbar.l # 链接阶段把所有 .o 拼成可执行文件 crowbar: $(OBJS) $(CC) $(CFLAGS) -o $ $(OBJS) $(LIBS)这段规则的核心是两行依赖声明。y.tab.h y.tab.c: crowbar.y告诉 make只要.y变了这两个文件都要重新生成lex.yy.c: crowbar.l y.tab.h告诉 make生成词法分析器之前必须先有语法分析器的头文件。参数--yacc -dv里--yacc是让 bison 兼容 yacc 的命名习惯产出y.tab.c而不是crowbar.tab.c-d是额外生成头文件-v是输出y.output这个状态机报告调语法冲突时非常有用。注意Makefile 里命令行前面必须是 Tab 而不是空格从网页复制代码时这一条最容易丢报错是missing separator跟语法错误长得完全不一样。4.2 词法分析器 crowbar.l把字符流切成 token.l文件的结构非常固定%{ %}里放 C 头文件和声明%option放 flex 的行为开关两个%%之间是「正则 → 动作」的规则表最后一段是 C 代码。Crowbar 的词法规则里关键字和标识符的匹配顺序是有讲究的关键字必须写在标识符规则前面否则if会被当成普通变量名吃掉。%{ #include stdio.h #include crowbar.h #include y.tab.h /* token 编号来自 bison 生成的头文件 */ %} %option noyywrap %% if { return IF; } else { return ELSE; } while { return WHILE; } for { return FOR; } [0-9] { yylval.int_value atoi(yytext); return INT_LITERAL; } [0-9]\.[0-9] { yylval.double_value atof(yytext); return DOUBLE_LITERAL; } [a-zA-Z_][a-zA-Z0-9_]* { yylval.identifier crb_create_identifier(yytext); return IDENTIFIER; } [ \t\n] { /* 空白字符直接丢弃不产生 token */ } //.* { /* 单行注释同样丢弃 */ } . { crb_compile_error(BAD_CHARACTER); } %%每个动作块里的yytext是当前匹配到的字符串yylval是传给语法分析器的值载体它的类型由.y文件里的%union定义。crb_create_identifier不是简单复制字符串它会把标识符登记到一张表里做去重后面执行阶段查变量就靠这个指针比较而不是字符串比较属于性能上的常见做法。最后那条.规则是兜底遇到不认识的字符直接报编译错误不加这条的话 flex 默认会把它原样输出到 stdout调试时会看到莫名其妙的字符混在结果里。4.3 语法分析器 crowbar.y从 token 序列到语法树.y文件同样分三段声明段定义 token 和优先级规则段写文法产生式最后是 C 代码。规则段里每个产生式后面的花括号是「归约动作」$$代表当前产生式的结果$1、$2依次代表右边各个符号的语义值。%{ #include stdio.h #include crowbar.h %} /* 语义值的联合体决定 yylval 能装什么 */ %union { int int_value; double double_value; char *identifier; Expression *expression; Statement *statement; StatementList *statement_list; } %token int_value INT_LITERAL %token double_value DOUBLE_LITERAL %token identifier IDENTIFIER %token IF ELSE WHILE FOR %token LP RP LC RC SEMICOLON /* 声明非终结符的类型让 bison 做类型检查 */ %type expression expression %type statement statement %type statement_list statement_list %% program : statement_list { execute_statement_list($1); } ; statement : expression SEMICOLON { $$ crb_create_expression_statement($1); } | IF LP expression RP statement ELSE statement { $$ crb_create_if_statement($3, $5, $7); } | WHILE LP expression RP statement { $$ crb_create_while_statement($3, $5); } ; expression : expression expression { $$ crb_create_binary_expression(ADD_EXPRESSION, $1, $3); } | expression * expression { $$ crb_create_binary_expression(MUL_EXPRESSION, $1, $3); } | INT_LITERAL { $$ crb_create_int_expression($1); } | IDENTIFIER { $$ crb_create_identifier_expression($1); } ; %%这段文法里有两个点值得注意。第一execute_statement_list($1)直接写在program的归约动作里意味着这个解释器是「边解析边执行」的——解析完整个程序语句列表已经在内存里然后一次性执行。第二每个crb_create_*函数都在create.c里负责 malloc 一个 AST 节点并填字段这是把「语法结构」和「内存分配」分开的标准写法后面加新语法只需要在create.c里补一个构造函数。4.4 执行器 execute.c一个 switch 撑起整个运行时语法树建好之后execute.c负责遍历它。核心就是一个按节点类型分派的大 switch每个分支做一件事求值、改环境、或者递归进子语句。void execute_statement(Statement *stmt) { switch (stmt-type) { case EXPRESSION_STATEMENT: /* 表达式语句求值后结果丢弃副作用留在环境里 */ execute_expression(stmt-u.expression_s); break; case IF_STATEMENT: /* 条件表达式求值取布尔字段判断走哪个分支 */ if (eval_expression(stmt-u.if_s.condition)-u.bool_value) { execute_statement(stmt-u.if_s.then_block); } else { execute_statement(stmt-u.if_s.else_block); } break; case WHILE_STATEMENT: /* 每轮都重新求值条件这是 while 和 if 唯一的区别 */ while (eval_expression(stmt-u.while_s.condition)-u.bool_value) { execute_statement(stmt-u.while_s.body); } break; case BLOCK_STATEMENT: /* 进入新作用域压一层变量表退出时弹栈 */ push_environment(); execute_statement_list(stmt-u.block_s.statement_list); pop_environment(); break; default: /* 走到这里说明 AST 构造有问题直接断言失败 */ DBG_assert(0, (bad statement type %d, stmt-type)); } }参数说明上Statement结构体里用一个type枚举加一个union存各种语句的字段这是 C 语言里做多态的经典手法。push_environment和pop_environment维护的是一个环境栈块作用域的变量遮蔽就靠这个栈实现进块压一层空表出块整层丢弃。DBG_assert在 Release 编译时会被宏掉所以默认分支只在调试版里生效用来兜住那些不该出现的节点类型。4.5 字符串池与 GC0.2 版本最该看的一段动态语言里字符串是高频创建的对象如果每次都 malloc 一份内存会碎得很快。Crowbar 的做法是搞一个字符串池相同内容的字符串只存一份返回同一个指针。typedef struct StringPoolEntry_tag { char *str; struct StringPoolEntry_tag *next; } StringPoolEntry; static StringPoolEntry *string_pool_head NULL; char * crb_create_string(const char *str) { StringPoolEntry *entry malloc(sizeof(StringPoolEntry)); entry-str malloc(strlen(str) 1); strcpy(entry-str, str); entry-next string_pool_head; string_pool_head entry; return entry-str; }这段是 0.1 阶段最朴素的做法来一个存一个链表串起来。问题也很明显——链表只增不减脚本跑得久了内存就一路涨。0.2 版本引入的 GC 就是在这一层动刀从全局变量表和当前环境栈出发做可达性标记标记完再扫一遍池子没被标记的字符串释放掉。读 GC 那部分代码时重点看两个函数一个负责「从根集合开始递归标黑」一个负责「遍历池子把白的删掉」。根集合包括全局变量、当前所有栈帧里的局部变量、以及求值过程中的临时值漏掉任何一处都会导致对象被提前回收表现为随机崩溃或者读到乱码这种 bug 极难定位属于典型的玄学问题。提示调 GC 时先关掉回收、跑一遍压力测试脚本确认程序本身没问题再打开回收跑第二遍两遍结果不一致就基本锁定是标记漏了根。4.6 Diksam 的编译器前端类型检查和字节码生成Diksam 和 Crowbar 最大的工程差异在「执行」这一步。Crowbar 是遍历语法树直接求值Diksam 是先把语法树编译成一串字节码再交给虚拟机循环执行。这个结构决定了它的源码目录里必然有一个compiler模块做前端一个虚拟机模块做后端0.3 之后入口挪到main正是为了把这两块拼起来。编译器前端里最关键的一步是类型检查。表达式节点在构造时只带语法结构类型是在一遍单独的遍历里推出来的每个表达式节点上会挂一个推断出的类型字段遇到赋值、函数调用、参数传递时就拿这个字段做匹配不匹配直接报编译错误。这一步做完后面的字节码生成就不需要再关心类型每个操作码都能确定操作数的宽度和语义。这种「前端做检查、后端做执行」的分工也是静态类型语言在工程上比动态语言更容易优化的原因。5. 常见问题排查五个我实际撞过的坑5.1 bison 报 m4 找不到 Files现象执行bison --yacc -dv crowbar.y之后屏幕上刷出一排m4: cannot open Files、m4: cannot open D:\Program没有任何.c文件生成。原因bison 安装在带空格的路径下比如C:\Program Files (x86)\GnuWin32它把自身路径拼进 m4 的调用参数时没有加引号空格把路径切成了两段。解决把 GnuWin32 重装到C:\GnuWin32这类无空格路径或者用 8.3 短文件名绕过。改完 PATH 之后必须重开命令行窗口旧的窗口读的还是旧环境变量。5.2 y.tab.h: No such file or directory现象编译过程中报mycalc.l:3:19: error: y.tab.h: No such file or directory或者gcc: y.tab.c: No such file or directory。原因跳过了 make直接对lex.yy.c或.l文件执行 gcc。y.tab.h是 bison 的产物没有先跑 bison 就不存在这个文件。解决老老实实执行gmake或make让 Makefile 按依赖顺序跑。如果只是想手动试一次顺序必须是先bison --yacc -dv xxx.y再flex xxx.l最后gcc。用ls确认目录里确实出现了y.tab.c、y.tab.h、lex.yy.c三个文件再往下走。5.3 程序跑起来报找不到 cygwin.dll现象在 Cygwin 终端里编译出来的crowbar.exe双击或者在 cmd 里执行时弹窗提示缺少cygwin.dll。原因Cygwin 的 gcc 产出的是依赖 Cygwin 运行时库的程序不是原生 Windows 可执行文件。离开 Cygwin 的 PATH 环境系统就找不到这个 DLL。解决要么在 Cygwin 终端里跑要么改用 MinGW 的 gcc 重新编译一遍。判断方法很简单用dumpbin /dependents crowbar.exe或者 Dependency Walker 看一眼依赖列表出现cygwin1.dll就说明是 Cygwin 产物。这也是我在 3.3 里建议优先走 MinGW 路线的原因。5.4 Linux 下链接报 cannot find -lmsvcp60现象在 Linux 上编译 Diksam 的某个版本链接阶段报/usr/bin/ld: cannot find -lmsvcp60。原因Makefile 是从 Windows 环境改过来的LIBS变量里还留着 Windows 专有的 C 运行库。Linux 上根本没有这个库链接器自然找不到。解决打开 Makefile把LIBS里的-lmsvcp60删掉如果还带着-mwindows之类的 Windows 链接选项也一并去掉。改完之后make clean再重新编译避免旧的.o文件干扰。5.5 脚本第一行就报非法字符 0x0d现象在 Linux 下执行./crowbar test/test.crb报错指向第 5 行提示非法字符(0x0d)但用编辑器打开那个文件看起来完全正常。原因.crb文件是在 Windows 下编辑的换行是\r\n词法分析器只认\n多出来的\r就变成了一个无法识别的字符。解决用dos2unix test/test.crb转一遍或者在编辑器里把行尾格式改成 LF。批量处理可以find . -name *.crb -exec dos2unix {} \;。这个坑最隐蔽的地方在于报错行号对得上、文件内容看着也没问题只有用cat -A看到行尾的^M才能确认。6. 二次开发的改造思路给 Crowbar 加一条新语句怎么验证真正检验有没有读懂这套代码的方式是给它加一个语法特性然后看整条链路会不会断。我一般拿for循环或者一个print原生函数开刀因为改动横跨词法、语法、AST、执行四个文件任何一层没接上都会立刻暴露。先列改动清单按依赖顺序排步骤文件改什么验证方式1crowbar.l加关键字规则返回新 token单独跑 flex看lex.yy.c里有没有新 token 分支2crowbar.y%token声明、加产生式、写归约动作跑 bison看y.output里有没有冲突告警3crowbar.h在StatementType枚举里加新类型编译能过说明枚举和 switch 对得上4create.c写crb_create_xxx_statement构造函数构造出的节点字段打印出来检查5execute.c加case分支实现语义写测试脚本跑出预期结果以加print为例语法层只需要一条产生式/* crowbar.y 里新增 */ %token PRINT statement : PRINT expression SEMICOLON { $$ crb_create_print_statement($2); } ;对应的执行分支就是一个求值加输出/* execute.c 里新增 */ case PRINT_STATEMENT: { Value *v eval_expression(stmt-u.print_s.expression); /* 按值的实际类型分派输出格式 */ switch (v-type) { case CRB_INT_VALUE: printf(%d\n, v-u.int_value); break; case CRB_DOUBLE_VALUE: printf(%g\n, v-u.double_value); break; case CRB_STRING_VALUE: printf(%s\n, v-u.string_value-str); break; default: printf((unknown)\n); } break; }改完之后的验证顺序不能乱。先make clean全量重编确保不是增量编译掩盖了依赖问题然后写一个只含print 12;的最小脚本确认输出 3再写一个嵌套在if和while里的版本确认执行器递归下去也没问题最后跑一遍资料包自带的test/test.crb确认新加的 token 没有和原有的标识符规则打架。如果第 3 步报「重复定义」八成是%token加在了%union前面或者枚举名撞了如果运行时直接段错误先检查create.c里是不是漏了字段赋值C 语言不会帮你把 malloc 出来的内存清零漏一个指针就是野指针。还有一个小技巧bison 生成的y.output文件里会列出所有状态和冲突加完产生式之后先看这个文件如果出现 shift/reduce 冲突说明你的新规则和现有表达式优先级有歧义最省事的办法是给新 token 补一行%left或者%right声明而不是去改文法结构。我第一次加语法的时候没看y.output直接跑测试结果一个表达式被解析成了完全不同的树调试了半天才反应过来是优先级问题。从那以后我每次动.y文件都强制先跑一遍 bison 看冲突数确认是零再往下走。希望这套流程能帮到你少在环境上耗时间把精力留给真正有意思的那部分——看着自己写的语法在屏幕上跑出结果。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/27 19:04:30

自制编程语言实战:Crowbar/Diksam 解释器与字节码虚拟机

简介:《自制编程语言》相关资料1面向希望从零理解语言设计与实现机制的开发者与学习者,聚焦编译器、解释器构建及跨平台编译工具链的入门实践。压缩包仅含1个PDF文件,大小约2.38MB,轻量便携,适合作为纸书之外的配套阅读…

2026/10/27 19:04:30

基于协同过滤的儿童图书推荐系统:从算法到工程落地

简介:这份资源是基于协同过滤算法实现的儿童图书推荐系统完整源码包,面向计算机相关专业的课程设计、毕业设计学生以及想入门推荐算法的Java开发者。项目采用SpringBoot框架搭建后端,MySQL存储用户与图书数据,通过分析儿童阅读行为…

2026/10/27 19:04:29

离线语音交互智能对话系统:从架构选型到部署调优的完整指南

简介:这是一套基于语音交互的智能对话系统开源实现,聚焦实时语音输入与大型语言模型响应,面向希望摆脱云端依赖、在个人电脑或低算力硬件上离线部署语音助手的开发者和AI研究者,解决低延迟自然对话与本地私有化运行两大痛点。压缩…

2026/10/27 19:40:03

AI代理如何重塑消费决策与商业生态

1. 当AI代理开始主导消费行为:一场静默的商业革命上周调试购物机器人时,我意外发现系统自动续费了三年份的猫粮——尽管我根本没有养猫。这个令人啼笑皆非的事件背后,隐藏着一个正在发生的重大转变:AI代理正在从服务提供者转变为具…

2026/10/27 19:40:03

CMake 3.10.0 for Windows:工业级构建的版本锚点与实战指南

简介:本资源为CMake 3.10.0官方Windows 64位安装包(RAR格式),专为Windows平台开发者、C项目构建初学者及跨平台工程实践者提供稳定可靠的构建工具支持,有效解决多编译器(如MSVC)、多IDE&#xf…

2026/10/27 19:40:03

n8n自动化工具核心概念与实战应用指南

1. 为什么需要理解n8n的核心概念? 第一次接触n8n时,我完全被它复杂的界面搞懵了。作为一个开源的自动化工具,n8n的强大之处在于它的灵活性,但这也意味着学习曲线相对陡峭。经过半年多的实际项目应用,我发现真正理解它…

2026/10/27 19:40:03

智元机器人技术演进与核心突破解析

1. 智元机器人技术演进全景图(2015-2025)2015年我在深圳科技园第一次见到实验室里的机械臂时,它还在用示教器进行点位编程。十年后的今天,当我看着搭载多模态感知系统的智元机器人自主完成精密装配任务时,不禁感慨技术…

2026/10/27 19:40:03

有机与无机的本质区别:从碳骨架、键合方式到生命系统

1. 从厨房灶台到显微镜下:为什么“有机”不等于“健康”,“无机”也不代表“危险”你有没有在超市买酱油时盯着配料表发过呆?看到“谷氨酸钠”就皱眉,看到“酵母提取物”却觉得天然;刷到短视频说“纯有机蔬菜不含农药”…

2026/10/27 19:35:02

AI技术变革中的资本博弈与创新路径

1. 技术变革中的资本力量博弈最近和几位做早期投资的朋友聊天,他们桌上摆着的BP(商业计划书)十份里有八份都带着"AI"这个标签。这让我想起2014年那波O2O创业潮,只不过这次的主角换成了大模型和生成式AI。作为经历过三波…

2026/10/27 14:24:03

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/27 9:34:54

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/27 0:59:21

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

2026/10/27 0:02:28

WorkBuddy_新建任务与工作模式19_“模式选错”的典型代价

“模式选错”的典型代价 摘要 在软件工程中,设计模式、架构模式、并发模式乃至团队协作模式的选择,往往在项目早期就决定了后续的开发效率和维护成本。本文通过多个真实场景,剖析“模式选错”带来的典型代价——包括返工成本、技术债务累积、…

2026/10/27 0:02:28

WorkBuddy_新建任务与工作模式20_新手推荐路径

新手推荐路径 摘要 在 AI 辅助开发工具快速普及的今天,新手面对最多的困惑往往不是"工具够不够强",而是"我该从哪一步开始用"。本文提出一条经过实践验证的三阶段学习路径:先从 Ask(问答式交互)起…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑