JSSE:由代理构建的 JavaScript 引擎

0 阅读18分钟

原文:p.ocmatos.com/blog/jsse-a…

一个在 JavaScript 中做梦的代理人

一月份的时候,我偶然看到一篇博文,作者是one-agent-one-browser的开发者。这是一个完全用 Rust 语言编写的浏览器,由一个人指导一个编码代理,在几天内完成。不过,它不支持 JavaScript。我读完之后心想:“用同样的方法构建一个 JavaScript 引擎能有多难呢?”

六周后,JSSE(JavaScript Simple Engine)成为我所知的第一个100%通过test262非预发布环境测试的JavaScript引擎:涵盖所有98,426个场景,包括[此处应填写具体框架名称]、[此处应填写具体框架名称]、[此处应填写具体框架名称language/]和[此处应填写具体框架名称]。它不是V8,不是SpiderMonkey,也不是JavaScriptCore。这是一个完全用Rust从零开始构建的引擎,由Claude Code以YOLO(You Only Live Once,你只活一次)的方式开发。built-ins/``annexB/``intl402/

我一行 Rust 代码都没写。真的,一行都没有。在我看来,这个仓库只是一个只写数据存储区。我甚至都没手动创建 GitHub 仓库;也是代理自动创建的。

它的起源

一切始于聊天室里关于代理程序编码和浏览器的对话。1月27日下午2点42分,我提出了一个想法:“好,我们是不是应该从零开始开发一个JS引擎,然后把它集成到浏览器里?”17分钟后,我找到了单页规范,并从中获得了关键信息:“最棒的是我们有test262测试用例,所以我们可以创建一个反馈循环,从而创建一个能够通过所有测试的JS引擎。”

下午 3 点 05 分,代码库上线了。我搭建了初始框架(CLAUDE.md,,PLAN.md技能),并在下午 3 点 59 分启动了一个Ralph Loop,并发出大量提示,使其能够自主地逐个任务实现引擎,运行 test262,审查代码并提交。

16:02,第一次提交成功。我去开会了。一个小时后,17:09,JavaScript 代码已经开始执行了:

target/release/jsse -e 'function Foo(x) { this.x = x; } var f = new Foo(42); console.log(f.x);'
42

当晚19:10,test262测试通过率达到了17.63%。我结束了当晚的工作。从零开始,用Rust语言写出了一个能正常运行的JS引擎,大概用了四个小时。

数字

指标价值
开始日期2026年1月27日
100% 非分期测试2622026年3月9日(42天)
总提交次数592
完全锈蚀约17万行
新增线路(所有时间)929,475
已移除线路(所有时间)448,317
我实际操作键盘的时间总共约4小时

最后一个数字最令人惊讶。总共四个小时,分散在六周内完成:有时是30秒,有时是2分钟。其余时间都是智能体自主运行的。

它是如何建造的

配置刻意保持简洁。Claude Code 以 YOLO 模式运行(自动接受所有工具的使用)。没有进行任何复杂的编排。我使用的插件有:

  • **/simplify**可能只用过一两次,当文件太大需要重构时才会用到。
  • **ralph-wiggum-loop**内置的自主循环插件。我一开始用的是这个,但发现过一段时间后它会丢失一些设置(下文会详细说明)。
  • **chiefloop.com**是一个用于夜间运行 Claude Code 的外部工具。它适用于长时间无人值守运行,但缺少一些我常用工作流程中的功能。

项目的核心是PLAN.mdClaude 根据 ECMAScript 规范和 test262 子模块自行生成的任务列表。我通常的提示是:“阅读PLAN.md中第一个未完成的条目,制定计划并实现它,将其标记为已完成,提交并推送。” 仅此而已。我从不审查代码,从不审查计划,也从不查看差异。PLAN.mdCLAUDE.md的规则是:不允许出现回归,并且始终尝试通过比之前更多的测试。有时,当 Claude 试图跳过它认为太难的任务时,我会提出异议:“你为什么要跳过这个?我们必须实现所有内容,所以最好现在就解决它。” 但我的指导仅限于此。

最长的无人值守循环成功运行了 16 小时,用于实现 Temporal API。我在睡前启动了它,回来后发现它已经完整实现了InstantIANA时区支持(通过 ICU4X ZonedDateTime)和12个日历系统,并完成了 4482 项测试,其中 262 项测试通过。Temporal 是 ECMAScript 近期历史上规模最大、最复杂的提案之一,而代理仅用一个通宵会话就完成了它。PlainDateTime``PlainDate``PlainTime``Duration``Temporal.Now

