简介《自制编程语言》相关资料1面向希望从零理解语言设计与实现机制的开发者与学习者聚焦编译器、解释器构建及跨平台编译工具链的入门实践。压缩包仅含1个PDF文件大小约2.38MB轻量便携适合作为纸书之外的配套阅读材料。内容围绕Crowbar与Diksam两个自制语言示例展开串起词法/语法分析、yacc/lex与bison/flex的使用、MinGW环境配置以及Linux与Windows下make/gmake的构建差异并讨论运行时、资源管理与代码可读性等设计取舍。已有870人学习可作为课程设计、编译原理自学或小型语言原型开发的参考帮助读者建立从语法规范到可执行程序的完整认知。1. 从一份 ZIP 开始Crowbar 与 Diksam 这两门编程语言真能跑起来很多人对「自制编程语言」的想象是先把《编译原理》啃完、把 LR 分析表手推一遍然后才有资格动手。这份资料包的思路反过来先给你一个几百行就能编译通过的解释器让你看着./crowbar test/test.crb把一行字符串打印出来再回头补理论。包里装的是两门递进的示例语言Crowbar 从 0.1 走到 0.4中途把垃圾回收塞了进去Diksam 在 0.3 之后把「编译成字节码」和「虚拟机执行」拆成了两块独立工程。外面还套着 calc、mycalc、llparser 三个更小的玩具分别对应手写词法分析、yacc/lex 生成、递归下降三条路线正好构成一条从易到难的学习坡道。适合写过 C、看得懂 Makefile、想把「源码文本 → 语法树 → 执行」这条链路亲手焊一遍的人。不适合想直接拿来做生产语言的——它连标准库都很薄。2. 目录结构与版本演进从 crowbar_book_0_1 到 diksam_book_0_4 的四个台阶资料包解压之后不是一个大工程而是一堆互不依赖的小工程。这一点很关键每个目录都有自己的 Makefile能单独make出可执行文件你改坏了 0.2 不影响 0.4 还能跑。我一般拿到这种包第一件事不是急着编译而是先find . -name Makefile数一遍看哪些目录是「完整可构建单元」再决定从哪个入口切进去。2.1 把资料包拆开看八个可独立构建的工程目录按功能和难度排大致是这么几层目录定位关键技术点建议顺序calc最小的四则运算计算器手写词法 手写递归下降第一站mycalc/mycalc_exyacc/lex 版计算器.y文法、.l词法、符号表雏形第二站llparser/llparser_exLL(1) 递归下降解析器预读一个 token、FIRST 集与 mycalc 并行crowbar_book_0_1~0_4完整脚本语言解释器语法树解释执行、0.3 起加 GC第三站diksam_book_0_1~0_2Diksam 早期版本单工程、树遍历执行第四站diksam_book_0_3~0_4Diksam 中后期compiler/与main/分离字节码 VM最后一站calc到llparser这几个是「零件级」的示例每个控制在几百行用来理解.l文件怎么产出lex.yy.c、.y文件怎么产出y.tab.c和y.tab.h。Crowbar 是「整车级」的它把词法、语法、语义检查、执行四段串起来了还带一个memory子目录做对象管理。Diksam 的 0.3、0.4 则是「平台级」的编译器后端不再直接执行语法树而是吐字节码由main里的虚拟机去跑。这种分层的价值在于你在mycalc里把 bison 的冲突搞清楚了到crowbar里看到crowbar.y那几百行产生式才不会懵你在llparser里手写过一遍预读逻辑到 Diksam 里看到它自己做的那套解析器实现就知道为什么它不依赖 yacc 了。2.2 版本之间的增量语法、GC、编译器与虚拟机分层是怎么加上去的同一门语言的四个版本是这份资料里最值得逐版对比的部分。我的做法是把 0.1 和 0.4 并排放着diff一遍重点看三处产生式数量、create.c/create.h里的节点类型枚举、以及新增的源文件。Crowbar 的演进主线大致是0.1 建立最小闭环——表达式求值、变量、if、while、函数定义与调用、原生函数注册文件数不多但五脏俱全0.2 开始补齐更完整的语句与控制流0.3 的目录名本身就点明了主题——加入垃圾回收从此memory相关的分配逻辑不再是裸malloc/free而是统一走一套对象头加标记清扫的路径0.4 在此基础上继续加语言特性并让解释器的错误信息更完整error.c、error_message.c两文件的分工就是把「抛出」和「文案」拆开。Diksam 的断点更明显。0.1、0.2 还是单目录树遍历执行跟 Crowbar 同构到 0.3目录里出现compiler/和main/两个子工程各自有make或gmake的构建入口。这意味着架构从「解析完直接走树」变成了「解析完生成中间代码再由另一套循环取指令执行」。这个切分是理解字节码虚拟机的分水岭编译器那侧只管把 AST 压成线性指令序列虚拟机那侧只管维护一个指令指针、一个操作数栈和一个运行时环境。两侧通过一份共享的 opcode 定义和字节码文件格式通信。提示看版本演进别只看新增了什么还要看删了什么。Diksam 0.3 之后main里不再有完整的语法分析代码那部分被整体搬走了这种「搬家」往往比新功能更能说明设计意图。3. 工具链搭建MinGW、Cygwin 与 bison/flex 三件套的落位这套代码的年代感很强Makefile 里写的是make、依赖的是gcc、词法语法靠flex/bison还有一个正则库oniguruma 5.9.4负责处理字符串匹配。放到今天的环境里直接跑八成第一步就翻车。我在三台不同配置的机器上装过这套工具链血泪经验是装之前先想清楚「我到底要跑哪个目录」因为 Linux 和 Windows 两条路需要的东西完全不一样。3.1 Windows 下的 MinGW 与 gmake/mingw32-make 的命名差异Windows 侧用 MinGW 就够了不需要整套庞大工具链。安装时在组件列表里勾上mingw32-base这一项里带着gcc和make再顺手把mingw32-gcc-g勾上后面改代码时可能用得上。安装目录里不要有空格和中文——这句提醒不是客套第 5 章里那条m4: cannot open Files的报错就是路径空格引发的。装完之后有个很容易被忽略的点MinGW 提供的 make 可执行文件叫mingw32-make.exe不叫make.exe。而这套代码的 Makefile 里递归调用写的是gmake。三个名字一回事。我第一次就是make敲下去提示找不到命令愣了半天。# 在 MinGW 的 bin 目录下做两个副本让 make / gmake 都能用 cd /d/MinGW/bin cp mingw32-make.exe make.exe cp mingw32-make.exe gmake.exe这两行做完make、gmake、mingw32-make三个词在命令行里就等价了。参数上没什么特别的-f指定别的 Makefile、-j并行编译都照常可用。不过要注意这套老代码里有些规则的命令是cd ./memory; make这种写法Windows 的cmd对;支持得不好遇到这种报错就老老实实进子目录单独敲一次。3.2 bison 与 flex 的 Windows 包m4 路径空格引发的连锁报错Windows 版 bison/flex 有现成的安装包选Complete package, except sources那个版本装完bison.exe、flex.exe就在安装目录的bin下把它们加进PATH即可。真正麻烦的是 bison 不是独立运行的——它依赖 GNU m4 做宏展开而那个年代的包里m4 的搜索路径是硬编码的一旦安装路径里出现空格比如装在D:\Program Files (x86)\GnuWin32\Bison就会看到一串让人头皮发麻的输出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原因很简单命令行在空格处被切断了m4 把D:\Program和Files当成了两个独立参数。解决办法有两个一是重装到无空格路径比如D:\GnuWin32二是把这个目录加入BISON_PKGDATADIR环境变量让它显式指定数据目录。# 方式二显式告诉 bison 它的 m4sugar 之类的数据文件在哪 set BISON_PKGDATADIRD:\GnuWin32\share\bison set PATHD:\GnuWin32\bin;%PATH% # 验证一下能不能跑通 bison --version flex --version环境变量里的路径同样不能带空格这是同一个坑的两种表现。我后来索性把整套 GnuWin32 装在D:\gnu32从此这类问题再没出现过。3.3 Linux 与 Cygwin 下 oniguruma 的编译、ranlib 与 installCrowbar 0.4 之后用到了正则表达式库 oniguruma版本 5.9.4。Linux 下标准三步走没问题# Linux 侧标准 autotools 流程 tar -xvf onig-5.9.4.tar.gz cd onig-5.9.4 ./configure # 生成 Makefile、config.h、onig-config make # 编译产物在 .libs/ 下 make install # 拷到 /usr/local/lib 和 /usr/local/include./configure那一步会跑一堆探测输出几十行checking for ...只要最后creating那几行都生成成功就说明环境没问题。make install需要root权限因为它要写/usr/local。麻烦出现在 Windows 上。如果你用 Cygwin 里的 bash 编译这套代码configure会把--prefix认成 Cygwin 的/usr/local但调用make install时链接和符号表处理的却是 MinGW 的ranlib.exe。两套路径体系一撞就得到D:\MinGW\bin\ranlib.exe: /usr/local/lib/libonig.a: No such file Makefile:415: recipe for target install-libLTLIBRARIES failedranlib的作用是给静态库里的.o建符号索引ar打完包不建索引的话链接器大概率会抱怨「找不到符号」。绕过办法是跳过make install手动做两件事# 进到 .libs 目录那里才是真正的产物 cd .libs # 用 MinGW 的 ranlib 建符号索引 ranlib libonig.a # 手动拷到 MinGW 认的库目录 cp libonig.a /d/MinGW/lib/ cp ../oniguruma.h /d/MinGW/include/这里的关键认知是libonig.a只要能被链接器在-L路径里找到放在哪都行不必非得走make install。头文件同理#include oniguruma.h能找到就够。注意Cygwin 编译出来的.a和 MinGW 的.a在对象格式上基本兼容但如果你混用了 32 位和 64 位的工具链链接阶段会报架构不匹配这时候回头检查gcc -dumpmachine的输出即可。4. 构建与运行make.sh 与 make.bat 背后发生了什么工具链就位之后构建本身反而是最短的一步。包里给 Linux 准备了make.sh给 Windows 准备了make.bat本质都是make换了层皮。但我强烈建议你第一次别直接跑脚本而是手工敲一遍make看着它一行行吐gcc -c -g ...这样出错的时候才知道停在哪一步。4.1 一个 .l 一个 .y 是怎么变成可执行文件的所有基于 yacc/lex 的工程构建流程都是同一个套路理解一遍到处适用。核心是两条隐式依赖链.l文件由 flex 处理产出lex.yy.c.y文件由 bison 处理产出y.tab.c和y.tab.h。其中.h里放的是 token 编号和语义值联合体.l文件里那句#include y.tab.h就是靠它拿到这些定义。# crowbar 的 Makefile 骨架我只保留跟构建链路相关的部分 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 crowbar: $(OBJS) gcc -o crowbar $(OBJS) -L./memory -lonig # 关键规则.y 一次产出两个文件作为同一个目标声明 y.tab.c y.tab.h: crowbar.y bison --yacc -dv crowbar.y lex.yy.c: crowbar.l flex crowbar.l # 依赖关系必须写死用到 token 定义的目标文件都要依赖 y.tab.h main.o: main.c y.tab.h gcc -c -g -Wall -Wswitch-enum -ansi -pedantic -DDEBUG main.c几个参数值得说清楚。bison --yacc是让产出的文件名沿用传统 yacc 的命名习惯改这个参数会让所有写死y.tab.h的源文件全部失效-d是生成那个头文件不加就没有 token 定义-v顺手产出一份y.output里面是完整的状态机和分析冲突记录调文法的时候这份文件比报错信息有用得多能看到到底在哪个状态上移进/归约打架。-Wall -Wswitch-enum -ansi -pedantic这组警告开得很狠尤其是-Wswitch-enum给枚举加新值时忘了在 switch 里处理编译阶段就会被拦下来——这在给语言加语法节点时是救命的。链接那行的-L./memory -lonig也值得一提memory是包里一个独立编译的子工程最终以静态库形式提供对象分配接口onig就是上一章装的 oniguruma。如果链接时提示找不到符号八成是这两个库之一没找到。4.2 跑起来calc、mycalc、llparser 与 crowbar 的示例输入先从最小的开始验证环境。calc编译完直接运行输入12*3回车期望输出7而不是9这就说明优先级处理是对的。mycalc是它的 yacc 版本行为应当完全一致——如果你的mycalc算出9那说明.y文件里的运算符优先级声明写错了或者产生了移进/归约冲突被 bison 默认按移进解决了。llparser跑起来是交互式的一行行读输入、一行行吐结果它演示的是递归下降怎么在没有语法生成器的帮助下活下来。这个程序最值得跑的原因是它内部有个词法分析器用固定长度缓冲区读行第 5 章那条踩坑记录就出在这里。Crowbar 的验证方式稍微正式一点# 先在一个目录里 make 出可执行文件 cd crowbar_book_0_1 make # 再用它跑自带的测试脚本 ./crowbar test/test.crbtest/test.crb是一段用 Crowbar 自己写的脚本里面覆盖了变量、循环、函数调用和原生函数。跑通它等于把解释器的主干路径全走了一遍。如果报错报错格式一般形如「第几行、什么错」行号信息来自词法分析器维护的line_number。Diksam 0.3 之后要分两步先进compiler目录编出编译前端产出一个中间文件再进main目录编出虚拟机拿中间文件去执行。这个流程本身就是对「编译 / 执行」分层的演示与 Crowbar 那种一步到位的解释执行形成对照。4.3 解释执行链路从源码文本到输出的四段把 Crowbar 的执行过程切开大致是四段。第一段是crowbar.l驱动的词法扫描把字符流切成 token注释和字符串字面量要用起始条件%start/%x单独处理否则跨行的/* */会被切成一堆乱码 token。第二段是crowbar.y里的语法分析边归约边调create.c里的构造函数造 AST 节点每个节点是一个带enum类型标签的结构体。第三段是语义检查查变量有没有声明、函数参数个数对不对错误通过error.c里的统一入口抛出。第四段是execute.c里的树遍历执行器它按节点类型分发execute_statement处理语句、eval_expression处理表达式遇到函数调用就压一个新环境。这四段的分界在代码里非常清楚create.c只造节点不求值execute.c只执行不解析eval.c专门管表达式求值interface.c负责原生函数注册。想加功能先想清楚该往哪个文件里加这个分层就是给你的路标。5. 避坑排查从 bison 找不到文件到 y.tab.c 缺失的六类报错这套资料年代久远配套工具链也换了好几代报错信息往往跟真实原因隔着好几层。下面这六条是我在不同机器上反复撞到的按「现象 → 原因 → 解决」整理遇到别的错也可以照这个思路拆。5.1 gmake: No targets specified and no makefile found现象是敲下gmake或make之后立刻得到No targets specified and no makefile found加一句Stop。原因通常不是 Makefile 有问题而是当前目录根本不对——你可能停在解压出来的根目录而 Makefile 在每个子工程目录里。解决方式是先ls确认有没有Makefile或makefile没有就cd进具体的那一层比如cd crowbar_book_0_1。还有一种可能是文件名大小写错了Windows 不敏感拷到 Linux 上就暴露了。5.2 bison --yacc -dv crowbar.y 报 CreateProcess failed现象是 make 执行到 bison 那条规则时失败输出process_begin: CreateProcess(NULL, bison --yacc -dv crowbar.y, ...) failed后面跟一句乱码的系统提示最后make: *** [y.tab.h] Error 2。原因只有一个PATH里找不到bison。要么根本没装要么装了没加环境变量要么加了但没重开命令行窗口。解决方式是where bisonLinux 用which bison确认能否定位到能定位到还要再验一次bison --version如果这里再报 m4 相关的错就是上一章说的路径空格问题把安装目录挪到无空格路径重装即可。5.3 mycalc.l:3:19: fatal error: y.tab.h: No such file现象是 flex 处理.l文件时报错说#include y.tab.h找不到。原因不是.l写错了而是构建顺序错了——y.tab.h是 bison 的产物如果先编译.l或者用gcc手工编译lex.yy.c这个头文件还没生成。解决方式是先单独跑一次bison --yacc -dv mycalc.y确认目录里出现y.tab.c、y.tab.h和y.output再跑flex mycalc.l最后编译。想一劳永逸就在 Makefile 里把依赖写全lex.yy.c: mycalc.l y.tab.h。这条在 macOS 上尤其常见因为系统自带的 flex 和 bison 版本较老行为还会有点差异。5.4 /usr/bin/ld: cannot find -lmsvcp60现象是在 Linux 下链接 Crowbar 时链接器抱怨找不到libmsvcp60.so。原因是 Makefile 里带的链接参数是给 Windows 环境准备的msvcp60是某个运行库在 Windows 下的名字Linux 上根本没有对应文件。解决方式是把-lmsvcp60从链接行里删掉——C 标准库部分gcc默认就链上了不需要额外声明。改完记得把改动记在注释里别下次换机器又踩一遍。5.5 ./crowbar test/test.crb 报非法字符 0x0d现象是脚本明明看着没问题一跑就报「第 5 行非法字符(0x0d)」。原因是文件换行符是 CRLF0x0d就是那个回车符而词法分析器的空白字符集合里只写了空格、制表符和\n没有\r。解决方式有两种一是转文件dos2unix test/test.crb二是改词法在.l的空白规则里把\r加进去。我更倾向第二种因为这是解释器该兼容的东西——用户从 Windows 拷脚本过来是很正常的场景让语言去适配文件而不是让文件迁就语言。5.6 llparser 里的 st_line 未初始化这个不是环境问题是代码本身的 bug资料里也提到过。词法分析器里声明了static char *st_line;这样的指针然后在循环里直接fgets(st_line, LINE_BUF_SIZE, stdin)。指针没有指向任何有效内存fgets往一个野地址写数据运气好是崩溃运气不好是静默写坏别的地方。正确写法是换成数组static char buf[1024];然后用fgets(buf, sizeof(buf), stdin)。改的时候顺手把LINE_BUF_SIZE换掉用sizeof就不会出现「缓冲区大小和宏对不上」的二次问题。这种 bug 在自制语言的示例代码里很常见读别人代码时看到裸指针接输入先警一警。提示这六条里有四条是环境问题、两条是代码问题。环境问题改一次就一劳永逸代码问题才值得记进笔记。判断标准很简单——换台机器还复现的是代码问题。6. 二次开发改造给 Crowbar 加一个新语法节点要动哪几个文件想真正吃透这套代码加一个语法特性是最有效的办法。以给 Crowbar 加一个复合赋值运算符为例改动点是有固定套路的照着走一遍你就摸清了整条流水线。文件改动内容注意点crowbar.l无需改和已在 token 表里若加**这类新符号才要动crowbar.y加产生式assignment_expression: ... PLUS_ASSIGN ...注意优先级放在赋值那一档的最右create.h/create.c新增节点类型枚举与构造函数枚举加了所有 switch 都要补分支eval.c在表达式求值里处理新节点复用已有二元运算的求值逻辑execute.c若涉及语句层的短路行为在此处理与eval.c的职责别混改动时最容易翻车的是枚举。给节点类型加一个新值之后execute.c和eval.c里那些按枚举分发的switch会开始告警——这正是-Wswitch-enum的作用它逼你把每个分支都补全而不是靠default糊过去。我第一次加节点就是图省事写了default: break;结果新语法被静默跳过输出全空查了两个小时才想起这个开关。验证的时候别上来就跑大脚本先写最小用例a 1; a 2; print(a);期望输出 3。跑通再逐步加变量作用域、函数内赋值、右结合嵌套这些边界一个边界一个用例。这个习惯是从那次静默跳过之后养成的现在不管改哪个模块都强制先把最小用例跑一遍再往下写。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?