大家都说 WebAssembly 是栈机器。Wikipedia 这么写,Wasm 设计文档也这么说。

但一个很反常的细节是:它连传统栈机器最基本的“搬弄栈”的能力都很弱。没有常见的 dup,没有 swap,没有 over。主要能做的栈操作,几乎只剩 drop

这就有意思了。一个被长期归类为栈机器的东西,偏偏不太允许你像栈机器那样写程序。

栈在 Wasm 里,更像编码,不像完整机器模型

先把概念压短。

寄存器机器,是显式写位置。比如 a = b + c,指令里写清楚取哪几个寄存器、结果放哪。

栈机器,是靠顺序隐式取值。push 2push 3addadd 默认拿栈顶两个值。

简单表达式里,两者差别不大。比如算 2 3 + 5 7,栈式写法很自然:先压 2、3,乘;再压 5、7,乘;最后加。

麻烦出在复用。

比如已经算出 x,现在要算 x x x。传统栈机器会用 dup 复制栈顶,再配合 swap 调整顺序。JVM bytecode 里就有 duppopswap 等操作。当然,JVM 也不是“纯栈机器”,它同样有 local variable 指令,比如 iloadistore

Wasm 的情况不同:

模型怎么引用值栈重排能力复杂复用靠什么
寄存器机器显式寄存器索引不依赖栈寄存器/变量
传统栈机器程序顺序隐式决定dupswapover栈操作
JVM操作数栈 + locals较完整栈操作与 locals
Wasm操作数栈表达输入输出基本只有 droplocals

所以原博客的核心判断很清楚:Wasm 确实用操作数栈表达指令输入输出,但它不像 Forth 或 JVM 那样,把栈重排当成主要编程能力。

二进制 Wasm 用逆波兰式,当然可以用栈求值。但这更像一种紧凑编码。文本 Wasm 甚至可以写成类 Lisp 的前缀结构。换句话说,栈在这里更多是“表达式序列化方式”,不是完整计算世界观。

误导不在名字,而在经验迁移

我更在意的不是 Wasm 到底该不该叫栈机器。技术圈太爱为名词开庭,但工程里真正伤人的,往往不是名字不准,而是名字让你带错模型。

如果你带着 JVM 或 Forth 的经验来手写、调试、优化 Wasm,就很容易踩坑。你以为可以靠栈操作优雅复用中间值,结果发现路很窄。公共子表达式消除、expr^2 变成 expr * expr 这类事,一旦涉及复用,通常就要引入 locals。

这不是说 Wasm 性能差。现代编译器可以把它转成 SSA,中间格式怎么编码,不必直接决定最终机器码质量。Wasm 当初选择这种设计,也有现实好处:格式紧凑,校验清晰,解释器实现容易,浏览器和运行时更愿意采用。

少即是多,有时是好设计;少到让老经验失效,就该换脑子。

multi-value 扩展后来改善了块与栈交互的限制,比如控制流块能返回多个值。但它没有把 Wasm 变成传统意义上的“栈语言”。真正的分水岭仍然在 locals、优化器和语义设计上。

这里有点像早期铁路借用马车时代的词。车厢、驿站、线路都像旧词,但调度逻辑已经变了。Wasm 也一样,名字沿用了栈机器的影子,骨架却更接近“用栈式编码包装复合表达式的寄存器机器”。

“名不正则言不顺”放在这里不算夸张。因为这个标签会决定你怎么读指令、怎么写生成器、怎么设计优化 pass。

Wasm 没有背叛栈。它只是没打算成为你熟悉的那种栈机器。