Whim 是什么?一门把 PHP 被否决的设想真跑起来的实验语言

可能已经有读者注意到,Whim 这门语言最近被公开发布了,它是作者在过去几个月里持续开发的作品。

不过在急着去找下载链接之前,先停一下。

下载链接其实就在这里,随时可以打开,但还是建议先读完这篇文章。

Whim 是一个实验性 playground。它公开了,所以其他人也可以拿来玩,但“公开”并不等于“有人支持”、不等于稳定、不等于安全、不等于合理,也不等于可以把它用在手头那件重要的事情上。

它不是 PHP 的替代品,不是另一个 PHP 实现,也不打算与 PHP 兼容。

它只是一个可以随手试验想法的地方:那些在写 PHP 时期望它具备的东西、那些目前只能活在 PHPDoc 里的东西、那些被否决或没写完的 RFC 里的东西、从其他语言抄来的东西,以及一旦真的有人用起来可能会发现完全是垃圾的东西。

还有一点最重要:Whim 不应该被拿来羞辱 PHP internals。

这一点后面会展开。

一切始于一本落灰的书

大约两年前,作者在他人推荐下买了一本书,书名叫《Crafting Interpreters》。

书到手时随手翻了几页,想着“以后一定认真读”,然后就放在那里落灰了。

直到 2026 年 1 月,实在没什么更有意思的事可做,这本书才终于被重新拿起来认真读。

接下来照着书里的过程实现了一遍 Lox 语言。书中用 Java 和 C 各写了一个解释器。当时选用 Rust,因为这是最熟悉的底层语言,一边对照 C 实现一边写 Rust 并不困难。

这个版本被命名为 Nox。

基础实现大约花了两天。书里故意留了一批特性作为读者练习,于是这些练习也被逐个实现。

然后就停不下来了。

不断往里加东西、删东西、重写运行时的一部分、改语法、优化 VM 的一些细节,如此反复折腾,最后它已经不太像 Lox 了。

两个月后,Nox 看上去可疑地像 Rust。下面这段 Rust 代码可以原样交给 Nox 编译:

rust
fn add(a: u32, b: u32) -> u32 {
  a + b
}

fn fib(n: u32) -> u32 {
    if n < 2 {
        return n;
    }

    add(fib(n - 1), fib(n - 2))
}

这段代码是合法的 Rust,同时也是合法的 Nox。

一开始这感觉很酷,但随后就必须面对一个问题:到底在做什么?

答案基本上就是“Rust,但是配上自己写的 VM”。

这个问题没什么意思。

Rust 并不是那个值得较劲的语言

Rust 是很好的语言,日常使用也很多。但回想起来,很少有哪次写 Rust 时会冒出“要是这门语言有 X 就好了”的念头。

PHP 完全不同。

PHP 用得极多,也数不清有多少次期望它拥有更丰富的类型、不同的语言构造、更严格的规则,或者某个在静态分析器里已经存在、但语言本身还没有的东西。

与此同时,Mago 也是长期投入的项目之一,它是一个面向 PHP 的静态分析器、格式化工具和 linter。如果读者常看这个博客,很可能已经知道 Mago 是什么。

做 Mago 意味着要花大量时间盯着 PHP 开发者已经通过 PHPDoc 在用的类型系统:泛型、形状数组(shaped array)、字面量类型、整数范围、类型细化、类型别名,还有很多别的东西。

于是很想知道:如果这些东西真的成为语言的一部分,会是什么感觉?

不是只有静态分析器能理解的语法,而是真正的语言特性——由运行时强制执行、被标准库使用、能通过反射看到,并且会在做出糟糕设计决策时反过来折磨设计者本人的那种。

所以换了问题。

与其做一门 Rust 形状的语言、再往上硬贴一些动态特性,不如让 Nox 的外观和行为更接近 PHP,然后用它来探索 PHP 的“如果当初会怎样”。

如果 PHP 有一个丰富得多的类型系统会怎样?

如果模式匹配可以解构对象、元组、形状、类型和范围会怎样?

如果函数是一等公民符号会怎样?

如果数组当初就被拆分成向量、字典和元组会怎样?

如果 PHP 某些最有动态味的特性根本不存在会怎样?

