Skip to content

RFC-009a: 令牌生命期分析——基于霍尔证明管道 ​

父 RFC: RFC-009: 所有权模型设计

依赖: RFC-027: 编译期谓词与统一静态验证

前置条件: RFC-027 已被接受。本 RFC 的所有机制(证明管道、SMT fallback、路径条件收集)依赖 RFC-027 的实现。

本 RFC 修正并替代 RFC-009 §"令牌冲突检测:流敏感活性分析"(第 663-684 行)。

摘要 ​

RFC-009 第 684 行声称令牌冲突检测"不需要……NLL"。结论正确,论据错误。

它不是"因为令牌是值,线性追踪就够了"。它是因为:令牌活性是霍尔逻辑命题,不是专门的流敏感分析。

{conflicting_tokens 全部死亡} op {WriteToken 安全获取} —— 同一个 {P} op {Q},和类型检查、谓词验证共享 RFC-027 的证明管道。没有新分析框架。一条管道,多种命题。


动机 ​

RFC-009 的混淆 ​

RFC-009 把两个问题混为一谈:

  1. 线性追踪(Move 后不可用)—— {v 未被 move} use(v) {类型匹配}。类型检查器已有。
  2. 令牌生命期交互(子令牌存活 → 父令牌暂停 → 子令牌死亡 → 父令牌复活)—— {conflicting_tokens 全部死亡} write(data) {安全}。需要活性分析,不是线性追踪。

当前代码的实际情况 ​

组件状态
BorrowChecker线性扫描 IR,被动响应显式的 Borrow/Release 指令
ControlFlowAnalyzer::analyze_instruction空实现(control_flow.rs:145-153)
liveness_analysis存在但仅用于 Drop 插入,未接入令牌冲突
Release 插入硬编码在 Call 指令之后——纯词法作用域(ir_gen.rs:2734-2736)

用户可见后果:

yaoxiang
data = vec![1, 2, 3]
view = &data              # 创建 ReadToken
x = view.total_count      # view 的最后使用
data.push(4)              # ❌ Release(view) 尚未执行,ReadToken "活着"

为什么需要重写 ​

上一版(009a v1)用"DAG 替代 NLL"叙事,引入了不必要的新概念(保守分支规则、循环特殊处理)。核心矛盾没有说清楚:借用检查不是独立系统——它是霍尔命题的一种。


核心设计 ​

一切皆霍尔 ​

类型检查:  { x: Int }        x + 1        { result: Int }
借用检查:  { view 已死 }     data.push(4)  { WriteToken 获取成功 }
谓词验证:  { y > 0 }         divide(x, y)  { result: Int }
回边切断:  { i == n }        下一轮循环     { cond == false }

同一个形式 {P} op {Q}。编译器对每个操作生成前置命题 P,送入证明管道验证。

借用检查和用户谓词共享同一条管道。 区别仅在于命题由谁生成、证不出来时怎么处理。

两类谓词,一条管道 ​

用户谓词系统谓词(借用)
命题生成程序员(类型标注)编译器(品牌树 + 所有权规则)
证明提供编译器 + 程序员编译器全自动
证不出来写证明函数或重构重构代码(门留着但极少需要)
可见性签名可见隐式,不污染类型签名
学习成本想用才学零

系统谓词的证明不给程序员开证明函数——编译器全自动。 证不出来时用户重构代码。

三种失败模式,同一验证引擎。 类型命题证不出来 → 编译错误(不可绕过)。借用命题证不出来 → 编译错误,重构代码(不可绕过)。用户谓词证不出来 → 编译错误,可写证明函数(可绕过)。失败策略不同,但验证引擎是同一个——SMT 求解器 + 编译器内核推理规则。区别仅在于"证不出来时谁负责补证明"——编译器拒绝替程序员写借用证明(借用命题的证明策略是结构分析 + SMT,不需要程序员介入),但接受程序员写的用户谓词证明函数。这不是管道不一致——是不同命题类别的责任边界不同。