实际的提示是什么样的

上一节抽象地描述了工作流程:计划、实施、测试、提交。但实际的日常工作中,这个过程究竟是怎样的呢?以下是项目中的一些真实案例,它们本身就讲述了一个故事。

出现次数最多的提示语——可以说是整个项目的核心,出现了20多次——是这样的:

请查看PLAN.md及相关 plan/ 文件,并提交对 test262 通过率影响最大的下一个功能。提出三个可能的功能及其各自的影响,一旦我们了解了这些影响,就可以选择一个进行开发。

这就是核心流程。我并不负责选择具体工作内容;我要求代理人分析 test262 的覆盖率缺口,提出按影响程度排序的备选方案,然后我再从中选择一个(或者直接说“开始”)。我的大部分互动都停留在这个层面:战略层面,而非战术层面。

当事情出现偏差时,提示信息就转向了调试:

发生了什么事?你想做什么?为什么这个 bash 脚本运行了 40 分钟?

长时间运行的会话有时会在某个测试上停滞不前,或者陷入编译循环。解决方法通常是终止会话,然后以更小的范围重新启动。

随着项目的成熟,我对并行计算的要求也越来越高:

不如我们把这些任务分别分配给不同的代理团队,让他们各自负责规划和实施,每个代理在自己的工作树中工作,完成后再把所有工作合并起来?这样大概能得到 300 个新的流程?

我们先创建 3 个计划,然后我会安排 3 个代理人分别在不同的工作流程中执行每个计划。

Claude Code 支持工作树——多个代理可以同时工作的独立 Git 分支。最难的功能就是这样实现的:将工作拆分成独立的轨道,并行运行,然后合并。Temporal API 的实现大量使用了这种模式。

有些提示简直就是马拉松式工作的信号:

是的。开始并完成所有阶段,并在过程中做出承诺。

我们或许应该直接着手处理方案 A,不要回避。先制定一个处理数组空洞的计划,然后付诸实施。

最后一个问题与数组空洞有关——这是 JavaScript 最棘手的语义难题之一。例如[1, , 3],数组 a 在索引 1 处有一个空洞,它的行为undefinedb 数组在一些细微的、规范规定的方面有所不同。代理程序处理了这个问题,但这需要专门的会话。

游戏后期的提示(98%以上)最有趣,因为工作性质完全改变了。我们不再是实现功能,而是寻找单个测试失败的原因:

我们离完全合规只差一步之遥了。还缺什么呢?我们先找出第一个未达到100%达标的类别,然后解决它。

以下哪项措施能够使特定类别达到 100% 合规?

最后冲刺到100%:

我们有一份 @PLAN.md 文件和一系列 @plan/ 文件。我们的最终目标是 100% 符合 test262 标准。我们目前已接近目标,但仍需弥补一些不足。您的任务是制定一个计划,帮助我们实现 100% 符合标准——稳步前进,避免任何倒退。

所有这些案例的共同点是,我从未告诉过代理如何实现任何事情。我只是设定目标、选择优先级,偶尔排除一些障碍。代理负责具体的工程设计。

通过率曲线

JSSE Test262 通过率

图表比我描述得更清楚。以下几个里程碑值得一提:

日期速度发生了什么
1月27日26%项目启动:词法分析器、语法分析器、基本解释器
1月31日51%String.prototype 接线错误修复暴露了约 11000 个测试用例(一天内增加了 23%)
2月3日63%基于状态机方法的生成器
2月10日86%临时 API 第一阶段
2月13日88%全面扩展:新增 intl402 测试,测试数量几乎翻了一番(约 48k → 约 92k),通过率仍然上升
2月22日95%SharedArrayBuffer + Atomics(一天内新增 868 次传递)
3月5日99.6%大规模解析器早期错误批处理(274 个修复)
3月9日100%Array.fromAsync这是最终的解决方案

1 月 31 日的更新是我最喜欢的。仅仅修复了一个String.prototype连接方式上的小 bug,就让大约 11,000 个测试一夜之间恢复正常。这正是拥有完整的 test262 测试套件作为反馈信号的价值所在;你可以通过 bug 的影响范围来发现它们。

发动机对比

为了进行比较,我对 Node.js 和 Boa(另一个基于 Rust 的 JS 引擎)运行了相同的 test262 测试套件。相同的代码检出、相同的测试环境、相同的超时时间、相同的机器(128 核):

引擎版本场景经过失败速度
JSSE最新的101,234101,04419099.81%
浮标v0.2191,98683,2608,72690.51%
节点v25.8.091,98679,20111,98686.86%

