
简介这份PDF资料围绕“自制编程语言”这一主题面向对编译原理和语言实现感兴趣的开发者系统介绍从零设计一门编程语言的核心过程。内容覆盖语言设计原则、语法与语义定义、词法分析、语法分析、代码生成与运行时管理等环节并以Crowbar和Diksam两款自制语言为例展示如何借助yacc/lex、bison/flex、MinGW等工具在Linux与Windows环境下完成编译器和解释器的实现。资源为单个PDF文件总大小2.38MB共1个文件适合作为编译原理自学或课程设计的补充参考。已有870人学习下载内容紧凑且具实操参考价值可帮助读者快速建立编程语言实现的知识框架。1. 自制编程语言资料到底打包了什么从calc到Diksam的一套完整实现想动手写一门自家的编程语言最大的障碍往往不是“不会写代码”而是“不知道从哪里下刀”。是先做词法分析还是先做语法分析解析器要不要用生成器内存谁来回收这套《自制编程语言相关资料》恰恰把这些问题的答案都打包好了。它不只是一份讲义而是从最基础的计算器程序 calc 开始一路递进到完整解释器 crowbar、再到编译器 Diksam 的整套源码包每个阶段都带版本号还整理了 Linux 和 Windows 两套构建脚本。适合两类人一是想搞懂解释器与编译器内部原理的从业者二是想找一个能改、能编译、能跑通全流程的参考实现来“抄作业”的人。我把环境和源码都跑过一遍下面写出能直接复现的步骤以及那些典型的坑。2. 搭建编译环境MinGW、bison/flex与Oniguruma的选型与安装这套源码包最让人头疼的不是代码本身而是它依赖的工具链。crowbar 和 Diksam 的语法分析器是用 bison/flex 生成的正则表达式部分依赖 Oniguruma而 Windows 下又得选 MinGW 或 Cygwin。环境搭不对后面的编译基本就是一场翻车演出。2.1 MinGW还是Cygwin先想清楚你要跑在哪套工具链上源码包里自带的是 make.sh 和 make.bat分别面向 Linux 和 Windows。Windows 用户面临第一个选择题用 MinGW 还是 Cygwin。二者看起来都能在 Windows 上跑 GCC但本质差别很大。Cygwin 是在 Windows 上模拟了一个 POSIX 环境编译出来的 exe 通常依赖 cygwin.dll 这个运行时相当于带了一层虚拟层。MinGW 则是把 GCC 工具链原生移植到 Windows编译产物不依赖额外的 DLL直接调用 Windows 系统 API。对这套源码而言MinGW 更省心因为作者的原版说明里也优先推荐 MinGW。用 Cygwin 也不是不行但要多装一堆包。我自己在 Windows 上第一次跑 crowbar_book_0_2 时就是被 Cygwin 的依赖折腾了一圈最后切回 MinGW 几分钟就编过了。MinGW 的安装方式很直接从官网下载安装器运行时在 Installation Directory 里指定安装路径勾选 mingw32-base 和 make 组件Apply 之后把 bin 目录加进系统 PATH。# 把 MinGW 的 bin 目录挂进 PATH注意替换成你自己的安装路径 export PATH/d/MinGW/bin:$PATH # 验证 gcc 是否可用 gcc --version提示MinGW 安装器里的 make 组件装完后可执行文件名是 mingw32-make.exe。如果想让 make 命令直接可用可以把它复制一份命名为 make.exe或者干脆创建别名否则手敲命令很容易踩“make 找不到”的坑。2.2 bison与flex解析器生成器在Windows下的安装路径陷阱crowbar 和 Diksam 的 .y 和 .l 文件需要 bison 与 flex 来生成 C 代码。Linux 用户一条 apt install 就解决了Windows 用户却经常卡在这里。最常见的坑是去第三方 Win32 移植站点下载了 bison 的安装包默认装到了C:\Program Files\GnuWin32\Bison。这一步埋下了两个雷。第一个雷是路径里的空格。bison 内部会调用 m4 来处理语法文件而 m4 在解析带空格的长路径时容易把路径切断报出m4: cannot open Files: No such file or directory。第二个雷是 Program Files 目录权限复杂后面手动调整文件时还要处理 UAC。我一般会下载 bison 和 flex 的“Complete package, except sources”压缩包手动解压到C:\GnuWin32这种无空格短路径下再把C:\GnuWin32\bin加入 PATH。检查是否装好的方法很简单bison --version flex --version如果两条命令都能输出版本号说明工具链就位。这里有个关键点容易被忽略bison 生成的是 y.tab.c 和 y.tab.hflex 生成的是 lex.yy.c而 lex.yy.c 里通常会#include y.tab.h。也就是说bison 必须先生成头文件flex 才能在后面顺利编译。后面遇到y.tab.h: No such file or directory的错误十有八九是 bison 没跑成功或者 Makefile 里生成头文件的规则顺序错了。2.3 Oniguruma正则库从configure到ranlib的完整编译流程crowbar 的正则表达式支持依赖 Oniguruma 库源码包里提到的是 5.9.4 版本。这个库在 Linux 下编译很顺configure、make、make install 三步走但 Windows 下有几个隐蔽的问题。Linux 下如果用的是 CentOS/RHEL 这类系统libonig.so不一定在默认搜索路径里编译时可以手动指定-I和-L参数。我在某台虚拟机上编译时遇到/usr/bin/ld: cannot find -lonig这个链接器错误说白了就是库路径没给到。# Linux 下编译 Oniguruma 5.9.4 wget_oniguruma_archive tar -xvf onig-5.9.4.tar.gz cd onig-5.9.4 ./configure --prefix/usr/local make sudo make installWindows 下用 MinGW 编译要走一段别扭的路。首先要用tar -xvf解包然后把C:\onig-5.9.4里的一堆文件展开。接着在 cmd 里bash进入 MinGW 的 shell 环境手动执行./configure和make。这一步比 Linux 麻烦因为 Windows 下的 make 工具链对 shell 脚本的支持并不完整。这里有个经典坑configure 之后生成的 libonig.a 在.libs子目录里直接 make install 会报ranlib: /usr/local/lib/libonig.a: No such file。原因是 Windows 的 MinGW 工具链对 install 目标的支持不完整需要手动把库文件复制过去再手动执行 ranlib。# Windows MinGW 下解决 make install 失败的常用方案 cd .libs ranlib libonig.a mkdir -p /usr/local/lib cp libonig.a /usr/local/lib/ cp libonig.dll.a /usr/local/lib/ 2/dev/null || true提示make install 失败时不要急着改 Makefile先看错误是出在“复制文件”还是“更新索引”。如果是 ranlib 找不到文件多半是路径没建对或库文件不在预期位置。3. 从mycalc到crowbar跟着源码包搭出第一个解释器环境就绪之后重点就该放在源码包本身。这套资料最有价值的地方在于它的递进结构不是直接丢给你一个完整的解释器让你看天书而是先给你一个能跑的 calc逐步加功能到 mycalc、mycalc_ex、llparser最后才是 crowbar。理解了这个递进关系读代码的效率会高很多。3.1 读懂源码包的递进结构calc、mycalc、llparser分别解决什么源码包里最基础的是 calc一个最简单的计算器程序。它本身不涉及 lex/yacc硬编码地用递归下降解析表达式。而 mycalc 开始引入 yacc/lex用 .y 文件定义语法规则用 .l 文件定义词法规则。mycalc_ex 则是在 mycalc 基础上加了更多扩展语法比如乘除优先级、括号表达式等。llparser 和 llparser_ex 是另一个维度的练习它们不依赖 yacc/lex而是用手写的 LL 解析器来做同样的工作。这正是这套资料的巧妙之处——同样一门口语的语法你可以看到自动生成解析器和手写解析器两种实现路线对比下来对语法分析的理解会深很多。crowbar 则把这些串起来做成一个真正能跑脚本的解释器。它有词法分析、语法分析、执行器、内置函数native 函数、字符串池、GC 等模块。源码包里的目录名也很有意思crowbar_book_0_1 到 0_4 是四个迭代版本每个版本都比上一个多一些关键能力。# 源码包解压后的顶层结构典型目录 calc/ # 最简计算器递归下降不用生成器 mycalc/ # 引入 yacc/lex 的计算器 mycalc_ex/ # 扩展版支持更多运算 llparser/ # 手写 LL 解析器实现 llparser_ex/ # 扩展的手写解析器 crowbar_book_0_1/ # crowbar 解释器第一版 crowbar_book_0_2/ # 加入 GC垃圾回收 crowbar_book_0_3/ # 进一步完善运行时 crowbar_book_0_4/ # 稳定版 diksam_book_0_1/ # Diksam 编译器第一版从 calc 直接跳到 crowbar 会有点懵但按顺序读代码就能看出每一层解决的问题。比如 calc 里没有符号表的概念mycalc 里开始有变量的赋值和引用crowbar 里才出现函数定义和闭包。这种“一层层加功能”的学习曲线比直接啃一整本编译书舒服得多。3.2 编译并运行crowbar的完整步骤crowbar 每个版本目录下都有 MakefileLinux 下直接 makeWindows 下用 mingw32-make 或 gmake。编译之前先确认目录里没有上次失败遗留的 y.tab.c 或 y.tab.h有的话全删掉否则可能出现新旧文件混用导致的一堆诡异报错。# Linux 下编译 crowbar_book_0_1 cd crowbar_book_0_1 make clean # 清理历史产物尤其是 y.tab.c 等生成文件 make # 产出的可执行文件是 crowbar跑一个测试脚本试试 ./crowbar test/test.crb如果编译报错y.tab.h: No such file基本可以断定 bison 没有成功执行。需要手动把 bison 的路径确认一遍或者在 Makefile 里给 bison 加上绝对路径。某次我在一个没有把D:\MinGW\bin加入 PATH 的 cmd 窗口里编译报错信息是process_begin: CreateProcess(NULL, bison --yacc -dv crowbar.y, ...) failed后面的 error 是make (e2): 系统找不到指定的文件。这个提示很明确——make 会启动子进程调用 bison但系统 PATH 里没有 bison。Windows 下更稳妥的做法是在项目目录里建一个编译脚本把关键工具用绝对路径写死#!/usr/bin/env bash # Windows MinGW 环境下的编译脚本实例 export PATH/d/MinGW/bin:/c/GnuWin32/bin:$PATH cd /c/win_sjis/crowbar_book_0_1 mingw32-make clean mingw32-make提示crowbar 的 Makefile 里-Wall -Wswitch-enum -ansi -pedantic -DDEBUG这些编译选项是刻意开着的。-Wswitch-enum会警告 switch 枚举分支不完整这在码编译器时非常有用能逼着你自己补全所有语法节点的处理逻辑。3.3 crowbar内部的GC与字符串池解释器最核心的两个机制crowbar ver.0.1 之后的版本加入了 GC这是解释器从“能跑”走向“能用”的关键一步。crowbar 的 GC 是 stop-the-world 标记清除算法在内存紧张或分配次数达到阈值时触发。源码里 eval.c 和 execute.c 里凡是创建新对象的路径最终都会回到一个统一的分配入口GC 扫描时从根集合出发沿着执行栈和全局变量表标记所有可达对象然后统一回收不可达的。字符串池是另一块值得研究的设计。crowbar 里每个字符串字面量只存一份通过 string_pool.c 统一管理。这样做的好处是字符串比较可以用地址判断而不是逐字节比较坏处是字符串不会被释放运行得越久内存占用越大。crowbar 的实现里字符串池和 GC 共存设计上做了折衷池里的字符串标记为不可回收但池本身有容量上限。/* string_pool.c 中字符串池的核心结构示意 */ #define STRING_POOL_MAX 1024 static char *string_pool[STRING_POOL_MAX]; static int pool_count 0; char *alloc_string(const char *str) { int i; for (i 0; i pool_count; i) { if (strcmp(string_pool[i], str) 0) { return string_pool[i]; /* 命中池直接返回已有地址 */ } } string_pool[pool_count] strdup(str); /* 未命中拷贝进池 */ pool_count; return string_pool[pool_count - 1]; }这段代码虽然示意性质强但把思路讲透了先查池里有没有相同的字符串有就直接复用地址没有才复制一份存进池子。理解了这个机制再去看 execute.c 里字符串变量的赋值逻辑会非常清晰因为赋值本质上就是在传递一个指向池内缓冲区的指针根本不涉及字符串拷贝。GC 和字符串池的配合也是值得注意的因为字符串池里的内存不会再次释放crowbar 在实现字符串拼接时专门做了优化——拼接结果如果比源字符串短直接在原缓冲区上覆盖否则才新分配。这个细节对理解“解释器怎么做内存优化”很有启发。4. Diksam编译器的四个版本从字节码到虚拟机crowbar 是解释器Diksam 则是编译器加虚拟机。Diksam 的各版本演进轨迹非常有学习价值源码包里四个版本层层递进从最简单的编译执行到后续加入面向对象特性能清楚看见一个编译器是如何慢慢“长大”的。4.1 ver.0.1与0.2compiler与解释器的分工diksam_book_0_1 里已经分出了 compiler编译器和 runtime运行时两大部分。compiler 负责把 Diksam 源码编译成字节码runtime 负责执行字节码。这个设计理念和 Java 很像——先编译到中间表示再在一个虚拟机上执行。0.2 版本里加入了垃圾回收。注意这里的 GC 和 crowbar 那边的 GC 实现角度不同crowbar 是树遍历解释器GC 在 AST 上工作Diksam 是栈式虚拟机GC 必须感知操作数栈、调用帧、全局变量表这些运行时结构。所以 diksam 的 GC 更像 JVM 里那种“从根集合出发扫描虚拟机栈”的思路。# 编译 diksam_book_0_1 的前提 # 0.1 和 0.2 需要先编译 compiler 部分再编译 runtime 部分 cd diksam_book_0_1 make clean make编译时要注意 0.1 和 0.2 的 Makefile 依赖compiler子目录里的生成代码。如果 make 中途报错找不到某些 .c 文件十有八九是make clean把生成文件删了但 bison/flex 没有重新执行成功导致下一轮编译拿不到完整的源文件。4.2 ver.0.3与0.4独立的main入口与构建方式变化diksam_book_0_3 和 0_4 的目录结构有一个明显变化——开始有了独立的main入口目录。前两个版本还是把 compiler 和 runtime 揉在一个可执行文件里0.3 之后把启动逻辑单独拎出来结构上更接近一个真正的编译器项目main 负责读源文件、调用编译器、启动虚拟机compiler 只负责产出字节码runtime 只负责执行字节码。这样的目录拆分为后续加功能打好了基础。0.4 版本加入了面向对象的类和继承支持这时候如果没有清晰的模块边界改动起来会非常痛苦。每个版本的 Makefile 也变了不再是单一目录里一条 make 通关而是要在外层目录运行 make让它递归进子目录构建。# diksam_book_0_4 的构建Linux 环境 cd diksam_book_0_4 ./make.sh # 或手动执行 make提示Diksam 对 make 的依赖比 crowbar 更明显。0.3/0.4 在 Windows 下用 MinGW 编译时建议在 MinGW shell 里跑make而不是在 cmd 里调用mingw32-make.exe否则子目录递归时会因为 shell 命令差异报一堆莫名错误。4.3 用对比的眼光读源码版本演进的四个观察点如果说读者的目标是“搞懂编译原理”那读这四版 Diksam 源码时建议带着四个问题去看第一字节码指令集从 0.1 到 0.4 增加了哪些指令编译器为“类”特性生成了哪些新指令第二符号表的结构怎么变的0.1 里只有全局函数和变量0.4 里多了类成员和继承关系符号表必须能表达作用域嵌套。第三运行时栈的布局怎么调整加入对象和类后调用帧里除了局部变量和返回地址还要有对象的引用。第四错误处理机制有没有变化0.1 里可能只是简单返回错误码0.4 里应该有类似异常处理的结构。每个版本之间的 diff 就是最生动的教科书。只看 diff 里红绿变化的行比从头通读一个四五百行的 execute.c 效率高得多。后面专门讲怎么用 git/文件对比工具来消化这套资料这里先不展开。5. 避坑指南编译自制语言过程中最典型的五个坑这套资料拖了几年时间网上各处反馈里踩坑记录非常多。我把最常见的几个整理出来按“现象 → 原因 → 解决”的顺序写你们遇到类似错误时可以直接对照。5.1 bison找不到process_begin与PATH环境变量现象在 Windows 的 cmd 窗口里执行mingw32-make报错信息是process_begin: CreateProcess(NULL, bison --yacc -dv crowbar.y, ...) failed. make (e2): 系统找不到指定的文件。原因make 执行到生成 y.tab.h 的规则时需要调用 bison 命令但 bison 的可执行文件不在系统 PATH 里。MinGW 的 make 不会有任何提示说你缺命令只会在 CreateProcess 这一步直接失败。解决把 bison 和 flex 的安装目录加入 PATH并且保证 PATH 里的路径没有空格。然后在同一个终端窗口里执行bison --version确认能找到。更保险的是用绝对路径在 Makefile 里覆盖BISON C:\GnuWin32\bin\bison.exe。从那以后我每次在 Windows 下编译这类项目都先敲一遍where bison确认工具在不在再开始 make。5.2 路径中的空格m4无法打开Files目录现象bison 确实执行了但报错m4: cannot open Files: No such file or directory紧接着还有一连串关于(x86)\GnuWin32\Bison/share/bison的错误。原因这是把 bison 装到了C:\Program Files\GnuWin32\Bison这种带空格的目录导致的。bison 内部用 m4 处理语法文件m4 按空格拆解参数把路径切成了好几段后面所有的 m4 宏文件都解析不到。解决卸载掉 Program Files 下的安装版改用手动解压方式把 bison/flex 放到C:\GnuWin32这样的短路径目录再把C:\GnuWin32\bin加到 PATH。这个问题的根源不是 bison 本身而是工具链对空格路径的兼容性所以我后来养成了习惯Windows 下所有编译工具一律装到无空格目录。5.3 Linux下运行crowbar报错0x0dCRLF换行符问题现象crowbar 在 Linux 下编译成功后运行./crowbar test/test.crb报错定位到一个脚本文件的某一行并且提示输入行包含了一个0x0d回车符。原因crowbar 的测试脚本是在 Windows 环境下打包的行尾带了 CRLF而 Linux 下的词法分析器只认识\n看到\r就当成未知字符。这类问题在源码包里很常见尤其是作者用 Windows 开发、在 Linux 发布时。解决用 dos2unix 工具把测试脚本转成 Unix 换行符或者直接 vim 打开后:set ffunix再wq保存。源码目录里的.c和.h文件不受影响因为编译器本身能处理带\r的行但脚本文件被词法分析器逐字符读入时就暴露了。# 批量转换所有测试脚本的换行符 dos2unix test/*.crb ./crowbar test/test.crb5.4 make install失败Windows下的ranlib问题现象在 Windows 的 Cygwin 或 MinGW 环境下编译 Oniguruma执行make install时失败报错ranlib: /usr/local/lib/libonig.a: No such file。原因Oniguruma 的 Makefile 里 install 目标会调用libtool --modeinstall把编译好的库安装到系统目录然后执行 ranlib 更新库索引。在 Windows 下libtool 生成的路径和 MinGW 的 ranlib 之间总是对不上实际文件复制过去了但 ranlib 拿到的路径是错的。解决不依赖 make install手工复制.libs目录下生成的库文件到目标目录然后手动执行 ranlib。这个方法虽然原始但比和 Makefile 死磕高效得多。注意编译时如果用的是./configure --prefix/usr/local手动复制时也要放到对应的/usr/local/lib下否则后面链接时又找不到库。5.5 y.tab.h找不到bison没有生成头文件的连锁反应现象执行 make 时gcc 编译 lex.yy.c 报错fatal error: y.tab.h file not found。原因lex.yy.c 里第一行就是#include y.tab.h而 y.tab.h 是 bison 生成的文件。bison 没有执行成功可能没装或 PATH 没配对又或者 Makefile 的构建顺序不对——lex.yy.c 先编译了y.tab.h 还没生成。解决先清掉目录里所有生成文件y.tab.c、y.tab.h、lex.yy.c再重新 make。如果确认 bison 没问题可以手动执行bison --yacc -dv crowbar.y生成 y.tab.h再执行 make。注意这里-d参数必不可少缺了它 bison 不会生成头文件这也是很多人gcc -c -g lex.yy.c时才发现头文件不存在的原因。6. 把源码包用出价值的进阶技巧靠diff追踪版本演进6.1 用diff对比crowbar从0.1到0.4的改动源码包里最有价值的东西其实不是单看某个版本而是版本之间的差异。crowbar_book_0_1 到 0_4Diksam 的 0_1 到 0_4每一版都对应书里的一章改动集中在某个主题上。手动翻代码判断“这版改了什么”很慢直接用 diff 或版本管理工具对比两个目录改动一览无遗。# 对比 crowbar 两个版本的全部差异 diff -ruN crowbar_book_0_1 crowbar_book_0_2 v01-v02.diff # 只看某个文件的变化 diff -u crowbar_book_0_1/execute.c crowbar_book_0_2/execute.c我第一次拿到这套资料时就是逐版本 diff 读的。0.1 到 0.2 的 diff 里改动最密集的是内存管理和对象创建的相关函数新增了一个 GC 模块execute.c 里对数组和字符串的处理全部改成了走 GC 分配。0.2 到 0.3 的 diff 里能看到错误处理机制重做了error_message.c 里新增了一堆错误码定义说明作者开始把运行时错误当成一等公民对待。0.3 到 0.4 的改动集中在语法解析部分新增了某类语法结构y.tab.c 的变化非常大。用 diff 学习的另一个好处是能直接看出作者的编码习惯。比如看到某个函数在 0.2 里是 30 行0.3 里变成了 80 行拆分出来两个辅助函数就能理解作者是怎么控制复杂度的。这种经验是看任何单版本源码都学不到的。从那以后我拿到任何迭代版源码项目时都会强制自己先跑一遍diff -ruN生成变更清单再决定细读哪些文件而不是从头开始盲读。版本演进本身就是最诚实的教学材料你看到的每一个函数变化都是一次真实的编码决策。希望这套资料能帮你在自制语言的路上少走几个弯。本文还有配套的精品资源点击获取