这和 Rust 'a 不同:'a 是必修课,证明函数是选修课——绝大多数用户一辈子碰不到选修课的门。

借用命题:编译器自动生成 ​

用户写 data.push(4)。编译器自动生成命题:

WriteToken(data, node) 可获取
  = forall t in conflicting_tokens(data): t 在 node 处已死
  = forall t in brand_tree.children(data): forward_reachable(node) ∩ consumers(t) == ∅

三条规则,零特殊情况:

  1. 品牌树(RFC-009 §2.7)回答"谁和谁冲突":前缀匹配,O(depth),深度 ≤ 3
  2. 消费者列表(DAG 构建时自动收集)回答"令牌最后被谁消费"
  3. 前向可达性回答"消费者还能不能被执行到":结构性切断 + 逻辑切断

前向可达性:从消费者反向走 ​

对令牌 T 的每个消费者 C:

从 C 出发,反向 BFS DAG。
边被切断,如果:
  1. 它是 break(结构切断)
  2. 路径条件 ⇒ !loop_cond 被 SMT 证明为真(逻辑切断,RFC-027 管道)

沿所有未切断的边反向传播(包括回边,回边将活性传播到前一轮迭代)。
标记所有能到达的节点 → unsafe。

查询:写操作在节点 W → W ∉ unsafe → 安全。

不需要发明"保守分支规则"。不需要"循环保守存活"。一条反向 BFS + 两条切断规则。

证明策略:快速通道优先,SMT 兜底 ​

每个需要令牌的写操作
  │
  ├→ 快速通道:DAG 结构分析(覆盖 95%+ 场景)
  │     │
  │     ├→ 品牌树前缀匹配 → 找出冲突令牌(O(depth))
  │     ├→ 反向 BFS,break 切断回边
  │     └→ 无回边可穿越 → 直接判定 Proved / Disproved
  │
  └→ 慢速通道:SMT 逻辑切断(仅当快速通道遇到可穿越回边时)
        │
        ├→ 回边起点有路径条件 → SMT 判 path_cond ⇒ !loop_cond
        │     ├→ Proved → 逻辑切断 → 降级回快速通道继续
        │     └→ Disproved / Unproven → 回边穿越 → 标记 unsafe
        │
        └→ 回边起点无路径条件 → 回边直接穿越

快速通道覆盖:线性代码、if/else、loop + break、无路径条件的 while。 慢速通道覆盖:while 循环体内、有路径条件暗示循环将退出时。 不覆盖:运行时条件无法静态证明 → 回边穿越 → unsafe → 编译错误(用户重构)。

SMT 不是主力——是安全网。和 RFC-027 的用户谓词不同:用户谓词以 SMT 为主力;借用系统谓词以结构分析为主力,SMT 只补结构分析够不到的边角。

SMT 是精度层,不是 soundness 依赖。 借用系统谓词的 sound 判定完全由快速通道承担 (区间 + 反向 BFS + break 切断);SMT 逻辑切断只决定「循环边界的合法程序能否放行」。 SMT 不可用 / 超时 / 未实现(RFC-027 impl: in_progress)时,fallback = 回边穿越 = 保守拒绝, 该拒的仍然必拒。无 SMT 时的保守度 = 循环内借用+写一律拒,与 Rust NLL 同级 (Rust 生产级借用检查同样无 SMT)。SMT 落地是纯精度增益,不阻塞 sound 主线的交付。


用例分析 ​

线性代码 ​

yaoxiang
data = vec![1, 2, 3]        # 节点 1
view = &data                # 节点 2:消费 data,生产 ReadToken(#1)
x = view.total_count        # 节点 3:消费 view(= #1 的最后一个消费者)
data.push(4)                # 节点 4:需要 WriteToken(data)