一些重要的注意事项:Node 的失败案例主要由 Temporal 库导致(Node 25 中未包含该库,其 11,986 个失败案例中有 8,980 个与此相关)。由于测试适配器无法运行 ES 模块,因此 Node 的模块测试(799 个场景)被跳过。Boa 的主要缺陷在于解析器层面(赋值目标验证、类解构、正则表达式属性转义)。这些都是成熟的、生产级别的引擎,而此次比较所依据的指标并非它们专门优化的目标,因此请理性看待此次比较结果。

JSSE 的场景数量更高(101,234 对比 91,986),因为它包含了完整的预发布测试套件。对于非预发布测试,JSSE 的场景数量始终为 100%。

我还针对acorn测试套件运行了 JSSE,作为 test262 之外的实际健全性检查。所有 13,507 个 acorn 测试均通过。

关于分阶段测试的说明

我一度以为 JSSE 也通过了预发布测试。但run-test262.py实际上并没有,它根本没运行这些测试。当我最终显式运行预发布测试套件时,事情变得有趣起来。

非测试环境的测试套件维护良好,内部一致性强。测试环境是测试用例在正式发布前进行测试的地方,这一点也体现libm在测试套件本身。JSSE 目前通过了 2808 个测试场景中的 2762 个(98.36%)。剩余的失败案例分为三类:不稳定的超时、精度差距,以及最值得关注的,测试套件本身似乎存在的 bug。

例如,六个预发布测试使用了一个共享的框架函数,该函数依赖于eval声明的变量泄漏到封闭作用域,而严格模式明确禁止这种泄漏。这些测试在 JSSE 和 Node 上都失败了。它们需要noStrict在上游添加一个标志。另一个例子:两个预发布测试期望块级作用域function arguments()声明具有 AnnexB 提升行为,这与主测试套件中的测试直接冲突。主测试套件反映了最近规范的更改,但目前还没有主流引擎实现该更改。JSSE 遵循规范,因此主测试套件中的测试通过,而预发布测试失败。

当你的目标测试套件开始反击时,这是一个好兆头。

性能如何?

很糟糕。我知道这是故意的。完全没有进行任何优化。JSSE 是一个纯粹的树遍历解释器,没有字节码编译。以下是一些针对 Node (V8) 和 Boa 的微基准测试:

基准Node v18浮标 v0.21JSSE v0.1JSSE 与 Node
环形0.18秒2.02秒2.90秒16倍
纤维0.21秒2.53秒28.57秒136倍
细绳0.19秒0.62秒4.74秒25倍
大批0.17秒0.53秒119.45秒703x
目的0.35秒0.69秒0.85秒2.4倍
正则表达式0.16秒0.24秒5.62秒35倍
关闭0.18秒2.79秒15.48秒86倍
JSON0.20秒0.44秒0.25秒1.2倍

性能差距非常大:从 1.2 倍(JSON,JSSE 实际上优于 Boa)到 703 倍(数组操作)。fib基准测试采用递归斐波那契数列,这会大幅增加函数调用开销,而 136 倍的性能差距正是预期中每次调用都会重新遍历抽象语法树 (AST) 的树遍历器的性能表现。数组基准测试是最坏情况,很可能是数组原型实现中存在某种病态路径。

与 Boa 进行比较更公平,因为它们都是 Rust 解释器。JSSE 的速度大约比 Boa 慢 1.2 倍到 225 倍,具体倍数取决于基准测试。但在对象操作和 JSON 处理方面,JSSE 与 Boa 的性能相当。

与 Node.js 相关的速度变慢

test262 测试套件本身就揭示了瓶颈所在。所有 30 个最慢的测试都是正则表达式 Unicode 属性转义测试(\p{...}),每个测试耗时 64 到 84 秒。其中最慢的Script_Extensions_-_Sharada.js测试耗时 84 秒。这些测试编译并运行正则表达式,枚举数千个 Unicode 代码点,瓶颈在于对 Unicode 属性类进行字节级 WTF-8 正则表达式匹配。在总共 198,258 个测试场景中,有 900 个耗时超过 10 秒,1,062 个耗时超过 1 秒。其余测试都很快完成。耗时较长的测试几乎全部是正则表达式测试。

这样很好。正确性是唯一目标。性能提升显然是下一步要做的事情。

成本

我在这个项目中使用了 Claude Code Max 20x 订阅,所以无需按令牌付费。但ccusage它追踪了等效 API 的成本,这个数字很有意思:47 天内 302 次会话,总成本为4,618.94 美元。按模型细分如下:

