简介:《自制编程语言》相关资料1是一份面向编程语言爱好者、编译原理学习者及希望动手实现小型语言的开发者的PDF文档,聚焦从语法设计到解释器、编译器落地的完整链路,适合作为课程学习与项目实践的辅助材料。资源包仅含1个PDF文件,约2.38MB,便于单文件检索与阅读。内容以Crowbar、Diksam两个自制语言案例为主线,串联词法分析与语法分析、yacc/lex与bison/flex的使用、MinGW工具链配置,以及Linux与Windows下不同构建方式的对照,并讨论编译器、解释器、运行时环境与资源管理等关键问题。已有870人学习,读者可借此理解语言设计原则、实现工具选型与跨平台构建思路,获得从语言规范到可运行实现的整体参照。
1. 一份按章节切开的编译器源码:为什么值得从 0_1 开始啃
很多人对「自制编程语言」的第一反应是玄学:词法、语法、语义、虚拟机、GC,随便哪一环都能劝退。这套自制编程语言相关资料的价值恰恰在于它把这件事切碎了——crowbar_book_0_1到0_4、diksam_book_0_1到0_4,每一份都对应书里的一章,是一个能独立构建出可执行文件的完整快照,旁边还配了mycalc、llparser这类前置练习。你不需要一次看懂一门语言,只需要看懂从 0_1 到 0_2 多出来的那几百行。
适合谁:写过 C、知道指针和结构体、但没亲手撸过 parser 的人;想把团队内部 DSL 做个原型、又不想一上来就上 LLVM 的人;以及被编译原理教材的龙书吓退过、想找一条能跑通的路的人。不适合完全没写过 C 的读者,光是 Makefile 和链接报错就够喝一壶。
2. 解包先做什么:八份分章源码的两条主线与一次构建
压缩包里最容易被忽略的是「版本粒度」。它不是给你一份最终代码让你读,而是把同一门语言在每个实现阶段的形态都留了一份,这意味你可以拿diff当阅读器用(第 6 章会展开)。先把目录摸清楚,再决定工具链,最后跑通最简的那一份,顺序不要反。
2.1 目录与产物对照:先知道每份代码能跑出什么
解包后按目录名扫一遍,大致是这么几类东西:
| 目录 | 内容定位 | 构建入口 | 产物 |
|---|---|---|---|
calc | 最小计算器,练手用 | 直接gcc | 单文件可执行程序 |
mycalc/mycalc_ex | yacc/lex 版计算器,含扩展 | Makefile | 词法+语法分析器 |
llparser/llparser_ex | 手写 LL 解析器,不用生成器 | Makefile | 递归下降解析器 |
crowbar_book_0_1~0_4 | 解释器 Crowbar 的分章快照 | make.sh/make.bat | crowbar/crowbar.exe |
diksam_book_0_1~0_4 | 静态类型语言 Diksam 的分章快照 | make.sh/make.bat | diksam/diksam.exe |
这里有个非常具体的结构差异,很多人第一次找不到构建入口就是栽在这:Diksam 的 0_1 和 0_2,源文件放在compiler目录下;到了 0_3 和 0_4,构建入口挪到了main目录。也就是说你在 0_4 里对着compiler目录敲make,会得到一个「没有目标」的报错。同一件事在 Crowbar 各章里没有发生,入口一直在章节目录根上,别把两套的习惯混着用。
mycalc和mycalc_ex是 yacc/lex 的入门线,llparser和llparser_ex是手写解析器的入门线,这两条线的价值在于它们足够小——一个.l加一个.y,几十行就能生成出能算1+2*3的东西。先在这两条线上把「生成器怎么和 gcc 串起来」这件事搞明白,再去看 Crowbar 的十几个.c文件,心理压力会小很多。
2.2 工具链选型:MinGW 还是 Cygwin,先把这件事定死
Windows 下要跑这套代码,绕不开这个选择,而且它不是「装哪个都行」,会直接影响你后面分发和排错的成本。
MinGW 的产物是原生 PE 可执行文件,只依赖系统自带的 C 运行时,拷到另一台没装编译环境的机器上也能双击跑。Cygwin 提供的是类 UNIX 环境,但它编出来的 exe 依赖cygwin.dll,一旦这个 DLL 不在搜索路径上,程序连启动都启动不了,报的还是一条看不出所以然的错。
我的选择逻辑很简单:只想要能跑起来的 exe,用 MinGW;需要./configure && make那套完整 POSIX 工具链(比如第 4 章编正则库),用 Cygwin。两者可以共存,装在同一个盘符下不同目录就行,别装进带空格的路径,这个坑在第 5 章会细说。
MinGW 这边要额外准备两个东西:bison和flex的 Windows 版本。装的时候有个细节——安装包只选「完整包,不含源码」,装完把bin目录加进系统PATH。Cygwin 这边对应的做法是在安装器里勾上bison、flex、make、m4这几个包,注意m4在Interpreters分类里、make在Devel分类里,默认是不勾的。
还有一个特别容易漏的动作:MinGW 自带的构建工具叫mingw32-make.exe,不是make.exe。章节目录里的脚本按make或gmake写死了命令名,所以你需要在MinGW\bin下复制一份改名成gmake.exe(或make.exe),这是后面所有构建能跑通的前提。
2.3 第一次构建:把 crowbar_book_0_1 跑到能读 test.crb
选 0_1 是有理由的,它是全链路最短的一份:词法、语法、表达式求值都有了,变量和控制流还没加,出错时你能定位到具体是哪个.c出的问题。
Linux、macOS 或 Cygwin 环境下:
# 进入第一章目录 cd crowbar_book_0_1 # 脚本会根据平台生成对应的 Makefile sh make.sh # 生成完之后再 make,顺序不能反 make # 用一个自带脚本跑一遍 ./crowbar test/test.crbWindows + MinGW 环境下:
:: 切到章节目录,路径不要带空格和中文 cd C:\dev\crowbar_book_0_1 :: 先跑批处理生成 Makefile make.bat :: 用改名后的 gmake 构建,等价于 mingw32-make gmake :: 直接跑 crowbar.exe test\test.crbmake.sh和make.bat干的其实是同一件事:把当前平台的编译器和链接参数写进 Makefile。所以它们不是构建命令,是配置命令,跑完必须再执行一次make。我第一次用的时候以为make.bat跑完就结束了,结果目录里躺着一个 Makefile 我却不知道。
构建过程里你会看到一长串 gcc 命令行,参数是这样的:
gcc -c -g \ -Wall -Wswitch-enum -ansi -pedantic -DDEBUG \ main.c interface.c create.c execute.c eval.c \ string.c string_pool.c util.c native.c error.c-g保留调试符号,后面用 gdb 单步跟解释器执行流程时必须有。-Wall -Wswitch-enum打开全部常规告警,并强制switch把枚举值列全——这条对写解释器特别有用,你加一个 Token 类型忘了在处理函数里补分支,编译器会当场告诉你。-ansi -pedantic锁死标准 C,避免用到编译器私有扩展,这样一份代码在两个平台上行为一致。-DDEBUG打开代码里的断言和调试打印,等你要做性能对比时把这条去掉再编一遍。
提示:如果你只看到
gmake: 'crowbar' is up to date.而没有任何编译输出,说明上一次构建已经成功,可执行文件就在当前目录,直接跑就行,不用删了重来。
跑通这一步的意义在于:你手上有了一个能读.crb脚本、能算表达式、能报语法错误的程序。接下来所有改动都建立在这个基线之上,一旦后面某章编不过,你至少知道基线是好的,问题出在增量上。
3. bison 与 flex 的接线:.l/.y 生成顺序与 Makefile 三处改动
这套资料里最密集的报错都集中在生成器这一层。原因不复杂:bison和flex是两个独立进程,它们生成的中间文件互相依赖,而 Makefile 里的依赖关系一旦写反,gcc 就会在你毫无防备的时候告诉你找不到头文件。
3.1 从 .y 到 y.tab.c:为什么 .l 不能先编译
Crowbar 的构建链路是这样:crowbar.y经 bison 生成y.tab.c和y.tab.h;crowbar.l经 flex 生成lex.yy.c;而crowbar.l里有一行#include "y.tab.h",因为词法分析器返回的 token 常量是语法文件里定义的。
这就意味着:y.tab.h必须先于lex.yy.c的编译存在。如果 Makefile 把lex.yy.c和y.tab.c放在同一层并列依赖里,make 有可能先编词法器,此时y.tab.h还不存在,gcc 直接抛出:
mycalc.l:3:10: fatal error: 'y.tab.h' file not found #include "y.tab.h"这条报错在mycalc里一模一样地复现,因为mycalc.l第 3 行也是这句 include。修法不是去改.l文件,而是改依赖:
# y.tab.h 由 bison 从语法文件生成,-d 参数就是让它额外吐出头文件 y.tab.h: crowbar.y bison --yacc -dv crowbar.y # 词法器的编译显式依赖头文件,make 就会先做上面那一步 lex.yy.c: crowbar.l y.tab.h flex crowbar.lbison的三个参数含义要清楚:--yacc让输出文件名沿用 yacc 的y.tab.c约定(代码里 include 的是这个名字,改不了);-d生成头文件;-v生成.output报告,把冲突的移进/归约项全列出来,调试语法歧义时这份报告比编译器报错有用得多。
3.2 bison 在 Windows 上的两个暗坑:PATH 与 m4
生成器装好了不代表能用。Windows 上最常见的两条报错,一条是找不到命令,一条是找到了但内部炸了。
第一条:
process_begin: CreateProcess(NULL, bison --yacc -dv crowbar.y, ...) failed. make (e=2): 系统找不到指定的文件。 Makefile:30: recipe for target 'y.tab.h' failede=2就是「文件不存在」。make 调bison用的是系统PATH,而你在某个终端里临时set PATH只对那个窗口有效,换个 CMD 就失效。解决方式是把 bison 的bin目录永久加进系统环境变量,然后关掉所有终端重开再验证。别在原来的窗口里反复set,那是自己骗自己。
第二条更隐蔽。bison 内部要靠m4做宏展开,而 m4 被调用时会带上自己的数据文件路径。如果你把 bison 装进了默认的Program Files (x86)目录,路径里的空格会把参数切断,于是你看到:
m4: cannot open `Files': No such file or directory m4: cannot open `(x86)\GnuWin32\Bison/share/bison': No such file or directory m4: cannot open `D:\Program': No such file or directory四条报错拼起来正好是完整路径被空格剁成的四段。这不是编码问题也不是权限问题,就是路径带空格。解决方式是重装到C:\GnuWin32这种无空格目录,或者手工把share\bison目录复制到无空格路径并设置对应的环境变量。我一般直接在装机第一步就把所有工具链放在C:\toolchain\下,省得后面反复踩。
3.3 一份能直接抄的最小 Makefile
把上面两条揉起来,mycalc这种规模的项目用这样一份 Makefile 就够了,Crowbar 各章的 Makefile 是它的放大版:
# 目标可执行文件名 TARGET = mycalc # 中间目标文件,注意是 y.tab.o 和 lex.yy.o OBJS = y.tab.o lex.yy.o main.o $(TARGET): $(OBJS) gcc -o $(TARGET) $(OBJS) -lm # 语法文件生成 C 源和头文件,-d 不能省 y.tab.c y.tab.h: mycalc.y bison --yacc -dv mycalc.y # 词法文件的编译依赖头文件,这一行是顺序保证的关键 lex.yy.c: mycalc.l y.tab.h flex mycalc.l # 两个 .c 都依赖生成结果,显式写出来避免并行 make 时抢跑 y.tab.o: y.tab.c gcc -c -g -Wall y.tab.c lex.yy.o: lex.yy.c y.tab.h gcc -c -g -Wall lex.yy.c main.o: main.c y.tab.h gcc -c -g -Wall main.c clean: rm -f $(TARGET) $(OBJS) y.tab.c y.tab.h lex.yy.c几个容易写错的地方值得单独说。第一,y.tab.c和y.tab.h写在同一条规则的目标位置,意思是「跑一次 bison 同时产出这两个」,make 会理解为任一缺失都要重跑。第二,lex.yy.o的依赖里同时列了lex.yy.c和y.tab.h,这是给make -j并行构建上保险,不加的话单线程能过、多线程偶尔失败,属于典型的玄学故障。第三,clean必须把生成出来的y.tab.c、lex.yy.c一起删掉,只删.o会让你在下一次构建时用上过期的生成结果,改完.y却不生效,能查半天。
注意:如果 make 报
No targets specified and no makefile found,先看目录里是不是只有make.sh和make.bat而没有Makefile。这两个脚本是生成 Makefile 的,不是 Makefile 本身,得先跑一次。
4. 把 oniguruma 编进 Crowbar:正则库在 Windows 与 Linux 的两条路
到后面几章,Crowbar 需要字符串模式匹配能力,常见的做法是挂一个成熟的正则库上去,而不是自己手写状态机——自己写的那套在边界条件上早晚翻车。资料里用的是 oniguruma 5.9.4 这个版本,两个平台各有一条编译路径,坑完全不同。
4.1 为什么要单独编一个库,而不是把源码拖进工程
把第三方源码直接拷进项目目录,看起来省事,实际后患无穷:源文件命名冲突、编译告警混在自己代码里、升级时无法区分改动。正规做法是把它编成静态库libonig.a,自己的工程只 include 头文件、链接.a。
静态库而不是动态库的理由也很实际:你编出来的crowbar.exe要能拷到别的机器上直接跑,动态库.dll一旦不在搜索路径就是启动失败。静态链接会把需要的代码打进 exe,体积大一点,但省掉一整类部署问题。
Linux 和 macOS 下这一步是标准三部曲:
# 下载后解包 tar -xvf onig-5.9.4.tar.gz cd onig-5.9.4 # 生成 Makefile,这一步会检查编译器、头文件、库函数 ./configure # 编译,-j 后面跟核数可以并行加速 make -j4 # 装到系统目录,需要写权限 sudo make installconfigure那一长串输出里,你只需要盯三件事:checking for alloca... yes、checking for working memcmp... yes、checking for prototypes... yes,这三个都是yes就说明基础环境没问题。最后一行是config.status: creating Makefile,出现了就说明配置成功。
4.2 Windows 下走 bash,还要补一步 ranlib
Windows 上没有configure脚本能直接用的 shell,得借 Cygwin 或者 MinGW 自带的 bash。把压缩包放到盘符根目录(比如C:\),然后:
# 在 bash 里操作,注意路径是 /cygdrive/c/ 这种形式 cd /cygdrive/c tar -xvf onig-5.9.4.tar.gz cd onig-5.9.4 ./configure makemake一般能过,卡住的多半是make install。因为install要往/usr/local/lib写东西,而 Windows 上这个路径的映射关系跟 Linux 不一样,MinGW 的ranlib处理这个路径时会直接报:
D:\MinGW\bin\ranlib.exe: '/usr/local/lib/libonig.a': No such file Makefile:415: recipe for target 'install-libLTLIBRARIES' failed意思是「我编译出来了,但我在目标位置找不到它」。绕过去的办法是跳过make install,手工做它本该做的两件事——给静态库建索引,然后拷到该去的地方:
# 进到 .libs 目录,make 把真正的产物放在这里,外面那个 .a 只是壳 cd .libs # 给静态库建立符号索引,缺这一步链接时会报 undefined reference ranlib libonig.a # 拷到 MinGW 的库目录,链接时 -lonig 才能找到 cp libonig.a /d/MinGW/lib/ # 头文件同理,拷到 include 下 cp ../oniguruma.h /d/MinGW/include/ranlib这一步特别容易被当成可选项跳过。它的作用是把.a里的符号表建好,缺了它,编译阶段一切正常,链接阶段才开始报一堆undefined reference to onig_new之类,而且报错位置跟你写的代码毫无关系,能查到你怀疑人生。
4.3 用十行代码验证库是不是真的能用
装完别急着回去改 Crowbar,先单独验证一遍。写一个只调版本号的最小程序:
/* check_onig.c —— 只验证头文件和库能被正确找到和链接 */ #include <stdio.h> #include <oniguruma.h> int main(void) { /* 初始化正则引擎,不调用的话后续 API 行为未定义 */ int r = onig_init(); if (r != ONIG_NORMAL) { printf("init failed: %d\n", r); return 1; } /* 直接打印库版本,能打出来就说明静态库真的链上了 */ printf("oniguruma version: %s\n", onig_version()); onig_end(); return 0; }编译命令和预期输出:
# -I 指定头文件目录,-L 指定库目录,-lonig 是链接静态库 gcc check_onig.c -o check_onig -I/usr/local/include -L/usr/local/lib -lonig ./check_onig # 期望输出形如:oniguruma version: 5.9.4Windows 下把-I和-L换成D:/MinGW/include、D:/MinGW/lib的形式。如果这一步能过,说明头文件位置对、库文件位置对、符号索引完整,后面 Crowbar 链接失败就只可能是它自己的 Makefile 里少写了-lonig,排查范围一下子缩小到一行。
5. 排查清单:五个高频翻车点,从 m4 路径空格到 0x0d
这套资料的时间跨度不小,社区里积累的报错帖基本都是环境问题而不是代码问题。下面五条是我和周围人踩过、并且能稳定复现的,按「现象 → 原因 → 解决」写清楚,遇到时直接对号入座。
5.1 m4 报一堆路径碎片
现象:执行 bison 时冒出四五条m4: cannot open ...,每条路径都是残缺的,比如Files、(x86)\GnuWin32\Bison/share/bison。
原因:bison 调用 m4 时把数据文件路径作为参数传过去,而路径里含空格,参数被 shell 层面切成了多段,每一段都被当成独立路径去 open。
解决:把 bison 重装到无空格路径(C:\GnuWin32这种),或者把share\bison整个目录复制到无空格位置,再把 bison 的查找路径指过去。改完必须开新终端,旧窗口里的环境变量还是旧的。
5.2 y.tab.h 找不到,但 bison 明明跑过了
现象:mycalc.l:3:19: error: y.tab.h: No such file or directory,或者gcc: y.tab.c: No such file or directory。
原因:Makefile 里lex.yy.c和y.tab.c是并列依赖,make 挑了词法器先编,此时 bison 还没跑;另外有一种情况是上一步 bison 其实失败了(比如 5.1 那个 m4 问题),但 make 继续往下走了。
解决:在 Makefile 里给lex.yy.c显式加上y.tab.h依赖,给y.tab.o加上y.tab.c依赖。同时往回翻构建日志,确认 bison 那一步真的没有报错——很多「找不到文件」的根因是上一条命令其实失败了。
5.3 CreateProcess failed,e=2
现象:process_begin: CreateProcess(NULL, bison --yacc -dv crowbar.y, ...) failed. make (e=2): 系统找不到指定的文件。
原因:bison不在当前进程的PATH里。注意是「当前进程」,你在别的窗口设过不算数;还有一种更隐蔽的情况,装的是 Cygwin 版 bison,但构建用的是 MinGW 的 make,两边环境隔离,互不可见。
解决:统一工具链来源——要么全用 MinGW 版,要么全在 Cygwin 终端里操作。在 CMD 里用where bison确认能找到,找不到就永久加进系统PATH并重开终端。
5.4 程序能跑,但第一行就报 0x0d 错误
现象:在 Windows 上编好,跑./crowbar test/test.crb,报错信息里带着类似5: 非法字符(0x0d)的字样,位置固定在行尾。
原因:.crb脚本文件是 CRLF 换行,而词法分析器的空白处理只认\n,\r被当成非法字符。这不是 bug,是文件编码问题,Linux 下编出来的同名二进制读同一个文件就没事。
解决:转换换行符。Linux 侧的dos2unix test/test.crb最省事;没有这个命令就用sed -i 's/\r$//' test/test.crb。如果你在 Windows 上编但想读同一份脚本,也可以在词法规则里把\r加进空白字符集合,但那属于改代码,不如统一脚本格式来得干净。
5.5 链接报 -lmsvcp60,或者运行时报缺 cygwin.dll
现象:Linux 下构建 0_4 时链接器报cannot find -lmsvcp60;或者 Windows 下双击 exe 弹出「找不到 cygwin.dll」。
原因:前者是 Makefile 里残留了 Windows 专用的 C++ 运行库链接项,Linux 上根本没有这个库;后者是用 Cygwin 工具链编出来的产物依赖cygwin.dll,换台机器就找不到。
解决:-lmsvcp60直接从 Makefile 的LDFLAGS里删掉,这份代码是纯 C,不需要它。cygwin.dll的问题要么把 dll 一起拷过去,要么换 MinGW 重新编一遍——长期看换 MinGW 更省心,产物是原生 exe,没有运行期依赖。
6. 拿 diff 当阅读器:两章之间到底加了什么
这套资料最值钱的用法不是「读完一章读下一章」,而是把相邻两章当成两个提交来对比。书里的叙述是线性的,但代码的增量是非线性的——某一章可能只加了三十行,却引入了整个对象管理机制,你顺着文字读完全感觉不到这个重量。
我的做法是先看目录级差异,再钻到文件级:
# -r 递归 -q 只报差异不打印内容,先看哪些文件被动过 diff -rq crowbar_book_0_1 crowbar_book_0_2 # 定位到具体文件后,看这个文件的逐行改动 diff -u crowbar_book_0_1/eval.c crowbar_book_0_2/eval.c | less # 只想看新增了哪些文件,不看内容差异 diff -rq crowbar_book_0_1 crowbar_book_0_2 | grep "Only in"第一条命令的输出会告诉你哪几个.c被改了、哪几个是新增的。通常是execute.c和eval.c改动最大,新增文件则往往对应一个新模块——比如某一章开始出现 GC 相关的东西,你在Only in里会看到新的源文件,那就是这一章真正的主题。
第二条命令配less是因为改动动辄几百行,直接在终端刷过去等于没看。我一般会先按}跳段落,看每个函数的边界,找到「多出来一整个函数」的位置,那基本就是本章的核心。函数签名比函数体更值得先看——参数里多出来一个Context *或者Object *,往往比函数体里那几十行更能说明设计变化。
有一点必须提醒:别对着 diff 死磕看不懂的改动,先跑一遍。我在对比 0_2 和 0_3 时就吃过这个亏,盯着新增的对象管理代码看了半小时没想明白为什么这么设计,后来把两章各自编出来、跑同一个脚本、用 gdb 跟了一遍内存分配路径,五分钟就清楚了——原来差异不在代码逻辑,在对象的生命周期被推迟了。从那以后我每次对比分章源码,都强制走一遍「先编、再跑、后读 diff」的顺序,读代码的效率比纯看文本高一个数量级。
这套东西本质上是半成品仓库,每一章都是一个能跑的检查点。你要是能把它当成 git 历史来读,收获会比顺着书本翻一遍大得多。希望帮到你。
本文还有配套的精品资源,点击获取