反向 BFS 从 view.total_count(节点 3)出发 → 节点 3 是 #1 的最后一个消费者 → 节点 4 > 节点 3 → 节点 4 不在 unsafe → ✅

if/else:无特殊规则 ​

yaoxiang
view = &data
if cond {
    use(view)               # then 分支消费 view
} else {
    do_something_else()     # 不碰 view
}
data.push(4)                # view 的最后消费者在 if 内 → if 之后无消费者 → ✅

if/else 是 DAG 的一个复合节点。内部消费归因至此节点。不合并分支状态。不保守表决。后面有没有消费者,整数比较。

澄清:"不合并分支状态"仅指借用活性(品牌消费者反向 BFS)。 move 状态(变量所有权)是另一条分析:per-CFG-node 前向数据流 (NLL/Polonius 风格),分支汇合时保守 meet(任一分支 Moved → 汇合 Moved), 字面量不可达分支(if false)不参与。两者分层:借用活性看"有没有后续消费者", move 分析看"变量是否可能已被转移"。

if/else 带返回值逃逸 ​

yaoxiang
view = &data
result = if cond {
    view                     # view 逃逸到 result
} else {
    something_else
}
use(result)                  # 间接消费 view
data.push(4)                 # view 仍有消费者(use(result))
                             # → push 在 unsafe → ❌ 正确报错

view 通过返回值逃逸 → use(result) 是 view 的消费者 → 从 push 反向走能到达 use(result) → unsafe。

循环:break 切断回边 ​

yaoxiang
view = &data
loop {
    use(view)                # consumer
    if is_last {
        data.push(4)         # 写操作
        break                # ← 结构切断
    }
}

反向 BFS 从 use(view) → 回边 → 向前走到 data.push(4) → 碰到 break → 切断 → data.push(4) 不在 unsafe → ✅

没有 break:

yaoxiang
view = &data
loop {
    use(view)
    data.push(4)             # 无 break 切断 → 回边可穿越 → 下一轮 use(view) 可达
                             # → push 在 unsafe → ❌ 正确报错
}

while:SMT 逻辑切断 ​

yaoxiang
view = &data
mut i: UpTo(n) = 0
while i < n {
    use(view)                # consumer
    i += 1
    if i == n {
        data.push(4)         # 路径条件:i == n
    }
}

反向 BFS 从 use(view) → 回边 → 走到 data.push(4) → 检查路径条件 i == n → SMT 查询:i == n ⇒ !(i < n)?→ Proved → 逻辑切断 → data.push(4) 不在 unsafe → ✅

注意判定目标是写节点自身的路径条件(i == n 属于 if 分支内的 data.push(4)), 不是回边节点的路径条件。


本质:品牌 ID 就是 'a ​

不说"我们不需要 'a"。说"#42 就是 '42"。

RustYaoXiang等价性
'a#42编译期生命周期标识符
'a: 'b outlives 约束#42 是 #42.field_x 的前缀字符串前缀比较 = 偏序关系
NLL 活性传播(CFG 不动点)反向 BFS(DAG)都是可达性计算
Polonius 事实SMT 逻辑切断都是路径条件推理
约束系统不动点求解品牌树前缀匹配 + BFS不同的编码,同一个问题

我们没有发明新分析。我们只是把 'a 从类型签名层降到了证明层。 品牌 ID 做的事和 'a 完全一样——标记借用的身份、追踪派生关系、判定冲突。区别只有一个:'a 在用户写的类型签名里;#42 在编译器内部。

这不丢人。Curry-Howard 说类型是命题,程序是证明。'a 不是命题的一部分——它是证明策略的一部分。Rust 把证明策略写进了命题签名。我们把它放回该在的地方。

语言设计约束消除了什么 ​

