Ante:在 borrow checker 和 RC 之间找到第三条路

Ante:在 borrow checker 和 RC 之间找到第三条路

programming-languagesmemory-managementrust

数据源:Verdagon blog + HN · HN

你刚写完一个新功能,兴奋地敲下 cargo build。终端跳出一屏 borrow checker 错误。第 N 次,&mutRc<RefCell<T>> 之间的纠结让你怀疑人生:为什么不能像写 Swift 那样随手 var,又不需要担心运行时崩溃?

这是 Rust 程序员熟悉的痛感。borrow checker 保证内存安全,但代价是陡峭的学习曲线和无处不在的所有权约束。而在路的另一边,Swift 的 ARC(自动引用计数)让开发者几乎不用想内存管理,但每一次 retain/release 都是运行时开销,且在复杂引用循环面前仍需 weak 手动救场。

长期以来,这两条路线被视为互斥的——要么编译期零开销但心智负担重,要么运行时自动管理但有一定代价。Ante 语言正在尝试打破这个二分法。

两者为什么难以融合?

Rust 并非没有尝试过。它的 Rc 类型提供了引用计数,但当你需要在多个地方修改共享数据时,必须加上 RefCell,写成 Rc<RefCell<Spaceship>>RefCell 在运行时会检查借用规则——如果两个调用者同时拿到可变引用,程序直接 panic。try_borrow_mut() 可以避免崩溃,但只是把问题转移到别处:调用者必须处理 None 返回。

Swift 也往这个方向走了。它引入了 borrowing system,但依赖昂贵的运行时排他性检查。同样,一旦规则被违反,程序在运行时崩溃。

核心矛盾在于:borrow checking 要求「同一时刻最多一个可变引用」;RC 则天然允许多个持有者共享同一份数据。两者在逻辑上就是对冲的。任何融合方案都必须解决同一个问题——当多个地方持有对同一数据的引用时,如何保证某个位置的可变操作不会让其他位置的引用变成悬垂指针?

Ante 的答案:shape-stability

Ante 是一个尚在开发中的系统编程语言,定位是「更简单的 Rust」。它引入了 shape-stability(形状稳定性)概念:如果一个类型的「形状」在运行中不会改变——即它占用的内存不会被释放或重新解释——那么对它的引用就是安全的,无论别处对它做了什么修改。

换句话说,只要一块数据不可能被销毁,你就可以持有任意多个指向它的可变引用——因为没有悬垂的风险。

来看一个例子:

type Entity =
    energy: I32
    health: I32

heal (healer: mut Entity) (target: mut Entity) =
    healer.energy -= 10
    target.health += 10

在 Ante 中,你可以用同一个 Entity 同时作为 healertarget 调用 heal

self_heal (entity: mut Entity) =
    heal entity entity

两个 mut 引用指向同一块内存,编译器却不会报错。理由很简单——Entity 的内部结构(两个 I32 字段)是形状稳定的。修改字段的值不会改变结构的形状,也不存在释放 Entity 的操作,两个引用始终合法。

更复杂的情况同样适用:你可以同时持有指向 Spaceship 的一个可变引用,和指向其 engine 字段的另一个可变引用:

refuel (ship: mut Spaceship) =
    engine_alias: mut Engine = ship.engine
    ship.engine.fuel := 200   // 使用原始的 ship 引用
    engine_alias.fuel := 100  // 使用 engine 别名

在 Rust 中这无法编译——ship 已经通过 ship.engine 产生了可变借用,不能再同时用于赋值。在 Swift 中同样不行。Ante 的编译器理解:只要 ship 本身不被销毁,它的所有子字段也都安全。这正是 shape-stability 赋予的自由度。

引入引用计数:sharedRc

shape-stability 真正发光的地方,是它和引用计数的结合。

在 Ante 中,给类型定义加上 shared 关键字,就启用了自动引用计数:

shared mut type Spaceship =
    engine: Engine
    name: String

launch (var ship: Spaceship) =
    set_fuel (mut ship.engine)

set_fuel (engine: mut Engine) =
    engine.fuel := 100

launch 拿到一个引用计数的 Spaceship,然后创建了一个指向其 engine 字段的可变引用并传给 set_fuel。这个操作在 Rust 中需要经过 RefCell::borrow_mut()(可能 panic),在 Swift 中需要经过运行时排他性检查(可能崩溃)。Ante 的编译器在编译期就能证明这个操作是安全的:launch 持有 ship 的引用,所以 ship 不会在 set_fuel 执行期间被释放。