如果可以实现一个被否决的 RFC、把它用在真实程序里,然后判断它到底是不是个好主意,会怎样?

这个问题有意思得多。

从 Nox 到 Elissa

第一步是把 Nox 的前端替换成 PHP 形状的东西。

Mago 的 PHP 解析器本来就是现成的,于是把它复制过来开始改。

Nox 已经有了基本上相当于类的结构体,还有简单的单元枚举,没有 trait,实现块直接写在结构体或枚举声明下面。把这套基本对象模型映射到类 PHP 的语法上并不太难。

给变量加上 $ 之后,语言的某些部分反而更简单了。

在 Nox 里,一个裸名字比如 foo,可能表示局部变量、函数、其他符号或常量,取决于它出现的位置。局部变量变成 $foo 之后,大量歧义随之消失。$foo 就是变量,new Foo 就是在尝试构造类,其他名字按周围语法来解析即可。

真正困难的不是让 Whim 看起来像 PHP。

真正困难的是让它表现得像来自 PHP 家族的东西。

Nox 有一套正常的模块系统。写 mod foo 的意思是“去找 foo.nox 或 foo/mod.nox,解析它,并把它包含进程序里”。

这个模型必须推翻。

PHP 的模型奇怪得多,但它正是 PHP 之所以像 PHP 的原因之一。require 发生在程序运行期间,可以写在条件语句里面,被加载的文件可以依赖运行时信息,自动加载器只有在解析某个类状符号失败之后才会被调用。

最初版本的 require!("foo.els") 只能处理编译期已知的字面量字符串。把它变成真正的运行时操作要多花很多功夫。

编译器不能再假设自己在执行之前已经看见了整个程序。模块系统随之消失,命名空间不再是模块,而变成附着在名称上的前缀。VM、符号解析器、文件加载器和自动加载器都必须协同工作。

到这个时候,这已经不再是一次语法替换了。

到 2026 年 5 月,这门语言已经变成了另一种东西,于是它被改名为 Elissa,与 Carthage Software 的命名主题保持一致。

在那之后,开发过程极为科学、规划极其周密。

大致是这样:

“把这个加上。”

然后:

“哦,这个不错,那这个也加上。”

或者:

“三年前曾为 PHP 提过类似的方案,结果被否决了。管它呢,在这里加上。”

没有宏大的语言设计文档,也几乎没有路线图。有些想法来自 PHPDoc,有些来自被否决的 PHP RFC,有些来自仍在草案或讨论中的 RFC,有些来自其他语言,有些是因为写某个程序时产生了需要,还有一些纯粹是因为空闲时间实在太多。

2026 年 8 月,为了让项目能够开源,又做了一次清理,期间决定再次改名。

Elissa 听起来太严肃了。

Elissa 听起来像是背后有一套计划、一份路线图,说不定还有个小委员会。

但这些东西并不存在。

真正需要的,是一个能直接说明项目性质的名字:这门语言存在的意义,就是可以随时尝试某个想法、用上一阵子,然后搞清楚它究竟是巧妙、无用、糟糕,还是三者兼有。

于是 Elissa 变成了 Whim。

本地目录至今仍然叫 elissa,这是理所当然的。

那么,Whim 是 PHP 方言吗?

也许算,前提是把“方言”这个词拉伸到开始尖叫的程度。

可以说它受 PHP 启发,可以说它是非常远的亲戚。但不要听到这些说法就以为它兼容。

如果说 Hack 是 PHP 的表亲,那么 Whim 就是父亲那一支里隔了五代的远房亲戚——原本不知道他的存在,他却总在纠缠着不放。

Whim 远看很熟悉,它有 $变量、类、接口、命名空间、闭包、属性(attributes)、异常,还有大量 PHP 开发者认得出的标准库概念。

再靠近一点就会发现,它的行为完全不同。

大部分 Whim 代码不能当作 PHP 运行,大部分 PHP 代码也不能当 Whim 运行。Whim 没有可变变量(variable variables),没有 eval,没有动态属性访问,也没有动态方法访问。名称区分大小写。它没有那种既是列表、映射、元组、有序字典、队列又是随便什么当天需要的东西的万能 PHP 数组类型。