复杂度来源避开了?原因
变量遮蔽✅语言禁止——一个名字永远指向同一个东西
for 跨迭代借用✅每次迭代新绑定——迭代间天然隔离
'a 生命周期标注✅品牌路径 = #42.field_x,编译器推导
命名生命周期 + 约束传播✅品牌路径前缀比较替代显式约束集
借用图约束求解(Polonius)✅品牌树前缀匹配 + DAG 消费者查询
循环体借用活性传播❌和 Rust 一样需要处理——用反向 BFS + 逻辑切断
条件分支保守性❌和 Rust 一样——SMT 覆盖可证明的,剩余保守拒绝

为什么 DAG 可行 ​

YaoXiang 的三项语言设计约束使 DAG 分析可行:

  • 无变量遮蔽——一个名字永远指向同一个东西,无需跨重绑定追踪
  • for 每次迭代新绑定——迭代间天然隔离,不存在跨迭代借用
  • 结构化并发——任务边界清晰,无需跨任务活性传播

这些约束消除了 Rust CFG 不动点迭代的主要复杂度来源。不是 DAG 比 CFG "更高级"——是更简单的语言设计允许更简单的分析。


详细设计 ​

系统谓词清单 ​

编译器自动生成以下命题,送入 RFC-027 证明管道:

系统谓词触发时机命题形式
borrow_conflict需要 WriteToken(v)forall t ∈ conflicting(v): dead_at(t, node)
use_after_move使用变量 v¬moved(v)
use_after_drop使用变量 v¬dropped(v)
double_dropDrop(v)¬dropped(v)
mut_violation写不可变变量 vis_mut(v)

现有的 BorrowChecker、MoveChecker、DropChecker、MutChecker变成命题生成器——不是消失,换身份。它们生成命题,管道验证命题。

品牌树 ​

RFC-009 §2.7 的品牌机制形式化为品牌树。

令牌语义——冻结优先,非复制优先:

&T 和 &mut T 的本质区别不是"能不能复制",是"允不允许同时有写":

ReadToken(T): 授予只读权限,同时冻结源数据 T——任何 WriteToken(T) 在此期间
              不可获取。冻结是 ReadToken 的首要语义。Dup(可复制)是冻结的推论:
              因为数据已被冻结(无突变可能),多份只读视图天然安全。

WriteToken(T):授予独占读写权限。因为存在写,任何其他令牌(读或写)都不可共存。
              不实现 Dup(线性类型)是独占的推论。

因果关系:

ReadToken 存在 → 源数据冻结 → 多份只读安全 → Dup
                      ↓
              WriteToken 被拒绝(borrow_conflict 系统谓词强制)

不是:

ReadToken 有 Dup → 可以有多个 → 顺便检查冲突  ← 因果倒置
BrandTree:
  nodes: Map<BrandId, BrandNode>

BrandNode:
  id: BrandId               # "#42"、"#42.field_x"
  kind: ReadToken | WriteToken
  source_var: Operand
  parent: Option<BrandId>   # 派生关系的父节点
  children: Set<BrandId>    # 派生子令牌
  consumers: Set<NodeId>    # 消费该令牌的 DAG 节点
  ref_count: usize          # ReadToken 冻结期间的安全副本数

冲突判断——冻结保证的执行机制:

rust
fn conflicts(a: &BrandId, b: &BrandId) -> bool {
    // 冲突条件:同源 + 至少一方是写 + 品牌路径重叠
    // 这意味着:
    //   1. ReadToken vs ReadToken → 无冲突(都是只读,无突变)
    //   2. WriteToken vs ReadToken → 冲突(写破坏了读的冻结保证)
    //   3. WriteToken vs WriteToken → 冲突(两个写不可共存)
    a.source() == b.source()
        && (a.is_write() || b.is_write())
        && (a.is_prefix_of(b) || b.is_prefix_of(a))
}

O(depth) 字符串前缀比较,深度 ≤ 3。常数级。

反向 BFS 活性分析 ​

