Appearance
2026-03-26 P3.3: Cleanup-Aware Ownership Facts
背景
P3.1 已经把 defer lower 成了显式 cleanup registration 与 RunCleanup 执行点,P3.2 也已经建立了 MIR local 上的 moved-vs-usable facts。
但当时有一个刻意留下的缺口:
- borrowck 只分析普通 MIR statement / terminator
RunCleanup还没有真正参与 ownership analysis
这会带来一个明显问题:
ql
defer user.name
defer user.into_json()实际执行顺序是后注册的 cleanup 先运行,也就是:
user.into_json()先消费useruser.name再读取user
如果 borrowck 不理解 cleanup 执行,这类 use-after-move 会直接漏报。
所以 P3.3 的第一步不该直接冲向完整 borrow / escape analysis,而应先把 ownership facts 接到 cleanup runtime order 上。
本轮目标
本轮只解决 cleanup-aware ownership,不宣称“borrow checker 已完成”。
交付范围:
RunCleanup会真实参与 ownership analysis- deferred expression 的 local read / consume / root-write 会影响后续 cleanup
move self调用的消费时机改为“参数求值之后”- 对 deferred cleanup 里的 moved use 给出更可解释的诊断说明
- 为后续 escape / closure capture / drop elaboration 预留干净扩展点
不做的事情
- 通用 borrow graph
- closure capture ownership
- nested
deferinside deferred cleanup 的完整 runtime 模型 - 完整 loop-sensitive cleanup execution reasoning
- projection-sensitive partial move
- drop elaboration
这些能力都需要更多语义基础,但不应阻塞 cleanup 进入 ownership facts。
设计原则
1. 不把 cleanup 再次 lower 成另一套隐式 IR
当前 MIR 已经有:
CleanupActionRegisterCleanupRunCleanup
所以这轮不新增第二套 cleanup IR,而是在 borrowck 里对 deferred expr 做受控的 HIR-level effect walk。
这样做的好处:
- 不污染
ql-mir当前形态 - 逻辑集中在
ql-borrowck - 后续如果要把 cleanup expr 真正 elaboration 成 MIR body,也不会影响当前对外接口
2. effect walker 只产出 ownership 相关效果
cleanup walker 不做 full interpreter,只关心三类效果:
- read local
- consume local
- root-write local
原因很简单:这轮关注的是 local 可用性,而不是值计算结果。
3. direct-local 约束继续保持
P3.2 里已经明确:
- 只对 direct local receiver 的
move self做消费建模
P3.3 不打破这个边界。cleanup 中的 foo.bar.into_json() 仍然不做部分 move 推理,避免把 place-sensitive 规则偷偷混进当前实现。
代码设计
BodyAnalyzer 扩展
为了让 cleanup walker 能把 HIR name resolution 映射回 MIR local,需要在 body 级预先建立反查表:
binding_locals: HIR LocalId -> MIR LocalIdparam_locals: param index -> MIR LocalIdreceiver_local: Option<MIR LocalId>
这样 cleanup walker 只要拿到 ValueResolution,就能落回当前 body 的 tracked local。
UseSite
普通 use 和 deferred cleanup use 的诊断文案不应完全一样。
因此本轮引入 UseSite:
spanlabelnote
普通路径:
- label:
use here
deferred cleanup:
- label:
used here when deferred cleanup runs - note:
deferred cleanup executes on scope exit in LIFO order
这样用户在看 diagnostic 时能直接理解“为什么一个看上去较早写下的表达式会在 move 之后才执行”。
apply_consume 与消费时机修正
P3.2 的实现里,move self call 会先把 receiver 标成 moved,再分析参数。
这会制造一个假阳性:
ql
user.rename(user.name)receiver 的消费应该发生在 call boundary,而不是参数求值之前。
所以本轮把调用分析拆成两步:
- classify pending consume
- 先分析参数
- 最后再
apply_consume
同时:
- 如果 local 之前已经是
Moved apply_consume不再粗暴覆盖旧状态- 会保留并合并 origins
这样后续多次消费和路径汇合也更稳定。
cleanup effect walker
本轮新增一组私有 helper:
eval_cleanup_expreval_cleanup_blockeval_cleanup_stmteval_cleanup_assign_target
核心返回值是:
statescontinues
也就是:
- 当前 deferred expr 执行到这里后的 local states
- 这条路径是否还会继续执行后续 statement
这样可以最低成本支持:
- block
ifmatch- straight-line stmt 序列
并对 return / break / continue 做基本截断。
branch merge
cleanup 里的分支不会像 MIR body 那样天然已有 CFG,所以这轮用 borrowck 内部 merge helper 做合流:
- 两边都继续执行 -> merge states
- 只有一边继续 -> 后续只沿继续执行的一边传播
- 两边都终止 -> cleanup eval 标记为 stop
这让下面这种 deferred conditional consume 能稳定得到 maybe moved:
ql
defer user.name
defer if flag { user.into_json() } else { "" }root write 与重建可用性
cleanup 里对 direct local root 的赋值必须能恢复 local 可用性,否则会误报:
ql
defer user.name
defer { user = fresh_user(); "" }
defer user.into_json()实际执行:
- consume
user - root-write
user - 读取新的
user
所以 eval_cleanup_assign_target 会区分:
- direct local root assignment
- projection assignment
只有 root assignment 才会把 local state 写回 Available。
当前刻意保留的限制
本轮仍然不试图过度承诺:
- nested deferred cleanup registration 目前只跳过,不做 runtime modeling
- closure capture 在 cleanup expr 内仍不建模
while/for/loop只做保守单次/merge 近似- projection write 不会被当作 root reinitialization
这些都已经被限制在私有 cleanup walker 中,后续替换成本可控。
测试策略
本轮新增并锁定的回归点:
- move receiver 在参数求值后才消费
- deferred cleanup 的 LIFO 顺序会导致 use-after-move
- deferred conditional consume 会产生
maybe moved - deferred root-write 会让后续 cleanup read 重新可用
- ownership render 能看到 cleanup 带来的 read / consume 事件
ql-analysis会聚合 deferred cleanup diagnostics
对后续 P3.3 / P3.4 的价值
这轮完成后,P3 不再只有“普通语句上的 ownership facts”,而是真正开始覆盖:
- scope exit
- deferred execution
- runtime order
这直接为后面的几件事打底:
- cleanup capture / escape analysis
- drop elaboration
- 更一般的 call contract
- closure / async suspend-point 的 owned value tracking
结论
P3.3 的第一步不应是“写一个看起来很厉害但规则全是假的 borrow checker”。
更正确的做法,是先把已经存在的 cleanup runtime order 接进 ownership facts。
只有这样,后面的 borrow / escape / drop 才会建立在真实执行边界之上,而不是继续在语义空洞上堆规则。