Tiny-Lua-Compiler: 可能是有史以来最小的 Lua 编译器

Lobsters Hottest 工具

摘要

Tiny-Lua-Compiler 是一个用于教学的、自举的 Lua 5.1 编译器和虚拟机,完全用纯 Lua 编写。其设计目标是体积足够小以便于研究,同时又功能完备到足以处理真实的语言特性。

<p><a href="https://lobste.rs/s/np3vpy/tiny_lua_compiler_possibly_smallest_lua">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/05/10 22:56

bytexenon/Tiny-Lua-Compiler 来源:https://github.com/bytexenon/Tiny-Lua-Compiler Tiny Lua 编译器 (TLC) 一个集教育性质的 Lua 5.1 编译器、字节码发射器和虚拟机于一体的单文件 Lua 项目 灵感来源于 Jamie Kyle 的 The Super Tiny Compiler (https://github.com/jamiebuilds/the-super-tiny-compiler) 许可证:MIT Lua Tiny Lua Compiler (TLC) 是一个用纯 Lua 编写的完整 Lua 5.1 编译器。它对源代码进行词法分析,构建抽象语法树 (AST),将其降低为 Lua 5.1 函数原型,发射真实的 Lua 5.1 字节码,并可以在其自带的基于寄存器的虚拟机中执行这些原型。核心代码全部包含在 tlc.lua 中。大多数编译器学习材料可以分为两类。一类是玩具编译器,虽然易于完成,但跳过了使真实语言变得有趣的部分;另一类是生产级编译器,虽然真实可靠,但规模庞大,使得主要思想被架构和历史包袱所淹没。TLC 旨在介于两者之间。它足够小巧,你可以在一个周末内读完,但又足够真实,能够处理词法作用域、闭包、向上值 (upvalues)、可变参数、多返回值、方法调用、循环、尾调用、字节码编码和执行。它不是生产级编译器,也不打算取代标准的 Lua 实现。它是一个教育性质的编译器,力求诚实:小到足以理解,完整到值得研究。 ## 它可以编译自身 TLC 可以编译其自身的源代码,并在其自己的虚拟机中运行结果: lua local tlc = require("tlc") local tlc2 = tlc.run(io.open("tlc.lua"):read("*a")) tlc2.run("print('Hello from a compiler running inside itself')") 这意味着一个用 Lua 编写的编译器正在编译另一个用 Lua 编写的编译器,然后编译后的编译器又运行新的 Lua 代码,这一切都在不离开宿主进程的情况下完成。 ## 试用方法 bash git clone https://github.com/bytexenon/Tiny-Lua-Compiler.git cd Tiny-Lua-Compiler # 在 TLC 自己的虚拟机中运行代码。 lua5.1 -e "require('tlc').run(\"print('Hello from TLC!')\")" # 编译为二进制 .luac 块并使用标准 Lua 虚拟机运行它。 lua5.1 -e "io.open('out.luac','wb'):write(require('tlc').compile('print(42)'))" lua5.1 out.luac lua5.1 tests/test.lua 你还可以将 TLC 用作库,根据需要选择任意详细程度: lua local tlc = require("tlc") -- 单行命令:编译并运行。 tlc.run("print('Hello from TLC!')") -- 编译为标准 Lua 虚拟机可以加载的二进制 .luac 块。 local bytecode = tlc.compile("return 21 * 2") -- io.open("out.luac", "wb"):write(bytecode) -- 如果需要,保存到磁盘。 -- 逐步遍历流水线。 local tokens = tlc.tokenize("local x = 1 + 2; return x") local ast = tlc.parseTokens(tokens) local proto = tlc.generate(ast) local value = tlc.execute(proto) print(value) -- 3 ## 为什么这个文件值得一读 代码是线性执行的。首先是工具函数,然后是词法分析器、解析器、代码生成器、字节码发射器、虚拟机以及公共 API——按此顺序排列,井然有序。你可以追踪单个源程序通过每个阶段的过程而不会迷失方向。实现中还保留了许多玩具编译器通常会跳过的细节。字符分类使用预计算的查找表。运算符匹配使用 Trie 树进行最长前缀匹配——无需手动实现前瞻。表达式通过优先级爬升 (precedence climbing) 处理,而不是为每个级别编写语法规则。连接链被扁平化为单个 CONCAT 操作。浮点数通过手工方式打包为 IEEE 754 格式,而不使用 string.pack。向上值捕获和 OP_CLOSE 显式处理。这些并非锦上添花,而是真实编译器行为开始显现的地方。跳过它们,你只学到了编译的轮廓;保留它们,你学到了它实际如何工作。 ## TLC 涵盖的内容以及未涵盖的内容 TLC 涵盖了足够多的 Lua 5.1 特性,使其感觉真实: - 词法作用域、闭包、向上值捕获与关闭 - 数值和通用 forwhilerepeatdobreakreturn - if / elseif / else - 方法调用(: 语法)、表构造器 - 多返回值、可变参数 (...)、尾调用优化 - 长字符串、字符串转义、十六进制数、科学计数法 - 完整的 Lua 5.1 字节码发射——输出可在标准虚拟机中加载 它故意省略的内容同样重要。没有常量折叠。没有调试信息——即映射每条指令到源码行的表;没有它,错误消息不显示行号,但字节码本身是正确的。最大的遗漏是元方法分派。当 a 是表时写 a + b,标准 Lua 会检查 __add。TLC 的虚拟机完全跳过这一步——运算符仅对原生类型有效。这移除了一项真实特性,但避免了虚拟机演变成对象系统。这种权衡是刻意为之。TLC 试图成为一个你可以真正读完的真实编译器。 ## 正确性 测试套件使用 TLC 和标准 Lua 分别编译每个用例,然后并排比较结果。没有模拟期望——如果 TLC 产生不同的输出,测试失败。这捕捉到了教育性编译器通常能蒙混过关的错误:错误的运算符优先级、损坏的闭包语义、多返回值调整错误、循环控制流 bug,以及不正确的字面量解析等。 ## API lua local tlc = require("tlc") tlc.run(code, env?, ...?) tlc.compile(code) tlc.compileToProto(code) tlc.parse(code) tlc.tokenize(code) tlc.parseTokens(tokens) tlc.generate(ast) tlc.emit(proto) tlc.execute(proto, env?, ...?) docs/api.md 记录了公共 API,docs/ast.md 记录了 AST 结构。 ## 从哪里开始 阅读本文件了解整体概况,然后从头到尾阅读 tlc.lua。之后,docs/api.mddocs/ast.md 补充参考材料,tests 展示行为表面。TLC 运行于 Lua 5.1 至 5.5,尽管生成的字节码面向 Lua 5.1。欢迎贡献;见 CONTRIBUTING.md。如果你报告 bug,请包含输入代码、预期行为、实际行为和 Lua 版本。 ## 参见 - The Super Tiny Compiler (https://github.com/jamiebuilds/the-super-tiny-compiler) - 原始灵感;一个用 JavaScript 编写的约 200 行的编译器 - FiOne (https://github.com/Rerumu/FiOne) - 一个 Lua-in-Lua 虚拟机,比 TLC 更完整,但更注重可读性 - Lua 5.1 源码 (https://www.lua.org/source/5.1/) - 参考实现;llex.clparser.clvm.c 是最相关的文件 ## 许可证 MIT。见 LICENSE

相似文章