模型成本(API等效)分享
克劳德作品 4.64,062.91 美元88%
克劳德十四行诗 4.6345.54美元7.5%
克劳德·海库 4.5124.83美元2.7%
克劳德·索内特 4.545.56美元1%
克劳德作品 4.540.11美元0.9%

Opus 4.6 承担了绝大部分繁重的工作。Haiku 则用于处理诸如子代理搜索之类的后台任务。总令牌数约为 89 亿,但积极的提示缓存机制有效控制了成本;仅缓存读取的令牌数就达到了 88 亿。

换个角度来看:一个 17 万行的 Rust 代码库,如果 100% 通过 test262 测试,其 API 等效成本为 4,619 美元。这大约是每行代码 0.03 美元,或者每达到 test262 合规性一个百分点,成本约为 47 美元。

我学到了什么

计划比代码更重要。Claude根据规范和 test262 子模块自行生成了整个计划PLAN.md。我没有编写它,也没有审查它,只是接受了它。而且它奏效了。但现在回想起来,计划的质量是项目速度的最关键因素。Claude 选择了一个合理的特性顺序,但它早期的一些架构决策(例如如何处理生成器、如何构建垃圾回收器、如何处理 WTF-8 字符串)导致了后期代价高昂的返工。如果我事先投入时间研究合适的架构并将其融入计划中,我很有信心这个项目只需一半的时间就能完成。经验教训不是“你必须自己编写计划”,而是“无论以何种方式制定,都要确保计划本身是好的”。

test262 是一个绝佳的反馈信号。 拥有一个全面且结构良好的测试套件,并让智能体能够自主运行,正是这类项目得以实现的关键。智能体无需深入理解 JavaScript 语义;它只需要让数值上升,同时保持数值不变即可。test262 正好提供了这样的信号。

代理程序容易在冗长的上下文中迷失方向。 这需要一些解释。Claude Code 会在接近上下文限制(本项目开发时为 20 万个 token)时自动压缩对话。压缩会概括较早的消息以释放空间,但不可避免地会丢失细节:哪些具体的测试失败了、尝试了什么方法、错误信息是什么等等。在一个长时间运行的任务中,经过几轮压缩后,Claude 的性能会明显下降。它会陷入循环,反复尝试相同的修复。它会通过一些测试,但会导致另一些测试出现回归,然后尝试修复这些回归,结果又会破坏其他东西。我的解决方法是停止会话,将任务拆分成更小的部分,然后重新开始。一个带有明确提示(“实现 SharedArrayBuffer”)的新会话比一个经历了多次压缩的长时间循环要好得多。最佳方案是:每个会话处理一个功能,规划它,实现它,运行测试,然后停止。

架构重构成本高昂,但并非灾难性的。 克劳德曾意识到,原有的架构并不适合异步函数,不得不回头重新构建。虽然耗时比预期更长,但最终奏效了。如果架构师是一位真正理解问题所在的人,就能避免这种情况,这也与第一点不谋而合。

Rust 非常适合智能体编程。 我特意选择 Rust,是因为我相信它是目前智能体驱动开发的最佳语言。它的类型系统和编译器可以在运行时之前捕获大量 bug,这意味着智能体可以减少调试时间,将更多时间用于推进开发。严格编译器本质上是继 test262 之后的第二个反馈信号。

接下来会发生什么?

性能方面,引擎本质上是一个树遍历器,最容易实现的改进是字节码编译和简单的虚拟机。仅仅消除重复的抽象语法树遍历,性能提升可能就达到 10 到 100 倍。除此之外,还可以考虑内联缓存、隐藏类,如果想更进一步,最终还要实现 JIT 编译。

但说实话,JSSE 的初衷从来就不是为了构建一个生产级的 JavaScript 引擎。它的目的是探索现有代理编码工具的潜力,并在过程中了解代理的工作流程。就这两方面而言,我对结果都很满意。

结论

这些工具令人惊叹,模型也令人难以置信,但这仅仅是个开始。智能体编码已经开始改变人们的工作方式,我相信它将彻底革新软件生产(我曾就此写过一些想法)。在不久的将来,从零开始编写一个 JavaScript 引擎并将其优化到比其他任何引擎都快,简直易如反掌,真的就像在公园散步一样轻松。我从这次实验中学到了很多,并且会继续乐在其中。

代码位于github.com/pmatos/jsse,采用 MIT 许可证。如果您想自行运行 test262,README 文件中包含完整的复现步骤。欢迎为 JSSE 做出贡献,但请保持贡献的独立性。

© 2026 保罗·马托斯