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 编译:
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 局部变量:
final $message = 'hello';这个特性可以工作。
它也几乎完全没用。
它给变量声明增加了一个关键字,要求 Whim 跟踪某个局部变量是否还能再次赋值。而在写那个 playground 的过程中,这个特性一次都没派上用场。
这就是有用的信息。
一个特性可以自洽,可以容易解释,可以完全按设计工作,却仍然无法证明自身存在的必要性。
Whim 还把 return、break 和 continue 当作表达式,而不只是语句。
这个特性令人喜欢,它让某些代码更好写。它还要求在编译器和 VM 中做大量改动,而这些改动从语法表面根本看不出来。
PHP 里也许可以做类似的事情,但语法看起来简单,这一点几乎无法说明实现工作量与兼容性风险有多大。
再比如 Whim 合并的符号表。
这个模型很好,它让类型系统的某些部分更干净。但它基本没有任何进入 PHP 的路径,因为 PHP 代码已经依赖函数、类和常量分处不同命名空间这件事。
所以现在已经出现了三种不同的结果:
- 一个能工作但最终证明几乎没用的特性;
- 一个效果不错、但需要的改动远比语法看起来深得多的特性;
- 一个在 Whim 里能用、但会破坏太多既有 PHP 代码的特性。
这就是实验的意义。
在这一系列文章里,要超越“看起来很酷,我也想要”这种表层反应。
对每个特性,都会说明它是什么、为什么加上它、它如何工作、难在哪里、有哪些限制、如何与语言其余部分互动、PHP 能否支持类似的东西,以及 PHP 为什么仍可能决定不要。
有时某个 Whim 特性可能启发出未来的 PHP RFC。
有时实现它恰好能证明 PHP 永远不该拥有它。
两种结果都有价值。