Whim 还把 PHP 中彼此分开的符号表合并了。

在 PHP 里,函数和类可以同名。在 Whim 里不行。一个名字只对应一个符号,这个符号可能是函数、类、常量、类型别名或别的东西。

这个模型在 Whim 里很有用,尤其当类型系统开始直接引用符号之后。

但如果 PHP 采用它,整个世界都会崩掉。

这个区别在后续讨论中会反复出现。

某个特性在 Whim 里能工作,并不能证明它可以被加进 PHP,不能证明 PHP 应该加它,甚至不能证明它在 Whim 里就是个好主意。

它唯一说明的是:这个特性被实现了,所以现在有一个真实的东西可以拿来探究,而不必继续无休止地争论假设。

规模逐渐超出预期

Whim 从一本书里的解释器起步,但现在它已经不再是个很小的 AST 遍历器了。

Whim 源代码会编译成字节码,字节码经过优化器,然后由 VM 执行。

标准库的大部分内容是用 Whim 自己写的。Whim 二进制文件里同时嵌入了标准库的源代码和编译后的字节码。

项目里有格式化工具和基于 AST 的 linter,两者都是 mago fmt 和 mago lint 的缩小版。

还有一个很小的语言服务器,提供语法高亮、格式化、解析器与 linter 诊断、代码折叠和选区范围。

神奇之处大概就到这里。

没有项目级索引,没有跳转定义,没有查找引用,没有调试器,没有性能分析工具,也没有静态分析器。Whim 有一套夸张的类型系统,但目前只在运行时强制执行。

有 Tree-sitter 语法和 Zed 扩展。Zed 扩展负责加载语法并启动语言服务器。它很有用,但并不是偷偷藏起来的完整 IDE。

另一方面,对于一门总被称作玩具的语言来说,标准库大得离谱。

它覆盖异步编程、通道、压缩、文件系统、进程、网络、TCP、UDP、TLS、HTTP、SMTP、正则表达式、Unicode、JSON、BSON、CSV、日期、时间、随机数生成、密码处理、反射、数据库,以及很多大概根本不急着加的东西。

其中大部分最初是对 PSL 相当直接的改写。

但这种直接很快就结束了。

Whim 有泛型、密封类型、更丰富的集合类型和更大的运行时类型系统。因此可以写出更严格的 API,表达那些 PSL 只能用 PHPDoc 描述、或者在 PHP 里根本无法描述的约束。

标准库的其他部分不太像 PSL 的移植,更像 PHP 扩展的演进版本。

比如 Whim\Database 是从零写的,支持 PostgreSQL 和 SQLite,使用非阻塞操作,具备连接池和事务,基本上是想做出一个比 PDO 更好用的东西。

Whim\Reflection 也是围绕 Whim 的实际语言模型设计的。它理解泛型、类型别名、newtype、字面量类型、整数范围、形状集合、联合类型、交集类型、可调用对象的捕获变量,以及类型系统里的其余部分。

它也无法被用来突破可见性、去摆弄私有或受保护状态。

在 Whim 里,私有就是私有。多新奇的思路。

这两者都值得单独写文章,所以这里就此打住,免得这篇介绍变成整个标准库的文档。

但这个东西真的能用吗?

到某个阶段,编译器示例就不够用了。

任何特性都能在一段专门为凸显它而写的十行代码里显得很棒。真正的考验是:在标准库、若干个包,以及一个需要长期维护的应用里用上几百次之后,是否还喜欢它。

于是开始用 Whim 构建东西。

在 Trifle 组织下做了一小组包,包括 trifle/clock、trifle/markdown 和 trifle/seal。

做这些包有三个原因。

第一,需要测试 Whim 内置的基于 Git 的包管理器。那个东西值得单独写一篇文章。

第二,需要验证 Whim 到底能不能用于正常工作,而不是只写些专门展示语言特性的漂亮示例。

第三,需要一个地方安放那些很有用、但不该放进标准库的代码。

Whim 的包管理器直接从 Git 仓库安装依赖。项目有一份清单文件,whim install 用 Git 拉取依赖,然后 Whim 生成锁文件。

没有中央仓库。

