ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

The Super Tiny Compiler 全解析:用约 200 行可读 JavaScript 吃透现代编译器的完整管线

The Super Tiny Compiler 全解析:用约 200 行可读 JavaScript 吃透现代编译器的完整管线 编译器【免费下载链接】the-super-tiny-compiler:snowman: Possibly the smallest compiler ever项目地址https://gitcode.com/gh_mirrors/th/the-super-tiny-compiler点击查看免费下载本篇指南以 README.md 为线索、以仓库核心教学文件 the-super-tiny-compiler.js 的逐段注释源码为骨架完整讲解一个现代编译器从词法分析、语法分析、遍历、AST 转换到代码生成的五大核心部件。读者学完后将能在阅读源码注释的基础上独立复述 Lisp 风格函数调用到 C 风格函数调用(add 2 2)→add(2, 2)的完整编译流程并通过 test.js 亲手验证每一阶段的中间产物。为什么每个开发者都应该认识编译器绝大多数开发者的日常工作中并不需要亲手编写编译器但编译器概念无处不在代码压缩、转译、模板引擎、正则表达式引擎、数据库查询优化器甚至我们常用的各类代码工具底层都借用了编译器的基本思想。README 的原话是compilers are all around you, tons of the tools you use are based on concepts borrowed from compilers编译器就在你身边你使用的众多工具都建立在从编译器借鉴而来的概念之上。编译器的可怕印象往往来自教材与工业级代码的复杂度而非其本质。本项目正是为了拆掉这层心理门槛而存在它用一个极端简化的例子把现代编译器的主要部件全部以易读的 JavaScript 呈现出来。按照源码注释的说法去掉全部注释后本文件实际可执行代码只有约 200 行见 the-super-tiny-compiler.js 头部注释 L79-L81任何人通读一遍都能理解大多数编译器是如何端到端工作的。需要说明本仓库是一个教学型最小实现其目标是解释编译原理而非构建工业级编译器README 与源码注释均未声称它具备完整语言的语法覆盖能力只选取了足以演示主要部件的最小语法子集。编译器的三大阶段与整体管线源码顶层注释the-super-tiny-compiler.js#L103-L115给出了大多数编译器共有的三个主阶段Parsing解析把原始代码转换成更抽象的代码表示。Transformation转换操作这份抽象表示把它改造成编译器需要的样子。Code Generation代码生成把转换后的表示重新输出为新的代码字符串。在本项目中这一管线由compiler函数串联为四条清晰的数据流the-super-tiny-compiler.js#L1028-L1036function compiler(input) { let tokens tokenizer(input); let ast parser(tokens); let newAst transformer(ast); let output codeGenerator(newAst); return output; }即input tokenizer tokens tokens parser ast ast transformer newAst newAst generator output下面按这条流水线逐段展开。目标语言一份极简的 Lisp 到 C 方言映射为了让流程具体可感本编译器选择将一类 Lisp 风格函数调用编译成 C 风格函数调用。假设我们只有add和subtract两个函数两种写法的对照如下源码注释 the-super-tiny-compiler.js#L88-L100数学表达式LISP 风格C 风格2 2(add 2 2)add(2, 2)4 - 2(subtract 4 2)subtract(4, 2)2 (4 - 2)(add 2 (subtract 4 2))add(2, subtract(4, 2))正如源码注释强调的这既不是完整的 LISP 也不是完整的 C 语法但足以演示现代编译器的大多数主要部件。全流程的规范输入输出示例即为(add 2 (subtract 4 2))→add(2, subtract(4, 2));这一点由 test.js#L10-L11 以测试常量形式固定下来。第一阶段解析Parsing之词法分析Parsing 通常被进一步拆成两个阶段the-super-tiny-compiler.js#L117-L139Lexical Analysis词法分析由 tokenizer词法分析器把原始代码按字符拆分为 tokens——一个描述语法孤立片段的微型对象数组可以是数字、标签、标点、运算符等。Syntactic Analysis语法分析把 tokens 重组为描述各语法片段及其相互关系的表示即中间表示IR/ 抽象语法树AST。以(add 2 (subtract 4 2))为例源码注释给出了 token 序列的形态the-super-tiny-compiler.js#L140-L156[ { type: paren, value: ( }, { type: name, value: add }, { type: number, value: 2 }, { type: paren, value: ( }, { type: name, value: subtract }, { type: number, value: 4 }, { type: number, value: 2 }, { type: paren, value: ) }, { type: paren, value: ) }, ]这一预期结果在 test.js#L13-L23 中被完整固化并由 test.js#L78 的assert.deepStrictEqual(tokenizer(input), tokens)校验。tokenizer 的实现要点tokenizer(input)the-super-tiny-compiler.js#L381-L538的骨架是用current变量作为游标追踪输入字符串位置用tokens数组收集结果在一个while (current input.length)循环内按字符分类处理开括号(/ 闭括号)直接产出type: paren的 tokenL404-L430它们稍后会被用于识别CallExpression。空白符用正则/\s/检测后直接跳过L439-L443。空白用于分隔字符本身不值得保留为 token。数字用正则/[0-9]/检测到数字后内层while循环持续吞并连续数字字符直到遇到非数字为止一次性产出完整的type: numbertokenL454-L474。这正是一个 token 可以是任意长度字符序列的体现。字符串检测到双引号后进入循环逐字符累积直到遇见下一个双引号产出type: stringtokenL483-L504例如(concat foo bar)中的foo、bar。名字函数名用正则/[a-z]/i匹配字母序列并累积产出type: nametokenL514-L529例如(add 2 4)中的add。兜底错误若上述规则全部未命中直接throw new TypeError(I dont know what this character is: char)L533保证非法输入快速失败。这段实现同时揭示了词法分析的通用原则字符按类型聚合、空白被丢弃、未知字符必须显式报错最终返回tokens数组L537。第一阶段解析Parsing之语法分析与 ASTparser(tokens)the-super-tiny-compiler.js#L555-L697把 token 数组转成 AST。与 tokenizer 的while循环不同parser 采用递归内部定义walk()函数按当前 token 类型分流numbertoken → 返回{ type: NumberLiteral, value }节点L571-L582。stringtoken → 返回{ type: StringLiteral, value }节点L586-L593。遇到开括号(L597-L600先跳过括号与紧随其后的函数名构造{ type: CallExpression, name, params: [] }基础节点L604-L616随后进入while循环不断调用walk()并把返回的节点 push 进params直到遇到闭括号)为止L652-L660。嵌套调用正是靠递归walk()自动推进游标来消解的——对于(add 2 (subtract 4 2))add的第二个参数会由一次深层walk()完整解析成嵌套的subtractCallExpression。最后跳过闭括号并返回节点L664-L667。未知 token 类型同样抛错L672。顶层则以{ type: Program, body: [] }为根L677-L680在一个while (current tokens.length)循环中不断ast.body.push(walk())L691-L693。这里用循环而非仅一次walk的原因在于一个程序可以由多个并列的表达式构成例如(add 2 2) (subtract 4 2)而不仅限于单一嵌套表达式。对于示例输入源码注释给出了完整的 AST 形态the-super-tiny-compiler.js#L158-L180它与 test.js#L25-L45 中固化的ast常量完全一致{ type: Program, body: [{ type: CallExpression, name: add, params: [{ type: NumberLiteral, value: 2, }, { type: CallExpression, name: subtract, params: [{ type: NumberLiteral, value: 4, }, { type: NumberLiteral, value: 2, }] }] }] }AST 是一个深层嵌套的对象它既便于程序操作又携带了大量关于代码结构的信息——这正是它成为编译器核心数据结构的原因。第二阶段转换Transformation与 AST 节点转换阶段处理的是 AST 节点AST Node。节点就是树中带type属性的对象每个节点描述树的一个孤立片段the-super-tiny-compiler.js#L194-L220。例如// NumberLiteral 节点 { type: NumberLiteral, value: 2, } // CallExpression 节点 { type: CallExpression, name: subtract, params: [...nested nodes go here...], }转换时可以对节点做增删改添加/移除/替换属性、新增或删除节点也可以保留原 AST 不动、基于它创建一份全新的 AST。因为本项目面向一门新语言C 风格所以转换策略是后者创建一份针对目标语言的全新 ASTL219-L220。遍历Traversal与访问者Visitor模式要对 AST 中所有节点做操作必须先能遍历它。遍历以深度优先顺序访问每个节点the-super-tiny-compiler.js#L222-L262对上面的 AST 访问顺序为Program树顶层CallExpression (add)Program.body 第一个元素NumberLiteral (2)add.params 第一个元素CallExpression (subtract)add.params 第二个元素NumberLiteral (4)subtract.params 第一个元素NumberLiteral (2)subtract.params 第二个元素代码中引入访问者visitor对象为不同的节点类型提供处理方法遍历到匹配类型的节点时即调用对应方法L267-L281var visitor { NumberLiteral() {}, CallExpression() {}, };为了让访问者真正有用还要把节点与父节点一起传入L284-L287同时支持在进入enter与退出exit两个时机触发因为树在深入时进入每个节点、回溯时退出每个节点。最终访问者形态为L316-L323var visitor { NumberLiteral: { enter(node, parent) {}, exit(node, parent) {}, } };traverser 的实现要点traverser(ast, visitor)the-super-tiny-compiler.js#L743-L807由两个内部函数协作完成深度优先遍历traverseArray(array, parent)对数组逐项调用traverseNodeL747-L751。traverseNode(node, parent)L755-L802取visitor[node.type]中的方法若存在enter则先以(node, parent)调用L759-L765按节点类型switch分流Program遍历node.bodyCallExpression遍历node.paramsNumberLiteral/StringLiteral无子节点直接break未知类型抛错L768-L795若存在exit方法则在递归返回后调用L797-L801。最后以traverseNode(ast, null)启动——顶层 Program 没有父节点L806。transformer 的实现要点transformer(ast)the-super-tiny-compiler.js#L858-L945把遍历与访问者结合起来产出全新 AST先创建newAst { type: Program, body: [] }并通过一个小技巧给旧 AST 根节点挂ast._context newAst.body作为从旧 AST 指向新 AST 的引用L862-L874。源码注释明确说明这只是为了教学简单化工业实现通常会有更好的抽象。访问者处理三类节点L877-L940NumberLiteral在enter时把{ type: NumberLiteral, value }push 进parent._contextL880-L890。StringLiteral同理 push 进parent._contextL893-L900。CallExpression构造{ type: CallExpression, callee: { type: Identifier, name }, arguments: [] }L903-L915并把node._context expression.arguments挂到旧节点上供参数累积L920当父节点不是CallExpression时用ExpressionStatement包裹——因为 JavaScript 中顶层CallExpression本质上是语句L924-L933最后 push 进parent._contextL937。新旧 AST 的对比在源码注释中以对照表形式给出the-super-tiny-compiler.js#L821-L855可概括为旧 AST 的CallExpression.nameparams[]变成新 AST 的CallExpression.callee(Identifier)arguments[]顶层调用被ExpressionStatement包裹。这份转换结果同样被 test.js#L47-L76 的newAst常量固化并在 test.js#L80 中由assert.deepStrictEqual(transformer(ast), newAst)校验。第三阶段代码生成Code Generation代码生成是编译器的最后阶段。源码注释the-super-tiny-compiler.js#L326-L342指出代码生成器以多种方式工作——有的复用早期的 token有的创建独立的线性表示而本项目与大多数编译器一样直接基于转换后的 AST 递归打印节点直到拼成一整串代码。codeGenerator(node)the-super-tiny-compiler.js#L961-L1009按节点类型switch输出Program对body逐项递归生成后用\n连接L968-L971。ExpressionStatement生成内层expression后追加分号;L974-L978。CallExpression输出callee( 各arguments递归生成结果以, 连接 )L984-L991。Identifier直接返回node.nameL994-L995。NumberLiteral直接返回node.valueL997-L999。StringLiteral给node.value加上双引号L1001-L1003。未知节点类型抛错L1006-L1007。把转换后的newAst送入该函数即得到add(2, subtract(4, 2));——注意末尾分号正是ExpressionStatement分支追加的。这与 test.js#L81 的断言一致。端到端验证运行测试仓库用 Node.js 内置的assert模块编写了完整的管道测试test.js。README 给出的运行命令只有一条node test.jstest.js#L78-L82 依次对四个阶段与整体编译做严格深比较assert.deepStrictEqual(tokenizer(input), tokens, Tokenizer should turn input string into tokens array); assert.deepStrictEqual(parser(tokens), ast, Parser should turn tokens array into ast); assert.deepStrictEqual(transformer(ast), newAst, Transformer should turn ast into a newAst); assert.deepStrictEqual(codeGenerator(newAst), output, Code Generator should turn newAst into output string); assert.deepStrictEqual(compiler(input), output, Compiler should turn input into output);在 Node 环境中执行后输出All Passed!即五个断言全部通过证明tokenizer → parser → transformer → codeGenerator → compiler的每一环都符合预期。开发者也可以自行node -e调用各函数观察任意阶段的中间产物tokens、AST、newAst、最终输出。如何把这个编译器当作模块使用package.json 声明了包的元信息名称the-super-tiny-compiler、版本1.0.0、入口main: ./the-super-tiny-compiler.js许可证为 CC-BY-4.0。源码文件末尾the-super-tiny-compiler.js#L1046-L1053通过 CommonJS 导出了全部六个 APImodule.exports { tokenizer, parser, traverser, transformer, codeGenerator, compiler, };因此可以直接按 test.js#L1-L7 的方式在业务代码中复用const { tokenizer, parser, transformer, codeGenerator, compiler, } require(./the-super-tiny-compiler); compiler((add 2 (subtract 4 2))); // add(2, subtract(4, 2));总结从最小实现中带走什么通读 the-super-tiny-compiler.js 的引导式注释后可以沉淀出几条可迁移到任何编译器 / 解释器项目的方法论管线即数据流tokenizer → parser → transformer → codeGenerator每一环的输入输出都是明确定义的纯数据结构字符串 → tokens → AST → 新 AST → 字符串这让每个阶段可以独立开发、独立测试——test.js 正是按阶段逐个断言的。AST 是通用枢纽语法分析统一产出带type的节点树转换阶段用 visitor 模式以 enter/exit 两个时机解耦遍历与操作这一模式在 Babel 等真实工具中被广泛使用。代码生成是 AST 的逆序列化递归打印节点、按类型决定语法外壳括号、逗号、分号、引号是最直观也最主流的做法。教学简化不损害原理完整性从源码结构可以推断本项目刻意用_context引用、单一 switch 等最简手法替代工业级的上下文管理与多趟优化但三大阶段、两类解析、visitor 遍历这些骨架与真实编译器同构。如果你觉得这篇引导仍不够清晰README 也欢迎读者反馈改进意见作为阅读者下一步的最佳动作是打开 the-super-tiny-compiler.js 从头到尾读一遍带注释的源码再运行node test.js亲手验证整个流水线。赞分享编译器【免费下载链接】the-super-tiny-compiler:snowman: Possibly the smallest compiler ever项目地址https://gitcode.com/gh_mirrors/th/the-super-tiny-compiler点击查看免费下载相关推荐终极指南如何通过The Super Tiny Compiler的200行代码轻松理解编译器工作原理终极指南如何通过The Super Tiny Compiler的200行代码轻松理解编译器工作原理 The Super Tiny Compiler是一个令人惊编译器如何用JavaScript实现WebAssembly编译输出The Super Tiny Compiler完整指南如何用JavaScript实现WebAssembly编译输出The Super Tiny Compiler完整指南 The Super Tiny Compil编译器零硬件跑通 Zephyr RTOSQEMU 下 5 条命令上手零硬件跑通 Zephyr RTOSQEMU 下 5 条命令上手 Zephyr RTOS 是开源轻量级实时操作系统基于 QEMU 模拟不用开发板就能编译并运操作系统嵌入式RTOS物联网上一篇终极入门教程activity-classifier文本分类模型快速上手指南下一篇锐龙处理器性能优化指南从诊断到实战的系统调优方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表