本算法引入「令牌创建时间」维度。令牌活性是区间 [created_at, last_use],不是反向可达集; 写操作只在令牌的活性区间内才构成冲突——这覆盖「写先、借用后」的合法顺序 (§2.4 语义:参数令牌调用结束释放),避免将其误报。

算法:check_borrow(token, node, dag, brand_tree)

输入:
  token: 需要检查的 WriteToken
  node:  写操作所在的 DAG 节点

输出:Proved | Disproved

算法:
  # 快速通道:反向 BFS
  unsafe = empty_set
  queue = brand_tree.consumers(token)

  while queue not empty:
    cur = queue.pop()
    unsafe.add(cur)

    for each pred in dag.predecessors(cur):
      # 结构切断:break 不穿越
      if pred 是 break 边:
        continue

      # 回边 → 检查是否需要 SMT fallback
      if pred 是回边:
        path_cond = 写节点 node 的路径条件   # 判定目标为写节点自身条件
        loop_cond = 循环条件
        # 先看结构上能否切断(对应的 break 已切断路径 → 不会走到这)
        # 再看路径条件
        if path_cond 非空:
          result = smt_fallback(path_cond, loop_cond)   # ← 慢速通道
          if result == Proved:
            continue                    # 逻辑切断
        # 无路径条件 或 SMT 证不出来 → 穿越回边
        # fall through

      if pred ∉ unsafe:
        queue.push(pred)

  # 判定(写先、借用后)
  # node < created_at(token) → 写发生时令牌尚不存在 → Safe
  if node ∈ unsafe and created_at(token) ≤ node:
    return Disproved
  else:
    return Proved


smt_fallback(path_cond, loop_cond):
  # 仅在回边 + 有路径条件时调用
  # 使用 RFC-027 证明管道,共享同一 SMT 求解器、同一预算
  return smt.prove(path_cond ⇒ !loop_cond)
  # Proved → 逻辑切断
  # Disproved / Unproven → 不切断,回边穿越(保守拒绝)
  # SMT 不可用/超时/未实现 = Disproved 分支——
  # SMT 只影响精度(合法程序能否过),不影响 soundness(该拒的必拒);
  # 无 SMT 时的保守度 = 循环内借用+写一律拒,与 Rust NLL 同级。

BrandNode 增加字段:

BrandNode:
  ...
  created_at: NodeId         # 令牌创建节点(borrow 区间的左端点)

O(N),其中 SMT 调用次数 = 回边数 × 有路径条件的回边比例。实际代码中 SMT 调用极为罕见——仅 while 循环体内、有精化类型变量的路径条件时触发。

路径条件收集 ​

由 RFC-027 §3.2-3.3 已有机制提供:

  • if guard:if y > 0 → true 分支压入 y > 0
  • match 模式:if let Some(v) = opt → 分支内压入 opt == Some(v)
  • 赋值:i += 1,编译器维护变量值域信息
  • while cond:循环体内压入 cond == true

每个 DAG 节点携带一个路径条件集合。反向 BFS 回边时,取回边起点的路径条件,SMT 判断是否排除下一轮循环入口条件。

路径条件的传播规则:

  1. 路径条件附着在写节点自身:分支内写操作 W 携带其分支条件(if i == n { W } → path_cond(W) = i == n)。反向 BFS 穿越回边时,SMT 判定的是 path_cond(W) ⇒ !loop_cond(能走到 W 的路径必然退出循环 → 下一轮 consumer 不可达 → 切断),不是回边节点的路径条件。
  2. join 保守清空:if/else 汇合点不携带分支内路径条件(两分支条件析取后 通常不可判定,直接清空)。汇合点之后的写操作 path_cond 为空 → 回边穿越。
  3. 路径条件语义化:path_cond 是 ConstExpr(RFC-027 §3.2 语义),不是 源码文本;smt_cut 将其翻译为 SMT 约束后求解。
  4. 无路径条件 → 回边直接穿越(unsafe),不调用 SMT。