没有人想运营一个仓库、审核包名、管理账号、找回密码、对抗垃圾信息,更不想某天早上醒来发现自己要为 Whim 版的 npm 负责。Git 仓库已经够用了。

Trifle 里的这些包不是生态繁荣的证据,因为根本没有生态。它们主要是在验证:当代码放在主仓库之外时,整套东西还能不能工作。

到目前为止最有价值的测试是 play.whim.sh。

从外面看,这个 playground 很简单:粘贴一段 Whim 代码,然后运行、格式化或检查它。

后端要复杂得多。

整个 playground 完全用 Whim 写成。它并发处理请求,使用 Whim 的异步特性和协程,连接数据库,运行 HTTP 服务器,并协调用户提交代码的沙箱执行。

Docker 为这些代码提供实际的沙箱,nginx 挡在 Whim 的 HTTP 服务器前面。整套东西跑在一台 5 美元的独立 Hetzner VPS 上。

它与其他项目相互隔离,这是出于谨慎,也是为了避免牵连。

构建这个 playground 给 Whim 施加了那些小示例从未触及的压力。它暴露了性能问题,检验了异步运行时,迫使若干标准库 API 协同工作,也展示了维护一个真实 Whim 应用是什么感觉。

另有一个用 Whim 编写的小应用,仅用于个人日常使用与维护。

所以严格来说,确实有两个“生产”应用跑在 Whim 上。

但这并不意味着 Whim 已经可用于生产。

应该在生产环境使用 Whim 吗?

不应该。

认真的,不应该。

只有在下面两条同时成立时才考虑:

  • 有大量空闲时间;
  • 并不太在意跑它的机器被攻破。

Whim 没有经过专业的安全审计。

明显的安全问题都检查过,也尽量避免写出显而易见的漏洞。HTTP 服务器也读过一遍,只能希望里面没有藏着 RCE。

这不能算安全审计。

作者不是安全工程师,没有拿薪水的蓝队,也说不清编译器、VM、HTTP 服务器、包管理器、标准库,或者其他那些实验期间写下的部分里可能存在什么样的安全问题。

所谓“实验性”,不只是在说某个 API 下周可能改名,也是在说:不应该假设它面对恶意输入时是安全的,也不应该把重要的数据托付给它。

playground 之所以公开,是因为提交的代码在 VPS 上的 Docker 沙箱里运行,而那台 VPS 与任何在意的东西隔离。即便如此也还是很小心,但这与把重要系统或私密数据交给 Whim 完全是两回事。

而且,发布 Whim 并不意味着要开始用它替代 PHP 来写自己真正认真的项目。

现在阅读这篇文章的网站就是用 PHP 写的,并且会一直如此。同一个应用还有一个后台,用来管理发票、客户、联系人、业务支出以及其他真正重要的信息。

这些不会交给 Whim。

Whim 可以有能力写出有意思的软件,同时又是绝不会拿来存放业务记录的东西。

这两件事并不矛盾。

不要指望获得支持

Whim 是开源的,但它不会成为社区共同维护的语言。

Pull request 完全关闭。

任何人都可以按许可证 fork Whim、修改它、构建自己的版本,或者把它带向完全不同的方向。只是不要把补丁送回来。既没有时间,也没有意愿去评审外部改动。

Issue 保持开放。可以提交缺陷,也可以提建议。

开一个 issue 并不等于承诺会回复、会修、会排期,甚至不保证很快会看。它可能在那里放上好几个月,可能不做任何修改就被关掉,也可能永远不会成为优先事项。

没有支持计划,没有固定路线图,也没有承诺的响应时间。

同样没有严肃的向后兼容保证。语法、语义、字节码、标准库 API 或包格式都可能改变,理由只是找到了更满意的设计。

开源意味着可以阅读它、fork 它。它不意味着这个项目欠谁一套治理模式、发布节奏、兼容策略或客户支持部门。

欢迎试用 Whim、用它写代码、做包、阅读实现,然后反馈发现的奇怪之处。

只是不要对项目抱什么期待。

请不要用 Whim 来羞辱 PHP

这是最在意的一点。

不希望有人看到 Whim 仓库里一天加三个特性、一周发十个版本之后,写出这样的话:

PHP INTERNALS 看啊!一门和 PHP 很像的语言都能做到这些!我们凭什么要为一个特性等好几年?