Ante 的核心洞察在于:只要有一个持有者保持对象存活,对对象内部的可变借用就是安全的。这个逻辑和 Rust 的 borrow checker 一脉相承,但 Ante 将其扩展到了引用计数场景——编译器追踪的核心问题是「有没有人保证这块数据不会被销毁」,而非传统 borrow checker 关注的「谁拥有这块数据」。

处理 union:uniq 机制

shape-stability 能安全地处理 struct,但 union(和类型/tagged union)是另一回事。因为 union 的 variant 可以在运行时改变——一个 Option<String> 可以从 Some("hello") 变为 None,原先的 String 被释放——这就是悬垂引用的根源。

Ante 为此引入了 uniq(独占可变引用)和临时转换机制。uniq 类似于 Rust 的 &mut:它保证在当前作用域内,没有其他代码可以访问指向同一数据的引用。

launch (var ship: Rc Spaceship) (var other_ship: Rc Spaceship) =
    match uniq ship.engine
    | StringTheoryEngine str ->
        other_ship.engine := ImpulseEngine 0x42
        str.[0] := 'z'
    | ImpulseEngine fuel ->
        ()

如果 shipother_ship 指向同一个 Spaceship,这段代码会导致非法访问——在拿到 str 引用之后,重新赋值 other_ship.engine 会销毁 StringTheoryEngine,使得 str 变成悬垂指针。Ante 编译器会拒绝这段代码。

Ante 的做法和 Rust 有一个微妙但关键的区别:Rust 通过所有权系统确保在整个程序的生命周期中,任何时候都不会有两个可变引用同时存在;Ante 则只要求在当前作用域内没有其他代码访问可能冲突的引用。这是一种局部约束而非全局约束,更接近 C 语言中 restrict 指针的思路——可以有多个指针指向同一块内存,但编译器只保证在某段代码中只有一个被使用。

代价与权衡

这种方案并非没有代价。Ante 的创建者 Jake 也坦言,当前的 type analysis 有一定局限性:要回答「Engine 类型是否可能包含一个 Spaceship 的引用」这种问题,编译器必须递归检查整个类型图。这意味着给 struct 添加一个字段可能成为破坏性 API 变更——对于库作者而言,这比 Rust 的约束更隐蔽。

目前团队正在探索几个改进方向:用匿名品牌(anonymous brand)标记引用计数类型以替代类型分析;引入类似 Pony 的 iso 隔离权限;或者通过 effect system 追踪可变性来源。

社区对此也有质疑。HN 上有评论指出,允许多个可变引用共存虽然在单线程场景下内存安全,但在多线程环境下可能导致 data race——即使对简单的 I32 字段的并发写入,在没有原子操作的情况下也可能产生未定义行为。Ante 的单线程安全性和多线程安全性之间的界限还有待明确。

格局正在松动

这篇文章的作者 Evan Ovadia(Vale 语言的创建者)提出了一个有趣的观察:我们曾经认为「共享可变借用」是不可能的——Rust 基本建立在这个信念之上。但越来越多的反例正在打破这个教条:

  • Ante 通过 local uniqueness 在共享可变数据上获取独占借用
  • Vale 通过 pure function 在共享可变数据上获取不可变借用
  • Group borrowing 允许编译器对非形状稳定的数据创建共享可变借用
  • Rust 的 GhostCell 让对象图可以互相引用

这些技术指向同一个方向:有一条更统一的内存安全理论正浮出水面,只是我们还没完全看清它的轮廓。

意义

Ante 目前仍是一个早期项目,部分特性已实现,部分仍处于理论阶段。它的价值在于提出了一个值得认真对待的问题:borrow checking 和引用计数真的是二选一吗?

如果这条路走通了,其影响将是深远的。开发者可以先用引用计数快速原型,再逐步将热点路径迁移到 borrow-checked 代码——不需要重写,不需要在两种范式之间手动搭桥。这种渐进性是现有语言都不具备的。

Rust 证明了零开销抽象是可能的。Swift 证明了自动内存管理可以是实用的。Ante 瞄准的是那个更难的命题:两者是否可以兼得?即使最终答案是否定的,这个问题本身就值得被认真追问。

本文的素材来自公开信息和社区讨论。如果你对这个话题有更深入的一手经验,欢迎指出文中的不足。

参考链接