与 RFC-027 的接口 ​

借用系统谓词和用户谓词共享同一条证明管道——区别在于主力证明策略不同:

查询类型命题来源主力策略Fallback
类型等式类型检查器结构等价—
用户谓词程序员类型标注SMT程序员证明函数
借用冲突编译器自动生成DAG 结构分析(快速通道)SMT 逻辑切断

SMT 求解器在借用检查中的角色:不是主力,是安全网。 只在 while 回边需要逻辑切断时调用。绝大多数借用检查在快速通道完成——O(N) 反向 BFS,零 SMT 开销。

与现有代码的关系 ​

现有组件处理
BorrowChecker变为 BorrowPredicateEmitter——生成借用的霍尔命题
MoveChecker变为 MovePredicateEmitter——生成 ¬moved(v) 命题
DropChecker同上——生成 Drop 相关命题
MutChecker同上——生成 is_mut(v) 命题
ControlFlowAnalyzer不再需要——管道统一处理
liveness_analysis保留——Drop 插入仍需要变量活性信息
ir_gen.rs Release 硬编码删除——Release 位置由 DAG 消费者分析驱动

NLL 与迭代边界 ​

令牌活性是区间 [created_at, last_use],不是反向可达集。created_at = 令牌创建节点; last_use = 消费者分析的最大消费节点。写操作 W 与令牌 T 冲突的充分必要条件: conflicts(T, W) ∧ created_at(T) ≤ node(W) ∧ node(W) 可前向到达 last_use(T)(反向 BFS 判定)。 「写先、借用后」的合法顺序(§2.4:参数令牌调用结束释放)由 created_at(T) ≤ node(W) 直接排除,无需任何特殊规则。此模型使 §权衡优点 5「算法不保守」的声明在全部顺序下成立。

令牌死亡时刻 = 最后使用点(NLL),非词法作用域末尾。

这是消费者分析的自然推论:消费者的位置定义了令牌的最后使用。use(v) 是 v 的消费者 → v 在 use(v) 之后即刻死亡。不需要额外的 {} 或 drop() 来提前结束令牌生命。

循环迭代边界是令牌副本的死亡线。 三条规则:

规则 1:循环内声明的变量在每次迭代结束时自动死亡。
        for 的每次迭代是新绑定(语言设计保证),loop 同理。

规则 2:品牌树 ref_count 在循环头只计入循环外创建的副本。
        循环内 Dup 产生的新副本,ref_count 在迭代边界清零。

规则 3:反向 BFS 穿越回边时,不携带当前迭代的活性信息。
        只携带循环头处的 ref_count(即:循环外的副本)。

实例:

yaoxiang
view = &data                          # 循环头:ref_count = 1,consumer = use(view)
loop {
    v2: &Point = view                 # 循环内 Dup → ref_count = 2
    use(v2)                           # consumer:v2 的最后使用 → v2 死亡 → ref_count = 1
    data.push(4)                      # ✅ 安全!v2 已死,只剩 view(ref_count = 1,非写冲突)
    # 迭代边界:规则 3——不携带 v2 进入下一轮。下一轮迭代开始时 v2 被新绑定重新创建。
}

此设计不需要额外的"循环保守存活"规则。反向 BFS 从消费者出发,消费者在循环体内 → 活性被限制在当前迭代内 → 回边不穿越。与 RFC-009a §用例分析的循环示例完全一致。

? 错误传播与作用域驱动 Release ​

? 是提前返回——作用域的正常出口之外多了一条退出路径。令牌必须在这条路径上被释放,释放顺序错误即 UB。

Release 指令由作用域分析生成,不是硬编码在 Call 之后。

编译器为每个作用域维护出口点列表:

  • }(作用域正常结束)
  • ?(错误传播,提前 return)
  • 显式 return

在每个出口点,按声明逆序(LIFO)插入该作用域内所有活跃令牌的 Release 指令。品牌树的父子关系自动处理派生令牌的级联释放:

