☰
自制编程语言源码实战:Crowbar/Diksam与bison/flex环境排错
2026/10/27 19:04:03 网站建设 项目流程

简介:《自制编程语言》相关资料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_4,diksam_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.sh(Linux)和make.bat(Windows),Makefile 是共用的。看起来是跨平台的,实际在 Windows 上能不能编出来,八成取决于三件事:编译器选 MinGW 还是 Cygwin、bison 和 flex 装在了哪个路径、make命令到底叫什么名字。这三件事任何一件没对齐,报错信息都会指向一个和真实原因无关的地方。

3.1 为什么选 MinGW 而不是 Cygwin

Cygwin 提供的是类 UNIX 环境,./configure && make这一套跑得最顺,编译 oniguruma 这类带 autotools 的库时确实省事。但它的产物依赖cygwin.dll,crowbar.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 --version

where是把 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.c,Makefile 里所有写死的文件名全部对不上);-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是因为数学函数要用 libm,Linux 上不加会报未定义引用,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.a(Linux 下还有.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。

如果系统里原本就有另一个版本的 oniguruma,make 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 再 ranlib

MinGW 环境里没有/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 *)"a+b", (const UChar *)"a+b", 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_check

onig_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——这个顺序帮我省掉的排查时间,比任何调试技巧都多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询