自制编程语言源码实战:Crowbar/Diksam与bison/flex环境排错

发布时间:2026/10/27 19:04:15
自制编程语言源码实战:Crowbar/Diksam与bison/flex环境排错 简介《自制编程语言》相关资料1 面向编程语言爱好者与编译原理学习者围绕从设计到实现的完整链路帮助读者理解解释器与编译器如何落地。资源为单个 PDF压缩包约 2.38MB以图文和代码示例为主适合配合本地环境阅读练习也可用于课程配套参考。文档涉及词法分析、语法分析、yacc/lex 与 bison/flex 的使用、LL 解析器实现以及 Crowbar、Diksam 两个自制语言项目的分版本演进同时给出 MinGW、Cygwin 在 Windows 下的配置思路并区分 Linux 的 make.sh 与 Windows 的 make.bat/gmake 构建方式便于按平台复现并排查工具链差异。已有 870 人学习内容兼具教程与项目参考价值可帮助建立编程语言设计、实现与使用的基本框架也能为编译器和解释器课程设计、小型语言原型开发提供对照材料适合具备一定 C 语言基础、希望深入理解语言运行机制的学习者。1. 从一份十年前的编译器源码包说起Crowbar 与 Diksam 能带你走到哪一步很多人学编程语言原理卡在“语法树画得出来、代码写不出来”这一步教材上的 LL(1) 分析表能推真让他给一门语言写出词法分析器、再让if语句真正跑起来就没了主意。这份《自制编程语言》相关资料补的正是这一环——它不是讲原理的电子书而是一组能直接make出可执行文件的源码包两门完整的玩具语言 Crowbar 与 Diksam外加 calc、mycalc、llparser 几个梯度示例从最简词法分析一路做到带 GC、带行号错误报告的解释器。适合已经会写 C、想亲手落地一门编程语言的从业者也适合想把编译原理课设做成能跑的东西的同学。下面按“包怎么拆、环境怎么搭、坑在哪”的顺序拆。2. 拆开压缩包先看目录crowbar_book_0_x 与 diksam_book_0_x 的版本梯度怎么排解压之后目录会摊一屏crowbar_book_0_1到crowbar_book_0_4diksam_book_0_1到diksam_book_0_4再加上calc、mycalc、mycalc_ex、llparser、llparser_ex这几个短名字的目录。第一次看容易犯的错误是从diksam_book_0_1开始啃理由是“编号靠前”实际上这套资料的编号不是按难度排的而是按“语言特性一次加一层”排的。先把版本梯度和示例定位搞清楚能省掉大量在抽象层之间来回跳的时间。2.1 两套语言不是并列的两个项目是同一根线上的两级Crowbar 走的是解释执行路线动态类型、变量不声明类型、求值器直接在语法树上递归后期版本才补上 GC 和更完整的运行时错误信息。Diksam 则是静态类型路线源码先过类型检查和符号表再编译成字节码由虚拟机分派执行错误信息里带着更精确的位置和类型描述。这两条路线解决的是不同的问题——Crowbar 让你专注“词法分析 → 语法分析 → 求值”这条最短链路中间没有类型系统这道墙Diksam 让你看到一门静态语言在真正跑起来之前还要额外做完哪些工作。我一般建议先跑crowbar_book_0_1理由很实际它的产物是一个几百行的解释器make完之后随手改一行execute.c就能看到行为变化反馈极快而 Diksam 从 0_1 开始就已经有符号表和字节码两三层结构直接进去容易把“类型检查失败”和“虚拟机指令写错”两类问题混在一起排错时很难定位到底哪一层出的错。2.2 calc、mycalc、llparser把工具链拆开单独练手的三个示例这几个目录的价值常被低估。它们的作用是让你在不被完整语言干扰的情况下把 yacc/lex 这套工具链单独走通一遍。关系大致是这样calc是手写的递归下降计算器mycalc用 yacc/lex 重写同样的四则运算功能llparser是手写 LL 分析带_ex后缀的是练习答案。目录实现方式你该拿它验证什么calc纯手写递归下降不理解 yacc 时先看清一个解析器最少需要几个函数mycalcyacc/lex 生成.l与.y的依赖顺序、y.tab.h从哪来mycalc_exyacc/lex 参考答案自己写完再对答案重点看错误恢复规则llparser手写 LL(1)预测分析表怎么落到代码里llparser_ex参考答案对比自己的 FIRST/FOLLOW 集算错在哪mycalc是这里面最值得单独跑的一个因为它是全套资料里唯一一个“只有两个源文件却完整依赖 bison flex”的最小单元。把它的y.tab.c、y.tab.h、lex.yy.c三个中间产物都跑出来看一遍后面编 Crowbar 和 Diksam 时遇到生成文件缺失你就知道该往哪一层找。2.3 版本递进关系与建议投入四个版本的递进逻辑是“特性一次加一层”从只有变量、表达式和print到加入函数调用与数组再到引入 GC最后补上更完整的错误消息与边界处理。具体每一版改了哪几处包内源码注释和目录差异比任何二手总结都准确所以别去背特性表直接做 diff 更划算# 在两版之间做递归 diff只看新增和被改掉的文件 diff -ru crowbar_book_0_1 crowbar_book_0_2 v12.diff diff -ru crowbar_book_0_2 crowbar_book_0_3 v23.diff wc -l v12.diff v23.diff-r递归、-u统一格式输出的补丁可以直接用patch -p1打回到旧版本上做对照实验。wc -l那一步是为了先估算工作量如果某一版的 diff 只有两三百行说明那一版是局部改动适合当天读完如果上千行说明动了核心数据结构得留出整块时间。我的投入建议是0_1 逐行读完并自己重敲一遍0_2 和 0_3 用上面这条 diff 看增量0_4 只看 GC 部分和error_message.c其余跳过。Diksam 那边反过来0_1 和 0_2 只用来看“类型检查加进来之后编译器前端多出了什么”真正值得细读的是 0_3 和 0_4 里字节码生成和虚拟机分派的部分因为那是静态语言路线独有的内容Crowbar 里学不到。3. Windows 下打通 bison/flex 链路MinGW 装在哪、PATH 怎么配、make 怎么走这套源码包同时提供了make.shLinux和make.batWindowsMakefile 是共用的。看起来是跨平台的实际在 Windows 上能不能编出来八成取决于三件事编译器选 MinGW 还是 Cygwin、bison 和 flex 装在了哪个路径、make命令到底叫什么名字。这三件事任何一件没对齐报错信息都会指向一个和真实原因无关的地方。3.1 为什么选 MinGW 而不是 CygwinCygwin 提供的是类 UNIX 环境./configure make这一套跑得最顺编译 oniguruma 这类带 autotools 的库时确实省事。但它的产物依赖cygwin.dllcrowbar.exe拷到另一台没装 Cygwin 的机器上直接起不来报的是找不到 DLL跟你的代码一点关系都没有。而这份资料的用途是反复改代码、反复编译、反复在本地跑测试脚本产物最好能脱离环境独立运行。MinGW 走的是另一条路直接调 Windows 原生 API产出真正的独立 exe不需要任何运行时 DLL。代价是它不自带 bison 和 flex得另外装而且它提供的make叫mingw32-make.exe不叫make.exe这一点在包里那些make.bat里已经处理过了手敲命令时容易忘。我的习惯是主环境用 MinGW 跑 Crowbar 和 Diksam只有在编 oniguruma 这种必须走 autotools 的库时临时开一个 Cygwin 终端。两个环境各干各的活不混用——混用是后面第 5 章那几类报错的主要来源。3.2 安装与 PATH 配置MinGW 的安装器是向导式的关键只有两步安装目录必须选一个不含空格的路径组件勾选里至少要有mingw32-base提供 gcc和mingw32-make。装完之后把bin目录加进 PATH然后开一个新的 cmd 窗口验证。:: 装完必须新开一个 cmd旧窗口的 PATH 是旧的 where gcc where mingw32-make gcc --version mingw32-make --versionwhere是把 PATH 里所有同名可执行文件的路径都列出来比gcc -v更有用如果你的机器上同时装过别的 GCC 套件比如某个 IDE 自带的where gcc会一次列出好几个排在前面的那个才是实际被调用的。这一步经常能提前发现“我明明配了 MinGW实际用的是另一个 gcc”这类问题。bison 和 flex 在 Windows 上一般用 GnuWin32 提供的那两个安装包装的时候同样选无空格路径比如D:\GnuWin32装完把D:\GnuWin32\bin追加到 PATH。这一步看着平淡但它是第 5 章第一个坑的直接成因装在C:\Program Files (x86)下面bison 会在内部调用 m4 的时候把路径按空格切开。:: 三个工具都就位之后做一次联合自检 where bison where flex where m4 bison --version flex --version注意where m4这一行。bison 生成解析器时会在内部调 m4 做宏展开很多人在这一步翻车却没意识到 m4 也是依赖的一部分——如果where m4什么都没有后面跑 bison 必然报 m4 相关的错。3.3 把 Makefile 的 y.tab.c 依赖链看明白包里每个工程的 Makefile 都遵循同一条链.y经 bison 生成y.tab.c和y.tab.h.l经 flex 生成lex.yy.c再把一堆.c编成.o链接成可执行文件。这条链的核心约束是顺序lex.yy.c里有一行#include y.tab.h所以 bison 必须先于 flex 跑y.tab.h不存在的时候跑 flex生成的lex.yy.c是编不过的。# 典型的生成规则和包里的 Makefile 结构一致 y.tab.c y.tab.h: crowbar.y bison --yacc -dv crowbar.y lex.yy.c: crowbar.l y.tab.h flex crowbar.l crowbar: $(OBJS) $(CC) $(CFLAGS) -o $ $(OBJS) -lm--yacc让 bison 按 yacc 的命名习惯输出y.tab.c不加这个参数新版 bison 会输出crowbar.tab.cMakefile 里所有写死的文件名全部对不上-d是生成头文件y.tab.h没有它lex.yy.c里那行 include 直接失败-v额外输出y.output里面是完整的 LALR 状态机和冲突报告调语法规则时这是唯一的黑匣子。lex.yy.c: crowbar.l y.tab.h这一行把y.tab.h显式写进了依赖就是用来强制顺序的。3.4 手动走一遍生成链确认每一步产物在第一次make之前我建议先手动把这条链走一遍。目的是把“工具能不能跑”和“Makefile 写得对不对”这两件事分开验证一次make失败的时候你才知道该怀疑哪一头。# 进入最小示例先用 mycalc 练手它的文件最少 cd /d/work/crowbar_book_0_1 # 第一步只跑 bison确认两个生成文件都出来了 bison --yacc -dv crowbar.y ls -l y.tab.c y.tab.h y.output # 第二步再跑 flex它依赖上一步的头文件 flex crowbar.l ls -l lex.yy.c # 第三步逐个编译再链接方便定位是哪个文件挂了 gcc -c -g -Wall lex.yy.c gcc -c -g -Wall y.tab.c gcc -c -g -Wall main.c interface.c create.c execute.c eval.c \ string.c string_pool.c util.c native.c error.c error_message.c gcc -o crowbar *.o -lm-g保留调试符号后面用 gdb 看解释器执行栈的时候必须有-Wall在这套代码上会刷出一堆警告别急着关其中一部分正是理解代码设计的线索-lm是因为数学函数要用 libmLinux 上不加会报未定义引用MinGW 上有时不加也能过但加上更保险。三步产物都齐了之后再执行./crowbar test/test.crb能打印出正常结果说明工具链这一关过了。这时候再回到make如果make失败而手动能过问题一定在 Makefile 的路径、变量或者换行符上——这就是先手动的价值。4. oniguruma 5.9.4为什么它排在 Diksam 前面以及两个平台各自的装法包里的 Diksam 在字符串处理和标准库部分依赖了 oniguruma 这个正则库。很多人编 Diksam 时报“找不到 oniguruma.h”第一反应是去改 Makefile 注释掉正则部分结果编出来的解释器一跑字符串相关的测试就崩。正确做法是先把库装好因为它是 Diksam 的前置依赖不是可选项。4.1 正则库在这套语言里的位置Crowbar 的字符串处理基本只有拼接和比较用不上正则。到了 Diksam语言本身要支持字符串的模式匹配标准库里需要一个成熟的、支持多种字符编码的正则引擎oniguruma 就是被选中的那个。它提供两样东西头文件oniguruma.h和静态库libonig.aLinux 下还有.so动态库。理解这一点很重要因为它决定了安装方式你不需要把 oniguruma 装到系统级别的/usr/local只要让 Diksam 的编译命令能找到头文件和库就行。这个认知能直接绕开第 5 章里那个make install失败的问题。4.2 Linux 和 Cygwin 下configure make make install这两个环境有完整的 autotools 和标准的目录结构按常规流程走就行。# 源码包解压到当前目录 tar -xvf onig-5.9.4.tar.gz cd onig-5.9.4 # 配置--prefix 决定装到哪不写默认 /usr/local ./configure --prefix/usr/local # 编译-j 后面跟核数能明显缩短时间 make -j4 # 装到系统目录需要写权限 sudo make install./configure那一步会做大量环境探测输出里出现checking for alloca... yes、checking for prototypes... yes这类行是正常的只要结尾没报 error 就能继续。--prefix我一般不写让它默认装到/usr/local因为后面gcc找-lonig时默认就会搜/usr/local/lib省去手工加-L。如果系统里原本就有另一个版本的 onigurumamake install会覆盖掉这在共用机器上容易引起别的项目出问题。这种时候改成--prefix/opt/onig-5.9.4编译 Diksam 时显式加-I/opt/onig-5.9.4/include -L/opt/onig-5.9.4/lib多打几个字符换个安心。4.3 MinGW 下make install 大概率失败改成手工拷 .a 再 ranlibMinGW 环境里没有/usr/local这套目录autotools 生成的安装脚本却仍然往里写报的错是ranlib.exe: /usr/local/lib/libonig.a: No such file——因为那个目录压根不存在。要么先建目录要么干脆跳过make install直接从编译产物里把两个文件捞出来。# 在 MinGW 环境下配置和编译prefix 指向 MinGW 自己的根 cd /c/onig-5.9.4 ./configure --prefix/c/MinGW make # 不进 make install直接从 .libs 里拿静态库 ls -l .libs/libonig.a # 手工复制到 MinGW 的 lib 和 include cp .libs/libonig.a /c/MinGW/lib/ cp oniguruma.h onigposix.h /c/MinGW/include/ # 关键一步重建静态库的符号索引 ranlib /c/MinGW/lib/libonig.a.libs是 libtool 生成的隐藏目录真正的静态库在里面外面那个libonig.la只是个描述文件——这也是为什么直接拷libonig.la没用。ranlib那一步不能省.a文件本质是一堆.o打包ranlib 会在末尾追加符号索引没有索引链接器搜符号时会失败报的是“undefined reference”看起来像代码写错了实际是库文件不完整。这套“拷 .a 拷头文件 ranlib”的方法我在任何一个不带系统目录结构的 Windows 编译环境里都用比折腾make install稳定得多。4.4 写一个二十行的链接验证程序库装完之后别急着回去编 Diksam先写个最小程序确认链接是通的。这一步花两分钟能省掉后面“Diksam 编不过到底是库没装好还是代码问题”的长时间排查。/* onig_check.c只验证头文件和库能被正常找到和链接 */ #include stdio.h #include oniguruma.h int main(void) { OnigRegex reg; OnigErrorInfo einfo; int r; /* 编译一个简单的正则成功返回 0 */ r onig_new(reg, (const UChar *)ab, (const UChar *)ab, ONIG_OPTION_DEFAULT, ONIG_ENCODING_ASCII, ONIG_SYNTAX_DEFAULT, einfo); if (r ! ONIG_NORMAL) { fprintf(stderr, onig_new failed: %d\n, r); return 1; } printf(oniguruma ok\n); onig_free(reg); onig_end(); return 0; }# Linux 或 Cygwin gcc -o onig_check onig_check.c -lonig # MinGW显式给出头文件和库路径 gcc -o onig_check.exe onig_check.c -I/c/MinGW/include -L/c/MinGW/lib -lonig ./onig_checkonig_new的参数依次是输出用的正则对象指针、模式串起始、模式串结束、选项ONIG_OPTION_DEFAULT表示不忽略大小写等特殊行为、字符编码、语法方言、错误信息结构体。跑出oniguruma ok就说明整条链路是通的如果编译过了但运行时报找不到 DLL说明链上的是动态库版本静态库没被选中回去确认.libs/libonig.a拷的位置对不对。5. 排查五类高频报错的现象、原因与处置这套资料发布时间早路径、工具版本、换行符这几件事上留下的坑挺集中。下面五类是我在 Windows 上复现这套源码时反复撞上的按“现象 → 原因 → 解决”拆开写碰到直接对号入座。5.1 bison 报 m4: cannot open Files: No such file or directory现象跑 bison 生成解析器时输出一长串错误里面能看到m4: cannot open Files、m4: cannot open D:\Program、m4: cannot open (x86)\GnuWin32\Bison/share/bison路径明显在空格处被切成了好几段。原因bison 内部调用 m4 做宏展开它把自己所在的路径拼进命令行参数时没有做引号转义。GnuWin32 的默认安装位置是C:\Program Files (x86)\GnuWin32路径里有两个空格于是Program、Files、(x86)各自变成了独立的参数。这跟你的语法文件一个字都没关系。解决把 GnuWin32 卸掉重装到无空格路径比如D:\GnuWin32然后把D:\GnuWin32\bin从 PATH 里删干净旧的、加上新的重开终端。不想重装的话也可以临时把BISON_PKGDATADIR环境变量指到D:\GnuWin32\share\bison但路径里只要还有空格问题就还在重装更干脆。5.2 gcc: y.tab.c: No such file or directory 与 y.tab.h 找不到现象make输出里先有一条recipe for target y.tab.h failed紧接着gcc: y.tab.c: No such file or directory再往下是mycalc.l:3:19: error: y.tab.h: No such file or directory。看起来是三个独立的错实际是同一个。原因bison 那一步失败了往往是 5.1 那个 m4 问题y.tab.c和y.tab.h根本没生成Makefile 因为没加.DELETE_ON_ERROR之类的保护继续往下跑 flex 和 gcc于是把“生成文件不存在”报成了“引用文件不存在”。新手看到最后一条.l:3:19会去改.l文件方向就全错了。解决永远从第一条报错看起。先把 bison 单独跑通、ls确认y.tab.c和y.tab.h都在再跑 flex。如果确认 bison 成功但 flex 还是报找不到y.tab.h那是 Makefile 里lex.yy.c这条规则没把y.tab.h写进依赖手动在规则里补上或者直接顺序执行两条命令。5.3 gmake: *** No targets specified and no makefile found现象在工程目录里敲gmake直接报No targets specified and no makefile found. stop.连编译都没开始。原因最常见的两种。一是当前目录不对——make.bat或者 Makefile 在上层目录你在src子目录里执行的二是文件名大小写问题Windows 不敏感但某些工具链读文件时按大写Makefile找包里给的是小写makefile切到别的环境就找不到了。还有一种少见情况是文件扩展名被编辑器改成了Makefile.txt。解决先ls看一眼当前目录里到底有没有 Makefile没有就cd ..往上找。确认有文件还报错就把名字改成标准的Makefile首字母大写。另外注意命令名MinGW 下是mingw32-make某些构建脚本里会把它复制成gmake.exe使用敲make反而可能调到别的工具链上的 make版本不匹配时报的错更莫名其妙。5.4 /usr/bin/ld: cannot find -lmsvcp60现象在 Linux 或 Cygwin 下编译crowbar_book_0_4时报链接错误cannot find -lmsvcp60提示找不到某个动态库。原因这份 Makefile 里保留了 Windows 平台的链接参数-lmsvcp60指向的是微软 C 运行库Linux 上当然没有这个.so。这类残留参数是跨平台工程里的常见现象作者在 Windows 上验证过Linux 分支没清干净。解决打开 Makefile 找到LDFLAGS或者链接那一行把-lmsvcp60删掉。要是担心还有别的平台专属参数把整行链接命令单独摘出来手工跑一遍缺什么补什么。改完记得在改动处留一行注释说明为什么删下次换平台时不用再推理一遍。5.5 报“第 5 行非法字符(0x0d)”与 .deps/euc_jp.Plo 报错现象./crowbar test/test.crb跑起来直接报某一行有非法字符0x0d可打开文件看那一行什么特别的都没有。另一个表现是在 oniguruma 目录里make时报.deps/euc_jp.Plo:1: *** 缺失分隔符.deps里的依赖文件全成了乱码。原因两个现象根因相同——换行符。Windows 记事本保存的.crb测试脚本是 CRLF每行末尾多一个0x0d这份解释器的词法分析器只按\n切行\r就被当成非法字符报出来。.deps那个是 configure 在另一个环境里生成、又被 Windows 版 make 读行尾和路径分隔符全对不上。解决测试脚本统一转成 LF用dos2unix或者编辑器里改行尾设置都行# 批量转换测试目录下的脚本 find test -name *.crb -exec dos2unix {} \; # 转换后确认没有残留 CR file test/*.crb | grep -i crlf.deps那个更彻底的办法是不混用环境在哪个环境里跑的configure就在哪个环境里make中间别切。实在要切make之前把.deps整个目录删掉让它重新生成。至于lexicalanalyzer.c里那个static char *st_line未初始化就传给fgets的写法属于示例代码本身的缺陷改成static char buf[1024]或者先malloc再传否则跑几个输入之后就是段错误。6. 跑通之后用 flex 的 trace 输出验证词法边界编出来能跑只完成了一半这套资料真正的价值在于你敢改它。改词法规则最容易翻车的地方是状态机crowbar.l里用%start声明了COMMENT、STRING_LITERAL_STATE这些互斥状态规则写在哪个状态下、BEGIN切到哪去光读代码很容易看走眼。我一般不看代码直接让 flex 把状态机打出来。# -T 开启 trace扫描时把每次状态切换打到 stderr flex -T crowbar.l gcc -c lex.yy.c # 也可以 -d 生成带调试断言的扫描器等价于在 .l 里写 %option debug flex -d -o lex_debug.c crowbar.l-T的输出版本能直接看出某个字符进来之后匹配了哪条规则、当前在哪个起始状态。配合自造的边界用例比单步调试快得多/* edge.crb专挑词法边界注释未闭合、字符串里带转义引号 */ var s a\b; /* 跨行 块注释 */ var t 1 2 * 3;注释未闭合这一条必须单独测。COMMENT*/ { BEGIN(INITIAL); }之后如果文件直接结束扫描器会停在COMMENT状态里出不来好的实现应该在EOF上挂一条规则报“注释未终止”而不是静默返回。我第一次跑这类用例时就吃了这个亏解释器不报错、也不输出结果查了半天以为是求值器死循环实际是词法分析卡在注释状态里空转。-T的输出量很大只在一个最小文件上开别拿完整测试集跑。定位到具体规则之后立刻关掉否则真正的错误信息会被淹掉。从那以后我每次拿到带.l/.y的源码包都先把 bison 和 flex 单独跑一遍、确认y.tab.h和lex.yy.c都在再动手改make——这个顺序帮我省掉的排查时间比任何调试技巧都多。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询