yaoxiang
Point.get_x: (self: &Point) -> (&Float, &Point) = {
    return (&self.x, self)    # 返回子令牌 &Float + 父令牌 &Point
}

fn use_case(p: Point) -> Result<(), Error> = {
    (x_ref, p_ref) = p.get_x()?   # 如果 ? 传播:
    # 品牌树知道 x_ref 是 p_ref 的派生(#42.field_x 是 #42 的前缀)
    # 释放顺序:x_ref(子)→ p_ref(父)→ LIFO 自动满足
    p.modify()                     # WriteToken——所有 ReadToken 已释放
    Ok(())
}

实现位置:保留在 ir_gen.rs,改为作用域驱动——不引入新 compiler pass。

| 冲突判断 | O(1) | 每次需要令牌 | | DAG 消费者查询 | O(1) | 每次需要令牌 | | 反向 BFS(快速通道) | O(N) | 每次需要令牌,N = 块内节点数 | | SMT 逻辑切断(fallback) | ~1ms | 极罕见——仅 while + 路径条件 |

上表复杂度为设计估算,未经实测;「~1ms」「极罕见」应视为量级预期而非测量值, 实现落地后以可观测性数据校准。

SMT fallback 的触发条件极为苛刻:同时满足 (1) while 循环 (2) 循环体内有写操作 (3) 写操作后有路径条件可判循环终止 (4) 编译器需要依赖该条件切断回边。实际代码中占比远低于 1%。其余的借用检查全部在快速通道完成。

和 RFC-027 用户谓词的关系:用户谓词以 SMT 为主力,借用系统谓词以结构分析为主力。两者共享同一 SMT 求解器和预算上限(RFC-027 §8),但借用系统谓词几乎不消耗 SMT 预算。

线性代码 → 无回边 → 分层 1 O(N) 秒出。循环 + 路径条件 → SMT 调用,线性算术毫秒级(RFC-027 预算 100ms)。一次 BFS 结果可缓存供同一令牌的多次查询复用。

错误信息设计 ​

核心原则:错误信息只出现用户写过的符号。

Rust 和借用相关的错误分两类:

变量级错误:E0597(活得不够长)、E0502(可变+不可变同时借用)、E0499(多次可变借用)。Rust 已经是标杆——变量名+行号,不出现 'a。YaoXiang 精确度持平。信息全在品牌树里:令牌创建点、消费者位置、请求点。

签名级错误:E0623(lifetime mismatch)、E0106(missing lifetime specifier)、E0477(不满足 required lifetime)。围绕 'a 展开。YaoXiang 不存在这类错误——签名里没有 'a。不是"报不出来",是用户没写过的东西不用报。

函数内冲突示例:

错误:`data` 被冻结,不能获取可变权限
 --> src/main.yx:5:9
2 |     view = &data
  |            ----- `data` 被冻结(只读令牌创建在此处)
4 |         use(view)
  |             ---- `view` 在此处仍在使用,冻结未解除
5 |         data.push(4)
  |         ^^^^ 此处需要可变权限

(与 Rust E0499 精确度持平——变量名+行号,不出现品牌 ID。)

函数间逃逸示例:

错误:`num`(第 4 行)持有的数据来源之一是 `default_str`(第 3 行),
但 `default_str` 在第 6 行失效,`num` 在第 5 行仍在被使用。

考虑:将 `default_str` 的声明提前到调用方,或使用 `ref default_str` 共享持有。

(与 Rust E0597 精确度持平。品牌摘要知道 num 有两条来源路径——编译器中已有,错误措辞可用。)


RFC-009 正文修正 ​

