资讯详情

资讯详情

Crowbar/Diksam解释器源码拆解:词法语法、GC与构建排错

简介围绕自制编程语言主题的PDF资料面向希望从语法设计、编译器实现到运行环境完整走通一遍的开发者与高年级学生尤其适合已具备C语言基础、想理解解释器与编译器构造流程的读者。压缩包内共1个PDF文件约2.38MB以图文讲义形式集中呈现知识点便于在单一文档中连续阅读与检索。内容围绕Crowbar、Diksam两款教学语言展开覆盖yacc/lex与bison/flex生成词法与语法分析器、MinGW与GCC工具链配置以及Linux和Windows下构建脚本的差异并涉及垃圾回收、虚拟机与跨平台编译等实现细节。已有870人学习可作为课程设计、毕业项目或自学编译原理的参考资料帮助读者建立从语言设计原则、解释器与编译器实现到代码规范与可维护性评估的整体认知并对照示例理解自制语言从早期版本到后续版本的演进思路。1. 从一份 2013 年的压缩包说起Crowbar 与 Diksam 到底能带你走多远去年整理旧资料时翻到一个 2013 年的 ZIP解压出来是五个目录calc、mycalc、llparser、crowbar、diksam。前三个是面向编译原理课程的过渡练习后两个是一套自制编程语言的完整实现——从词法、语法到 AST 遍历执行、GC、正则字面量一层层往上搭。它的价值不在能跑出多强的语言而在于把一个编程语言从零到能执行脚本的每一步都留了可编译的版本快照Crowbar 从 0.1 到 0.4Diksam 从 0.1 到 0.4每个版本都能单独make出可执行文件。适合两类人想动手写解释器但卡在语法树怎么和执行器对上的以及想读懂 bison/flex 生成物到底怎么嵌进 C 工程的。下面按我实际拆包、编译、改代码的顺序走一遍。2. 先看地形五个顶层目录与两条语言演进线解压后别急着make先把目录扫一遍。这个包的目录命名很克制没有一坨多余的资源文件所有目录都是可以单独编译的小工程这点对复现特别友好——任何一个目录编译失败都不影响你继续看别的。2.1 顶层五个目录对应三种难度calc是最小可用集用来讲表达式求值这件事本身mycalc和mycalc_ex是同一份东西的两份前者带注释、可以跟着改后者是练手答案走的是 yacc/lex 路线llparser和llparser_ex同理只是把语法分析换成了 LL 手工递归下降。这五个目录可以当作热身代码量小编译一次就能跑非常适合在动 Crowbar 之前先把.l生成lex.yy.c、.y生成y.tab.c y.tab.h这套生成物关系摸熟。真正的主线是crowbar_book_0_1到crowbar_book_0_4四个目录以及diksam_book_0_1到diksam_book_0_4四个目录。八个目录摆在一起看你能直观看到同一个语言随着章节推进源码里多了哪些文件——这比读一本讲编译原理的书要直观得多因为你能diff出增量。2.2 Crowbar 的四个版本0.1 是骨架0.2 之后开始长肉crowbar_book_0_1是能跑通的最小解释器。从它的编译日志能反推出源文件构成main.c、interface.c、create.c、execute.c、eval.c、string.c、string_pool.c、util.c、native.c、error.c、error_message.c外加 bison/flex 生成的y.tab.c、lex.yy.c。这个文件划分本身就是一份架构图interface负责和外界的输入输出打交道create负责把语法树造出来execute负责遍历执行语句eval负责算表达式native放内置函数string_pool做字符串驻留。到crowbar_book_0_2引入了 GC。这是个关键分水岭0.1 的解释器对象分配完就不管了跑长脚本会一路涨内存0.2 之后多了memory.c这一层对象分配走统一的入口GC 才能扫描到所有活对象。我在 0.1 上跑过一个循环创建字符串的脚本内存曲线是直线上升的换成 0.2 之后再跑曲线就平了——这个对比实验值得自己动手做一遍比看十页讲标记清除的文字管用。crowbar_book_0_3和crowbar_book_0_4继续往上加特性。按这类书的常规推进节奏中间版本会补上数组、结构体、闭包、异常处理之类的东西具体每个版本加了什么最可靠的办法是直接diff -r crowbar_book_0_2 crowbar_book_0_3看create.c、execute.c里多了哪几个case分支比猜快得多。2.3 Diksam 的两个阶段先只有 compiler后来才有 maindiksam_book_0_1和diksam_book_0_2的目录结构里只有compiler编译也是在这个子目录里make/gmake到diksam_book_0_3、diksam_book_0_4才多出main目录。这个结构变化很说明问题早期的 Diksam 只做到把源码编译成中间表示还不能端到端执行后面有了main才是编译 执行的完整链路。如果你只想快速看效果建议直接从diksam_book_0_4入手如果你想理解一个编译器前端是怎么从零长出来的就从diksam_book_0_1往上爬因为compiler目录里的文件是逐个加进去的增量最干净。2.4 目录与关键文件对照目录主要内容构建方式适合谁calc最小表达式求值直接编译想看最朴素的递归下降mycalc/mycalc_exyacc/lex 版计算器make想搞懂.y.l生成物llparser/llparser_exLL 手工分析器make想对比手写与工具生成crowbar_book_0_1可执行解释器骨架make/gmake第一次跑通全链路crowbar_book_0_2加了 GCmake/gmake想看对象生命周期crowbar_book_0_3/0_4特性递增make/gmake想学增量演进diksam_book_0_1/0_2只有compilermake/gmake想看编译器前端diksam_book_0_3/0_4多了mainmake/gmake想跑完整编译执行提示包里还带了一份 oniguruma 5.9.4 的正则库源码Crowbar 后期的正则字面量依赖它。没有它带/.../字面量的脚本会直接链接失败。3. 构建环境MinGW、Cygwin 与 oniguruma 三条依赖线怎么搭这个包的构建环境是 Windows 下最容易翻车的一环因为它的脚本同时假设了三套东西MinGW 的 GCC、GNUWin32 的 bison/flex、以及 Cygwin 的 shell 工具。任何一套装歪报错信息都会指向一个和真实原因无关的地方。3.1 MinGW 装 base 就够但 make 得改名Windows 下装 MinGW跑mingw-get-setup.exe安装管理器里只需要勾mingw32-base这一项就够编译 C 了。装完记得把D:\MinGW\bin加进PATH。这里有个小坑MinGW 提供的 make 叫mingw32-make.exe但很多目录里的make.bat和 Makefile 直接调make。两种解决办法我一般选第二种:: 方式一每次手敲全名 D:\MinGW\bin\mingw32-make.exe :: 方式二复制一份改名为 make.exe之后直接 make copy D:\MinGW\bin\mingw32-make.exe D:\MinGW\bin\make.exe第二种的好处是make.bat不用改一行坏处是你以后在这台机器上跑的每个make实际都是 MinGW 的那个和 Cygwin 的make混用时要留个心眼——后面 oniguruma 那节就是这个问题的重灾区。3.2 bison/flex 的安装路径不能带空格bison 和 flex 用的是 Windows 版安装包装的时候默认目录通常是C:\Program Files (x86)\GnuWin32。路径里这个空格会在 bison 内部调用 m4 时炸掉典型报错长这样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原因不复杂bison 把--prefix路径拼成命令行传给 m4中间没加引号空格就被拆成了两个参数。解决办法只有一个把它装到没有空格的路径比如C:\GnuWin32然后把这个目录下的bin加进PATH。装完在 cmd 里验证bison --version flex --version能打出正常的版本信息这一步才算过。m4: cannot open ...这一类报错一旦出现后面y.tab.c、y.tab.h全部生不出来接着就是 gcc 报y.tab.c: No such file or directory看起来像编译问题其实根子在 bison 的路径上。3.3 oniguruma 5.9.4Linux 一条路Windows 另一条路Linux 下是最顺的标准三步tar -xvf onig-5.9.4.tar.gz cd onig-5.9.4 ./configure make sudo make install./configure会检查 alloca、memcmp、可变长参数这些平台特性全绿之后make出libonig.a和libonig.somake install装到/usr/local/lib。装完确认一下ls /usr/local/lib/libonig.* ls /usr/local/include/oniguruma.hWindows 下就麻烦得多。原因是configure是 shell 脚本cmd 跑不了得借 Cygwin 的 bash但 Cygwin 的make编出来的东西会依赖cygwin.dll和 MinGW 的 GCC 不是一个 ABI混着链就出事。我踩过最典型的一次是make install阶段libtool: install: ranlib .libs/libonig.a D:\MinGW\bin\ranlib.exe: /usr/local/lib/libonig.a: No such file Makefile:415: recipe for target install-libLTLIBRARIES failed原因说穿了很简单libtool 用的是 Cygwin 的路径视角/usr/local/lib在 Cygwin 里是C:\cygwin\usr\local\lib而ranlib是 MinGW 的它看的是 Windows 绝对路径同一个字符串在它眼里就是D:\usr\local\lib当然不存在。比较稳的做法是绕开make install手动收尾# 在 Cygwin 的 bash 里跑完 configure 和 make ./configure make # 不要 make install直接进 .libs 目录看产物 cd .libs ls libonig.a # 用 MinGW 的 ranlib 补一遍符号表再手动拷到 MinGW 的库目录 ranlib libonig.a cp libonig.a /cygdrive/d/MinGW/lib/ cp ../oniguruma.h /cygdrive/d/MinGW/include/ranlib这一步不能省。有些环境下ar打完包没有生成符号索引链接时会报undefined reference to onig_new之类看着像函数没声明其实是索引没建。拷完之后编译时加-lonig就能链上。3.4 一次完整的构建验证以crowbar_book_0_1为例Linux 下和 Windows 下的流程略有差别但验证方式一致编译出可执行文件再拿自带的test/test.crb跑一遍。# Linux / macOS cd crowbar_book_0_1 make ls -l crowbar ./crowbar test/test.crb # WindowsMinGW cd D:\win_sjis\crowbar_book_0_1 gmake dir crowbar.exe crowbar.exe test\test.crb编译过程中你应该能看到一长串gcc -c -g ...逐文件编译的日志最后一步链接出crowbar。如果只看到两三条就报错大概率是 bison/flex 没生成y.tab.c、lex.yy.c如果编译全过但链接报cannot find -lmsvcp60那是源码里带了一条 Windows 专有的库依赖在 Linux 下把-lmsvcp60从 Makefile 的LDFLAGS里删掉就行。4. 核心链路拆解一个 .crb 文件从 crowbar.l 走到 execute.c这一章是整份资料里最值得反复看的部分。Crowbar 的价值就在于它把脚本变成执行结果这条路切成了清晰的三段词法/语法生成前端、create.c造 AST、execute.c遍历执行。三段之间靠结构体指针传没有中间文件调试时可以直接在execute.c里打断点看树。4.1 词法层crowbar.l 的起始条件与 token 产出.l文件负责把字符流切成 token。Crowbar 里比较有意思的是它要给注释和字符串字面量做特殊的起始条件处理因为这两种状态里绝大部分字符都不该按普通 token 规则走。%{ #include stdio.h #include crowbar.h %} %option noyywrap /* 排他起始条件进入后只有显式列出的规则生效 */ %x COMMENT %x STRING_LITERAL_STATE %% /* { BEGIN COMMENT; } COMMENT*/ { BEGIN INITIAL; } COMMENT. { /* 注释内容丢弃 */ } COMMENT\n { /* 换行也要吃掉否则行号会漂 */ } \ { BEGIN STRING_LITERAL_STATE; } STRING_LITERAL_STATE\ { BEGIN INITIAL; return STRING_TOKEN; } STRING_LITERAL_STATE\\n { /* 转义处理 */ } STRING_LITERAL_STATE. { /* 累积到字符串缓冲区 */ }这几条规则的核心是%x声明的排他起始条件一旦BEGIN COMMENT只有COMMENT前缀的规则会被匹配普通标识符规则全部失效。这比在一个巨型正则里做状态判断要干净得多。注意起始条件在 flex 里用%x排他或%s共享声明%start是 bison 的关键字写进.l文件 flex 不认。我见过有人在.l顶部写%start COMMENT STRING_LITERAL_STATE结果 flex 直接报语法错改回%x立刻就好。4.2 语法层crowbar.y 与 y.tab.h 的生成关系.y文件定义文法规则和对应的语义动作。bison 处理的顺序是先分析文法、检查冲突再生成y.tab.c解析器主循环和y.tab.htoken 常量定义。这个顺序决定了你必须先跑 bison 再跑 gcc否则.l里#include y.tab.h会直接失败。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: y.tab.o lex.yy.o $(OBJS) $(CC) -o $ $^ -lonig -lm y.tab.c y.tab.h: crowbar.y bison --yacc -dv crowbar.y lex.yy.c: crowbar.l y.tab.h flex crowbar.l--yacc让 bison 用yacc兼容模式产物统一叫y.tab.c/y.tab.h-dv是同时生成y.output和y.tab.h其中y.output记录了完整的 LR 状态机调冲突的时候就是靠它——你能看到 bison 到底在哪个状态上既想移进又想归约。$(CC) -o $ $^里的$是目标名crowbar$^是所有依赖文件列表这是 Makefile 的自动变量写法比手敲一长串.o可靠。4.3 中间表示create.c 造树execute.c 遍历create.c的职责是把语法动作里收集到的 token 组装成节点。这类实现通常会有一个统一的分配入口然后按语句类型和表达式类型各写一个创建函数。/* 按节点类型分配统一走 memory 层方便 GC 扫描 */ Expression *crb_create_binary_expression(ExpressionType type, Expression *left, Expression *right) { Expression *expr crb_malloc(sizeof(Expression)); expr-type type; expr-u.binary_expression.left left; expr-u.binary_expression.right right; return expr; } /* 语句节点表达式语句、if、while、return 各一个分支 */ Statement *crb_create_expression_statement(Expression *expr) { Statement *stmt crb_malloc(sizeof(Statement)); stmt-type EXPRESSION_STATEMENT; stmt-u.expression_s expr; return stmt; }两个要点。第一节点用type字段做标签u是联合体不同类型节点共享同一块内存布局——这是 C 里实现多态的经典手法省内存也省类型转换。第二分配统一走crb_malloc而不是裸malloc因为 0.2 之后的 GC 需要在分配时把对象挂到可达图里绕过这个入口分配的对象 GC 是扫不到的这就是用了 GC 还漏内存最常见的原因。execute.c则是一个大switch按statement-type分发void crb_execute_statement(Statement *stmt) { switch (stmt-type) { case EXPRESSION_STATEMENT: crb_eval_expression(stmt-u.expression_s); break; case IF_STATEMENT: /* 条件求值结果按布尔语义转换再选分支 */ if (crb_eval_expression_to_boolean(stmt-u.if_s.condition)) crb_execute_statement(stmt-u.if_s.then_block); else crb_execute_statement(stmt-u.if_s.else_block); break; case WHILE_STATEMENT: while (crb_eval_expression_to_boolean(stmt-u.while_s.condition)) crb_execute_statement(stmt-u.while_s.block); break; default: crb_error(unknown statement type); } }这种树遍历解释器实现起来最快缺点是每次执行都要走一遍指针跳转比字节码慢但对学习来说完全够用而且调试极其直观——你在crb_execute_statement上打断点就能看着执行流一步步走完整棵树。4.4 运行时eval.c、native.c 与 string_pool.ceval.c负责表达式的值计算返回的是一个带类型标签的值对象。这里的坑在于类型提升整数和浮点混算时得先统一类型字符串拼接则要先判类型再返回错误否则运行到一半直接段错误。native.c是内置函数注册表通常维护一张函数名 → 函数指针 参数个数的表词法分析时把标识符解析成IDENTIFIER执行时先在变量表里找找不到再查这张内置表。string_pool.c是字符串驻留。所有字符串字面量在创建时先查池子有就复用没有才新建。好处是字符串比较可以退化成指针比较坏处是 GC 时池子本身要当作根节点处理。改动这块的时候池子的生命周期和 GC 的扫描顺序必须一起改只改一边必出悬垂指针。4.5 内存memory.c 与 0.2 之后的 GC 分水岭crowbar_book_0_2起多出来的memory.c是整套实现里最值得细读的文件。常见的做法是维护一个所有已分配对象的链表GC 触发时从根集合全局变量、当前调用栈上的局部变量出发做标记再清扫未标记对象。C 里没有对象图的反射能力所以每个需要被 GC 管理的类型都得自己实现一个mark函数遍历自己内部的指针字段。验证 GC 有没有真的生效可以写一个循环创建字符串的脚本跑一分钟看内存曲线或者在crb_malloc里加一个分配计数器再在 GC 前后打印一次存活对象数。这两个办法我在 0.1 和 0.2 之间对比过差距非常明显。5. 排查手记五类构建与运行报错的现象、原因和解决这份资料的报错类型高度集中基本就五类。我把每条按现象 → 原因 → 解决记下来遇到时按顺序对号入座就能省下大半时间。5.1gmake: No targets specified and no makefile found现象在crowbar_book_0_1目录下敲gmake直接报找不到目标文件。原因九成是当前目录不对。Makefile 在crowbar_book_0_1里但你的 cmd 还停在上一级。Windows 下用cd切换后如果路径带空格又没加引号切换本身就会失败人还以为是切过去了。解决切完目录先dir看一眼有没有Makefile没有就是没切对。路径带空格时统一加引号cd D:\win_sjis\crowbar_book_0_1。5.2m4: cannot open Files: No such file or directory现象跑gmake后bison 那一行直接失败后面跟着一串m4: cannot open ...。原因bison 安装在C:\Program Files (x86)\GnuWin32这类带空格的目录bison 拼 m4 命令行时没加引号路径被空格切断。解决卸载重装到C:\GnuWin32然后把C:\GnuWin32\bin提前到PATH最前面。装完bison --version能正常输出才算修好。这条修完y.tab.c、y.tab.h的缺失问题通常一起消失。5.3gcc: y.tab.c: No such file or directory/mycalc.l:3:19: error: y.tab.h: No such file现象编译阶段报源文件不存在文件名是y.tab.c或y.tab.h。原因这两个文件是 bison/flex 的生成物不是仓库里自带的。要么 bison 那一步失败了见上一条要么 Makefile 里缺依赖导致 gcc 先于 bison 跑。解决手动按顺序跑一遍确认每一步的产物bison --yacc -dv crowbar.y ls y.tab.c y.tab.h flex crowbar.l ls lex.yy.c哪一步没产物就修哪一步。手工跑通了再回头看 Makefile 的依赖行通常是lex.yy.c没写成依赖y.tab.h。5.4 Linux 下运行脚本报非法字符(0x0d)现象./crowbar test/test.crb跑到第 5 行就报非法字符0x0d。原因test.crb是在 Windows 下编辑保存的行尾是 CRLF。Linux 版的词法规则只认\n\r就变成不认识的字符了。解决转一下行尾或者干脆在.l的空白规则里同时吃\rdos2unix test/test.crb # 或者批量处理 find . -name *.crb -exec dos2unix {} \;改词法规则是治本但会影响所有版本的一致性我的习惯是只转换测试文件源码不动这样和书的描述保持一致。5.5ranlib: /usr/local/lib/libonig.a: No such file现象oniguruma 在 Windows 下make install阶段失败报找不到libonig.a。原因Cygwin 的 libtool 传递的是 POSIX 路径MinGW 的ranlib按 Windows 绝对路径解析两边对不上。解决跳过make install进.libs目录手动ranlib libonig.a再把.a和头文件分别拷到 MinGW 的lib、include下。另外diksam_book_0_4在 Linux 下如果报cannot find -lmsvcp60把 Makefile 里这个库删掉即可它是 Windows 平台的遗留依赖。6. 二次开发给 Crowbar 加一个语法结构和内置函数改这套代码最有效的入口不是加复杂特性而是走一遍从词法到执行的完整改动链。我拿加一个print_line内置函数和一个复合赋值运算符练手因为这两处几乎覆盖了所有需要改动的文件。改动点对照如下按这个顺序改改完一次编译通过的概率最高改动点文件具体动作新 tokencrowbar.y在%token段加ADD_ASSIGN词法规则crowbar.l加 { return ADD_ASSIGN; }同长度规则放前面语义动作crowbar.y加assignment_expression: ... ADD_ASSIGN ...的产生式节点创建create.c加crb_create_assignment_expression复用已有二元表达式结构执行分支execute.c、eval.c在EXPRESSION_TYPE分发里加一个case内置函数native.c在注册表里加一行{print_line, NATIVE_PRINT_LINE, 1}改.l的时候有个顺序坑flex 是按规则书写顺序匹配的和都能匹配前缀的话短的写前面就把长的挡了。所以这类多字符运算符必须写在之前。改完先单独跑flex crowbar.l确认没报 rule cannot be matched 再编译。.y里加产生式后必须重新跑 bison然后看y.output里有没有新增冲突。加这种产生式一般不会引入移进-归约冲突但如果你的新规则和已有的赋值规则共享前缀冲突告警一定要处理别用%expect硬压下去——压下去的结果是运行时解析行为变得不可预测这种玄学问题最难查。验证方式是分层跑测试别一上来就写复杂的脚本# 第一层语法能过 ./crowbar -p test/assign.crb # 只解析不执行看语法树 # 第二层语义正确 ./crowbar test/assign.crb # 期望输出结果 # 第三层回归 ./crowbar test/test.crb # 原来能跑的还得能跑如果项目里的可执行文件没带只解析的开关那就在crb_execute_statement前面加一个打印 AST 的调试函数自己给自己造一个。第三步的回归尤其重要新增 token 会影响整个解析表的状态编号原来跑通的脚本一定要重跑一遍。我从这份资料里留下的最深一个习惯是只要动了.y或.l就先删掉y.tab.c、y.tab.h、lex.yy.c再重新生成哪怕 Makefile 里的时间戳依赖看起来是对的。增量构建在这两个生成物上翻车太多次了多花五秒钟全量重建比对着一个诡异报错排查半小时划算得多。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →