资讯动态

从calc到Diksam:自制编程语言词法语法分析与解释器实现

发布时间:2026/10/17 13:48:15 来源:尧图企业网站定制
简介这是一份围绕《自制编程语言》整理的中文参考资料面向对编译器与解释器原理感兴趣的开发者帮助读者从零理解一门编程语言从语法设计、词法语法分析到运行时实现的全过程。包内为1个PDF文档压缩包大小约2.38MB内容覆盖编程语言设计原则、yacc/lex与bison/flex等经典编译器生成工具、MinGW与Cygwin等环境搭建并以Crowbar和Diksam两个自制语言作为贯穿性实例按版本演进逐步展示计算器、语法分析、虚拟机到完整解释器的实现差异同时兼顾Linux与Windows两种平台下的构建方法。该资料目前已有870人学习下载。读者可据此快速建立自制语言的整体知识脉络对照书籍逐章理解crowbar、Diksam各阶段源码与工具用法在动手实践时也能作为一份实用的设计实现速查手册。1. 一套把词法分析、语法分析写到能跑的自制编程语言资料从 calc 到 Diksam想亲手写一门编程语言最常见的卡点不是算法看不懂而是看完书不知道从哪行代码开始。这份资料把「自制编程语言」这条路上最硬核的部分一次性补齐了它包含从四则运算计算器 calc、yacc/lex 驱动的 mycalc、手写递归下降解析器 llparser到两个完整解释器 crowbar 与 Diksam 的 0.10.4 全版本演进源码。crowbar 是带垃圾回收的脚本语言Diksam 是带类型推断和继承的静态语言两个项目都从零实现了词法分析、语法分析、AST 构建、执行器与原生函数库还给了 Linux 和 Windows 两套构建脚本。适合想入门编译原理的学生、要做 DSL 的工程师以及想通过读真实解释器源码理解 GC 和虚拟机的人。它不是让你背概念而是让你跟着版本号从 0.1 改到 0.4把一门语言从「能解析表达式」一路推到「能跑脚本」。2. 资料全貌8 个递进工程与两个真实语言的演进路线2.1 源码包怎么排的版本号就是学习进度条拿到这份资料先别急着编译它的目录编排本身就是一份教学设计。先是calc一个最简单的表达式求值器接着是mycalc用 yacc/lex 写的计算器版本mycalc_ex在它上面做了扩展然后llparser与llparser_ex是手写递归下降解析器的两个阶段。真正的主菜从crowbar_book_0_1开始一路到crowbar_book_0_4每升一个版本就加一块核心能力0.1 是最简执行器0.2 引入字符串和数组0.3 开始有垃圾回收的基本形态0.4 补上正则表达式库依赖。diksam_book_0_1到diksam_book_0_4则是另一条线从 0.1 的编译器骨架到 0.4 能处理继承与类型推断工程目录里还区分了compiler与main两个子模块说明它已经按真实编译器的分层在组织代码。这种「同一门语言、四个版本递进」的安排比直接丢一个完整编译器高明得多。你可以在 0.1 和 0.2 之间做 diff清楚看到加一个数据类型需要动哪些文件在 0.3 和 0.4 之间做 diff能看到引入 GC 时执行器做了哪些调整。我拆这套资料时就是把两个版本的 .c 文件并排 diff 着读的改动的文件列表比任何架构图都直观。2.2 技术栈选型yacc/lex 和 bison/flex 的取舍先看生成器的选择。mycalc用的是 yacc/lex这是经典教材组合但到了crowbar_book_0_2之后Makefile 里的命令变成了bison --yacc -dv crowbar.y说明作者在 GNU 工具链上做了替换。bison 的--yacc选项让它在输出文件命名上兼容 yacc 的习惯生成y.tab.c和y.tab.h这样 lex 文件里#include y.tab.h的写法两边通用。flex 对应 lex生成的lex.yy.c直接参与编译。这个选型带来的一个隐藏好处是bison/flex 的产物是可读的 C 代码你可以把生成器输出的y.tab.c打开看状态转移表也可以把lex.yy.c里的 DFA 表打印出来调试。如果用手写解析器出了问题只能自己扛用 yacc/lex 这类工具语法规则文件.y本身就是文档。坏处也明显生成代码里大量的 goto 和宏展开报错信息极其劝退比如后面避坑章节会提到的m4: cannot open Files这种路径被拆断的报错不熟悉 bison 内部机制的人根本无从下手。2.3 构建脚本与跨平台约束make.sh 与 make.bat资料的构建体系刻意做了双平台设计。Linux 下每个工程目录里都有make.sh内容不外乎调用 make 再清理中间文件Windows 下对应的是make.bat。但真正决定你能不能跑起来的不是这两个脚本而是依赖链。crowbar 0.4 依赖 oniguruma一个正则表达式库资料锁定在 5.9.4 版本Windows 下编译它需要先过 configure 这一关而 configure 生成的 Makefile 又依赖sh与make——所以资料里专门写了用 Cygwin 的 bash 来执行 configure再回到 MinGW 环境里用mingw32-make编译。一个细节值得注意crowbar_book_0_1的 Makefile 里有一个cd ./memory; gmake.exe的步骤这是把内存管理模块单独拆成了一个子构建单元。也就是说crowbar 在 0.1 时就已经把memory抽象成了独立模块这对后续 GC 的引入是关键的架构铺垫。你在改这份代码时也应该遵守这个分层不要跨模块直接操作内存所有分配都走 memory 层。3. 核心实现拆解crowbar 里一个表达式从 token 到求值的完整链路3.1 词法分析状态机在 .l 文件里怎么切crowbar 的词法分析器写在crowbar.l里它的核心不只是识别关键字和标识符还要处理注释、字符串字面量、正则表达式字面量这些需要「进入子状态」的输入。看.l文件的片段就能理解它的结构%{ #include crowbar.h #include y.tab.h %} %start COMMENT STRING_LITERAL_STATE %% [ \t\n] { /* 忽略空白 */ } // { BEGIN COMMENT; } COMMENT[^\n]* { /* 直到行尾都是注释 */ } COMMENT\n { BEGIN INITIAL; } \ { BEGIN STRING_LITERAL_STATE; } STRING_LITERAL_STATE[^\\\] { /* 普通字符串字符 */ } STRING_LITERAL_STATE\\[\nrt] { /* 转义字符 */ } STRING_LITERAL_STATE\ { BEGIN INITIAL; } %%这里的%start COMMENT STRING_LITERAL_STATE声明了两个附加状态BEGIN COMMENT会让词法分析器切换到只匹配注释规则的状态直到遇到\n再回到INITIAL。字符串字面量的处理思路一样用\\[\nrt]这类规则处理转义。这个设计在讲解词法分析时是很好的范本你不用把注释识别规则写成一个巨大无比的正则状态切换能把复杂输入切成一小段一小段独立处理。需要留意的是.l文件里不能直接引用y.tab.h里还没声明的 token——这是新手最容易踩的坑。bison 的-d选项会生成y.tab.hflex 生成的lex.yy.c通过#include y.tab.h拿到 token 编号所以构建顺序必须是先跑 bison 再跑 flex否则lex.yy.c里全是未定义的 token 常量。3.2 语法分析与 AST.y 产生式到 create.c 的节点crowbar 的语法规则集中在crowbar.y。以赋值表达式为例它的产生式会返回一个 AST 节点节点的类型由create.c里的构造函数决定/* crowbar.y 中的片段 */ expression : expression ASSIGN expression { $$ create_assign_node($1, $3); } | expression ADD expression { $$ create_binary_expression_node(ADD_EXPRESSION, $1, $3); } ;create_assign_node在create.c里做的是节点分配和类型标记/* create.c */ AST *create_assign_node(AST *left, AST *right) { AST *node alloc_ast(); node-type ASSIGN_EXPRESSION; node-u.assign.left left; node-u.assign.right right; return node; }这里能看到 AST 节点用了一个带union的结构体不同类型的表达式共用同一块内存区域type字段决定解释器按哪个成员去读。这样做的好处是省内存、访问快坏处是类型不安全写解释器时容易读错 union 成员。它用alloc_ast()而不是直接malloc说明内存分配统一走 memory 模块这对后面 crowbar 引入 GC 是必要的——所有 AST 节点都要能被 GC 追踪到。3.3 执行器execute.c 与 eval.c 的职责边界crowbar 的执行器拆成了execute.c和eval.c两个文件。execute.c负责语句级别的控制流比如 if、while、函数定义这些不产生值的结构eval.c负责表达式求值。这个拆分在解释器里很常见职责边界清晰execute拿到一个语句节点后内部可能会反复调用eval求条件表达式的值但eval永远不会反过来调用execute。/* execute.c 中 while 语句的处理 */ static void execute_while_statement(Global *interpreter, Statement *stmt) { for (;;) { Value v eval_expression(interpreter, stmt-u.while_s.condition); if (!v.boolean_value) { break; } execute_statement(interpreter, stmt-u.while_s.body); } }这段代码说明了一个解释器实现中的关键问题eval_expression返回的是一个Value结构体而不是 C 语言的原生类型。Value里有一个boolean_value成员但布尔值之外的类型整数、字符串、函数指针都存在同一个联合体里。所以写执行器时每次取Value都要先看类型标记否则一个整数被当成函数指针调用崩溃得毫无征兆。3.4 运行支撑字符串池、原生函数与错误消息crowbar 的工程文件里还有几个值得单独看的模块。string_pool.c做字符串驻留string interning所有字符串常量在运行时只保存一份通过哈希表按内容查找。这样比较两个字符串是否相等就退化成比较指针节省了大量strcmp。代价是string_pool里的字符串不能随便释放这是解释器早期不引入 GC 时的常见妥协。native.c是原生函数native function的注册表crowbar 脚本里能直接调用的print、fopen这类函数本质都是 C 函数通过一个结构体暴露给解释器。扩展 crowbar 的最简单路径就在这里在native.c里写一个符合签名的 C 函数把它挂到native_function_table里脚本里就能直接调用。error_message.c则是把所有出错信息集中管理而不是散落在各执行文件里这样多语言支持比如日文/英文切换只需要改这一个文件。4. 环境搭建与运行Linux、Windows MinGW、Cygwin 三条路实测4.1 Linux 最快路径把依赖装齐然后 make.sh在 Linux 下跑通这套资料是最省心的。前提是装好 bison、flex 和 make然后进入crowbar_book_0_1目录直接执行make.sh。如果你的发行版默认没有 yacc 的兼容层bison 的--yacc选项已经处理好命名兼容crowbar.l里#include y.tab.h能找到生成的头文件。这里唯一要留意的是 oniguruma 依赖只有 crowbar 0.4 用到可以先从官网拉 5.9.4 的 tar 包wget http://www.geocities.jp/kosako3/oniguruma/archive/onig-5.9.4.tar.gz tar -xvf onig-5.9.4.tar.gz cd onig-5.9.4 ./configure make sudo make install./configure会检查系统里是否具备alloca、memcmp等函数原型生成对应平台的config.h所以不要在 Windows 上编译完再把产物拷到 Linuxconfigure 这一步的检测结果不通用。装好后运行测试脚本./crowbar test/test.crb如果报错提示第 5 行附近有 0x0d 字符那就是文件换行符问题见避坑章节。4.2 Windows MinGWmingw32-make 的最小配置Windows 下资料推荐的是 MinGW 方案。先用官网的mingw-get-setup.exe装基础包在 MinGW Installation Manager 里勾选mingw32-base这样会顺带把mingw32-make装上。注意 MinGW 默认安装目录如果带空格后面 bison 很容易踩路径坑建议装到D:\MinGW这类干净路径。编译时不要直接敲make要用完整路径的mingw32-make.execd C:\win_sjis\crowbar_book_0_1 D:\MinGW\bin\mingw32-make.exe如果你的系统里装了 Cygwin可能会把make.exe和mingw32-make.exe搞混。Cygwin 的make生成的是依赖 cygwin.dll 的 exeMinGW 环境下运行会报找不到 DLL反过来 MinGW 的mingw32-make在 Cygwin 的 bash 里调用也可能因为路径翻译问题翻车。最稳妥的做法是MinGW 环境下只认mingw32-makeCygwin 环境下只认make两条路不要混用。4.3 Windows Cygwin买 cygwin.dll 这份依赖之前想清楚资料里对 Cygwin 的定位是「用来跑 configure 和编译 oniguruma」而不是直接替代 MinGW。Cygwin 的意义在于它提供了一整套 POSIX 模拟层configure这套为 Unix 设计的脚本在 Cygwin 下能顺利跑完。但如果你的目标产物是脱离 Cygwin 环境、在干净 Windows 上直接运行的解释器就得想清楚Cygwin 编出来的 exe 依赖cygwin.dll分发时得带上这个 DLL很多初学到这里就会卡住。所以我一般会推荐分工Cygwin 只负责执行./configure和make生成libonig.a最终链接进 crowbar 时切回 MinGW 的工具链。另一种省事的办法是直接用 GNU Win32 项目预编译好的 bison 和 flex 二进制把下载到的包解压后手动把bison.exe、flex.exe和对应的 DLL 放进一个无空格的目录然后把该目录加入 PATH。这样绕开了 Cygwin 一整条依赖链但要注意 GnuWin32 的 bison 版本比较老某些语法规则可能需要微调。4.4 跑通验证mycalc 交互台与 test.crb 脚本跑通之后怎么确认解释器行为正确两个方案。一是用mycalc这种交互式程序它从 stdin 读表达式实时求值适合快速验证词法/语法的改动。二是用各crowbar_book_0_X目录里的test/test.crb脚本它们是配套的回归测试用例。我拆项目时习惯先跑一遍 test.crb 拿到基线输出改完代码再跑一遍做 diff比肉眼盯输出可靠得多。如果你遇到gmake: crowbar is up to date.这种提示说明 Makefile 觉得产物已经是最新了但你可能改过.y或.l文件。此时手动删掉y.tab.c、y.tab.h和lex.yy.c再重新 make或者直接在命令行里touch这几个源文件逼 make 重新生成。5. 避坑记录这份资料里反复出现的 6 个经典报错5.1 bison 在“Program Files”路径下 m4 直接崩溃现象Windows 下运行bison --yacc -dv crowbar.y报错一堆m4: cannot open Files: No such file or directory紧接着跟着m4: cannot open (x86)\GnuWin32\Bison/share/bison/m4sugar/m4sugar.m4这类碎片化路径。原因GnuWin32 版的 bison 默认装在C:\Program Files (x86)\GnuWin32\Bison路径里的空格和括号让 bison 内部调用的 m4 在拼接参数时把路径拆断了Program和Files被当成两个独立参数。解决把 bison 装到无空格的目录比如D:\GnuWin32或者手动设置环境变量让 bison 指向对的 share 目录。要是非得装在 Program Files 下就写一个调用脚本在脚本里用引号把 bison 可执行文件的完整路径包起来。这是我在这个项目上见过的最高频报错几乎是每个 Windows 用户都会撞一次。5.2 y.tab.c 或 y.tab.h 不存在的幽灵错误现象运行 make 时gcc: y.tab.c: No such file or directory同时mycalc.l:3:19: error: y.tab.h: No such file or directory感觉文件像黑匣子一样凭空消失。原因bison 没有成功执行。常见原因是 bison 不在 PATH 里比如用 MinGW 时只装了 gcc 没装 bison还有一种情况是 Makefile 里依赖关系没写清楚make 并行执行时 gcc 比 bison 先跑导致 y.tab.c 还没生成就开始编译。解决先单独跑一遍bison --yacc -dv crowbar.y确认y.tab.c和y.tab.h生成了再 make。用 mingw32-make 时如果报process_begin: CreateProcess(NULL, bison ...) failed就是系统找不到 bison 可执行文件把 bison 所在目录加进 PATH 即可。5.3 Linux 下 test/test.crb 报 0x0d 非法字符现象在 Linux 下编译成功但运行./crowbar test/test.crb时报5: 错误(0x0d)指向文件第 5 行而肉眼看去那一行完全正常。原因test.crb 是在 Windows 下编辑的换行符是 CRLF0x0d 0x0acrowbar 的词法分析器不认\r这个字符在行尾把它当成了非法输入。解决用dos2unix test/test.crb转换换行符没有 dos2unix 就sed -i s/\r$// test/test.crb。更通用的做法是改词法分析器让它在初始状态忽略\r字符但既然是配套的测试脚本直接转文件格式更快。5.4 Linux 链接时找不到 libmsvcp60现象在 Linux 上编译 crowbar 0.4链接器报/usr/bin/ld: cannot find -lmsvcp60后面还跟着找不到libmsvcp60.so的提示。原因Makefile 里残留了 Windows 特有的链接参数-lmsvcp60是 MSVC 的运行库Linux 上不存在这个库。说明这套 Makefile 是从 Windows 环境移植过来或者没做条件分支。解决打开 Makefile找到LIBS -lmsvcp60 ...这一行把-lmsvcp60删掉。crowbar 0.4 的正则支持依赖的是 oniguruma链接参数应该是-lonig而不是 msvcp60。这种跨平台遗留问题在移植开源项目时很常见养成编译失败先看链接参数的习惯能省不少时间。5.5 oniguruma 的 make install 在 Windows 下翻车现象在 Cygwin 下执行make install安装 oniguruma报错D:\MinGW\bin\ranlib.exe: /usr/local/lib/libonig.a: No such file或者.deps/euc_jp.Plo:1: *** 缺少分隔符。原因install 步骤里的 libtool 脚本在 Cygwin 和 MinGW 两种环境的路径语义之间打架Cygwin 的/usr/local/lib与 MinGW 的D:\MinGW\lib互相不认.deps目录里的依赖文件用了 Windows 反斜杠路径make 解析不了。解决绕开make install手动拷贝编译产物。进入onig-5.9.4/.libs目录找到libonig.a拷到 MinGW 的lib目录下然后手动执行ranlib libonig.a更新索引。我一般还会顺手把onig-config脚本的路径改对否则后续 autoconf 项目找依赖时还是会对不上。5.6 Cygwin 下 .deps/euc_jp.Plo 的“缺少分隔符”现象Cygwin 的 bash 里执行make install报Makefile:415: recipe for target install-libLTLIBRARIES failed同时.deps/euc_jp.Plo:1: *** 缺少分隔符一行接一行。原因automake 生成的.deps/*.Plo文件里保存的是编译时的依赖路径Windows 下这些路径带反斜杠和盘符冒号Cygwin 的 make 把:后面的内容当成规则分隔符解析自然就报错。解决最干脆的办法是删除.deps目录再重新 make让它重新生成一份干净的依赖数据也可以直接绕过 install用上一节提到的手动拷贝方案。这个报错不影响编译本身只影响依赖追踪所以删.deps很安全别被这一堆红色输出吓住。6. 二次开发给 crowbar 加复合赋值运算符的完整改造路径接口设计做得好不好看加特性时改几个文件就知道。crowbar 四层分离词法在.l语法在.yAST 构造在create.c求值在execute.c/eval.c加一个运算符就是按这条链路依次改。以为例。先改crowbar.l在赋值相关 token 附近加一条规则把识别成新 token { return(PLUS_EQUAL_TOKEN); }然后是crowbar.y在赋值表达式的产生式里追加一条优先级直接复用赋值运算符即可。别忘记在%token声明里加上PLUS_EQUAL_TOKEN。接着在create.c里加一个节点构造函数把左右操作数挂进 AST。最后在eval.c的表达式求值 switch 里处理它先对左操作数求值并确认它是可赋值目标再对右操作数求值两者做加法后写回左操作数指向的位置。改造完了别直接跑完整脚本先用mycalc风格的表达式验证a 1; a 2; print a;预期输出 3。再跑一遍 test.crb 做回归防止改crowbar.y时动了别的产生式导致解析器行为偏移。我拆这个项目时养成的习惯是每改完一层就编译一次.l改完跑词法、.y改完跑语法、执行器改完跑冒烟脚本。编译器项目最怕一次性改完三四个文件再编译报错定位成本会翻好几倍。希望这套资料里踩过的坑能帮你把第一条解释器的路走得更顺。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