RFC-009 §"令牌冲突检测:流敏感活性分析"已更新:

  1. 删除"不需要的东西:……NLL"——不是因为结论错,是因为理由错("令牌是值,线性追踪就够了")
  2. 层 1/层 2 过渡方案保留,完整方案指向本 RFC
  3. 明确:品牌 ID(#42)就是 'a——信息完全一样,编码不同。不是发明了新分析——是把生命周期从类型层降到了证明层

权衡 ​

优点 ​

  1. 类型签名不含生命周期:#42 就是 '42——同一个信息,编码在品牌树里,不暴露在类型签名中。此点不可证伪:数一下 Rust 里有 3 个引用参数的泛型类型需要几个 'a 参数,YaoXiang 里需要几个。答案是 3 vs 0。

  2. 概念统一:借用检查和用户谓词共享同一条证明管道——{P} op {Q},管道验证 P。Curry-Howard 一致。

  3. 零新分析框架:不引入新分析框架。用户不感知"借用检查器"的存在——就像用户不感知"类型检查器"的实现细节。

  4. 错误信息只含用户写过的符号:少了一整个维度的错误类别(E0623、E0106、E0477——全部围绕 'a)。变量级错误与 Rust 精确度持平。

  5. 算法不保守:反向 BFS + break 切断 + SMT 逻辑切断。不需要"循环内保守存活"。不需要"分支保守合并"。

缺点 ​

  1. 不是新发明:品牌 ID 做的事和 'a 完全一样——编译器内部的约束求解复杂度没有消失,只是编码方式从"变量名+约束集"变成了"品牌路径+前缀匹配"。对终端用户的差异仅在于签名里不写 'a。

  2. 全新实现:品牌树在代码中仅存概念,需从头实现。BorrowChecker、ControlFlowAnalyzer 被替换。

  3. SMT 依赖:逻辑切断依赖 Z3(RFC-027 已引入,不新增依赖)。但借用检查几乎不触发——仅 while + 路径条件时调用。

  4. 极少数模式需重构:编译器自动证明覆盖不了的跨分支借用,用户需重构代码。和 Rust 'a 的兜底不同:Rust 有 'a 当笔(标注即能过);YaoXiang 的兜底(证明函数)不是 MVP。


替代方案 ​

方案为什么不选
实现完整 Rust NLLYaoXiang 的设计约束(无遮蔽、for 新绑定)已消除 NLL 主要复杂度来源,不需要 CFG 不动点
保持当前(硬编码 Release)不够——用户必须手动管理令牌作用域
只在 spawn 块做分析不够——非 spawn 代码中的令牌使用是大多数
GC 替代借用检查违反语言设计原则——YaoXiang 无 GC

实现阶段 ​

阶段内容依赖
Phase 1品牌树数据结构实现—
Phase 2系统谓词生成器(Borrow/Move/Drop/Mut → 命题)Phase 1
Phase 3反向 BFS 活性分析 + 管道接入(分层 1)Phase 2
Phase 4路径条件收集 + SMT 逻辑切断(分层 2)Phase 3 + RFC-027 Phase 2
Phase 5Release 指令改为 DAG 消费者驱动Phase 3
Phase 6删除 ControlFlowAnalyzer、重构 BorrowCheckerPhase 4

开放问题 ​

  • [x] 品牌树在循环展开时的 ref_count 跨迭代语义——走 NLL:令牌在最后使用后死亡。循环内绑定的副本在迭代边界死亡,反向 BFS 不携带跨迭代活性。详见 §NLL 与迭代边界。
  • [x] ? 错误传播路径上的令牌释放顺序——Release 由作用域分析驱动(保留在 ir_gen.rs)。每个作用域出口点(}、?、显式 return)按 LIFO 释放活跃令牌。品牌树父子关系自动处理级联释放。详见 §? 错误传播与作用域驱动 Release。
  • [ ] 证明函数语法(远期,非 MVP——不阻塞任何 Phase)

参考文献 ​


生命周期与归宿 ​

状态位置说明
已接受docs/design/rfc/accepted/成为正式设计文档