Safari 18.2 今天把 WebAssembly 尾调用优化正式落地了——递归不再爆栈,函数式语言终于在浏览器里跑开了

写递归函数的时候,开发者最怕的不是写错,而是栈溢出。

函数末尾 return 一个函数调用——递归也好、互相调用也好——在传统 Wasm 里,编译器会给你新建一个栈帧,每次递归都多压一层。深度 10000 的链表求和,栈上压 10000 层,直接崩。

这就是 Wasm Tail Calls 要解决的事。


尾调用优化:递归变循环

当一个函数的最后一步是调用另一个函数时,尾调用优化(TCO)会丢弃当前栈帧,直接跳到目标函数——不是”调用”,是”跳转”。

没有 TCO:每次递归压一层栈,O(n) 栈空间;有 TCO:编译器把递归变成跳转,O(1) 栈空间。

;; 有 TCO:末尾直接跳转,不压栈
(func $sum-tco (param $list i32) (param $acc i32) (result i32)
  loop (param $list i32) (param $acc i32) (result i32)
    local.get $list
    i32.eqz
    if (result i32)
      local.get $acc
    else
      local.get $list
      i32.load
      local.get $acc
      i32.add
      local.get $list
      i32.const 4
      i32.add
      return_call $sum-tco ;; 尾调用:丢弃当前帧,直接跳转
    end
  end
)

谁最受益

Wasm Tail Calls 主要受益者是编译到 Wasm 的函数式语言:

  • Haskell(GHC Wasm 后端):纯函数式语言没有 loop 语法,全靠递归+TCO 才能跑
  • Scheme / Racket:规范要求 TCO,Wasm Tail Calls 让它在浏览器里合规运行
  • Erlang/Elixir:BEAM actor 模型靠大量轻量进程,尾调用是关键
  • Scala(Dotty):支持尾调用注解

对主流前端场景的影响是间接的——如果你用 TypeScript 或 Rust 写 Wasm 模块,通常不需要关心 TCO;但如果你用的框架、语言或工具链内部依赖函数式范式,现在它们的 Wasm 编译结果会更小、更安全。


三引擎支持现状

浏览器 Wasm Tail Calls
Chrome 119+(2023-11)
Firefox 120+(2024-01)
Safari 18.2+(2026-10)

Safari 18.2 后,三大浏览器全部支持 Wasm Tail Calls,配合 WasmGC(Safari 18.2 同批支持),浏览器对函数式语言编译到 Wasm 的支持正式闭合。


下一步

  1. 查工具链:如果你用 GHC(WebAssembly target)、rustc(Wasm backend)、Scala.js,确认它们已支持 return_call 目标
  2. 看性能:内存受限场景(嵌入式 Wasm)下,TCO 可以显著降低栈内存占用
  3. 搭配 WasmGC:同批 Safari 18.2 支持的两个特性,组合使用:函数式语言 → Wasm + GC + Tail Calls → 浏览器原生运行

评论区

0 条评论

登录后可评论。

铁锈·Rust工具链 51 阅读