闭嘴。别这么说。

Whim 和 PHP 面对的条件完全不同。

可以第二天醒来就改 Whim 的语法、删掉一个特性、合并两个符号类别、重命名半个标准库,然后午饭前发布出去。

有正当理由声称“你搞坏了我的生产系统”的人,基本上一个也没有。

PHP 没法这样运作。

PHP 有几十年的既有代码、庞大的包生态、大量已部署的应用、依赖引擎行为的扩展、把它打包进发行版的各类操作系统、为它建模的静态分析器和 IDE、围绕它的怪癖构建的框架,以及那些合理地期待旧代码继续运行的人。

在 Whim 里看起来很小的改动,在 PHP 里可能破坏成千上万个项目。

PHP 还有一整套流程。人们讨论改动、争论语义、实现、评审、投票、写文档,然后维护很多年。

这个过程可能很慢,可能累人,有时也非常烦人——提过 RFC 的开发者对此并不陌生。

但它存在是有理由的。

一个被否决的 RFC 不等于 PHP internals 判断错误。在 Whim 里实现同一个想法,也不能神奇地证明那个 RFC 本该通过。

它可能提供新的证据,可能揭示更好的设计,可能证明当初的某个担忧其实并不成立。

它也可能证明这个想法本来就是坏的。

它无法做的是:抹掉 PHP 的兼容性约束、实现成本、维护负担,以及达成共识的必要性。

Whim 不试图替代 PHP。

Whim 不试图与 PHP 兼容。

Whim 里的特性并不都属于 PHP。其中很多如果加进 PHP,会带来无法接受的向后兼容破坏。

即便某个特性理论上能进入 PHP,在 Whim 里实现它也不能证明它可以被安全、高效、兼容地加进去,并以人们愿意维护二十年的形式存在。

一个人的实验项目是独裁。

PHP 不是。

Whim 对 PHP 究竟有什么用

这一切并不意味着 Whim 对 PHP 的未来毫无用处。

它可以真正发挥作用。

Whim 提供了一个可以低成本犯错的地方。

不必完全在抽象层面上争论一个特性,而是可以直接实现它,在大型标准库里用它,用它构建包和应用,观察它如何与反射、泛型、运行时类型、异步执行、错误处理以及其他所有东西互动。

然后就能知道,长期共处之后是否还接受它。

例如,Whim 有 final 局部变量:

php
final $message = 'hello';

这个特性可以工作。

它也几乎完全没用。

它给变量声明增加了一个关键字,要求 Whim 跟踪某个局部变量是否还能再次赋值。而在写那个 playground 的过程中,这个特性一次都没派上用场。

这就是有用的信息。

一个特性可以自洽,可以容易解释,可以完全按设计工作,却仍然无法证明自身存在的必要性。

Whim 还把 returnbreakcontinue 当作表达式,而不只是语句。

这个特性令人喜欢,它让某些代码更好写。它还要求在编译器和 VM 中做大量改动,而这些改动从语法表面根本看不出来。

PHP 里也许可以做类似的事情,但语法看起来简单,这一点几乎无法说明实现工作量与兼容性风险有多大。

再比如 Whim 合并的符号表。

这个模型很好,它让类型系统的某些部分更干净。但它基本没有任何进入 PHP 的路径,因为 PHP 代码已经依赖函数、类和常量分处不同命名空间这件事。

所以现在已经出现了三种不同的结果:

  • 一个能工作但最终证明几乎没用的特性;
  • 一个效果不错、但需要的改动远比语法看起来深得多的特性;
  • 一个在 Whim 里能用、但会破坏太多既有 PHP 代码的特性。

这就是实验的意义。

在这一系列文章里,要超越“看起来很酷,我也想要”这种表层反应。

对每个特性,都会说明它是什么、为什么加上它、它如何工作、难在哪里、有哪些限制、如何与语言其余部分互动、PHP 能否支持类似的东西,以及 PHP 为什么仍可能决定不要。

有时某个 Whim 特性可能启发出未来的 PHP RFC。

有时实现它恰好能证明 PHP 永远不该拥有它。

两种结果都有价值。

本作品采用《CC 协议》,转载必须注明作者和本文链接