docs(contracts): record the leak-class branch-param drop gate (#63)
Audit of the #63 leg-3 cycle flagged ledger drift: 0008-memory-model.md enumerated the Own-param drop gates as a closed set, but the leak-class branch-param fall-through drop now ships in codegen with no home in the contract. Per the honesty-rule (a contract describes the actual present state), reconcile it. Adds the fourth gate with its three soundness guards — disjointness from the fn-return dec (partition by aggregate: ==0 vs >=1), no-use-after-free (the use-after-consume rejection makes an aggregate>=1 param dead past the construct), and the heap-RC-ADT type precondition (field_drop_call != ailang_rc_dec; closure/static-Str/Var params are skipped to avoid a static-constant underflow). Notes that the pre-tail-call Own-param dec now shares the per-branch model (gate-source = MArm.consume) and updates the binder-name-injectivity precondition to cover the per-branch consume maps carried on MArm / MTerm::If and correlated by the traversal-order cursor. The accepted heap-capturing-closure-in-leak-class leak (soundness over completeness) is recorded in the backlog as Brummel/AILang#67. refs #63
This commit is contained in:
@@ -279,12 +279,51 @@ emit drop calls:
|
|||||||
the requirement that every ptr-typed slot in the active
|
the requirement that every ptr-typed slot in the active
|
||||||
ctor's pattern is in `moved_slots[scrutinee]`.
|
ctor's pattern is in `moved_slots[scrutinee]`.
|
||||||
|
|
||||||
|
- **Leak-class branch-param drop.** When a `match`-arm or
|
||||||
|
`if`-branch falls through (no tail-call) and an `Own`, ptr-typed
|
||||||
|
parameter is LIVE on that branch (its per-branch consume is `0`)
|
||||||
|
but CONSUMED on a sibling branch (per-fn aggregate
|
||||||
|
`consume_count >= 1`), it is dec'd at that branch's close, before
|
||||||
|
the `br` to the join — never at the join, which is a merge point
|
||||||
|
that cannot tell the consumed path from the live one. Three guards
|
||||||
|
make this sound:
|
||||||
|
- *Disjointness from the fn-return dec.* The fn-return Own-param
|
||||||
|
dec above owns the `aggregate == 0` class (live on every path);
|
||||||
|
this gate owns the `aggregate >= 1` class. The predicates are
|
||||||
|
mutually exclusive, so no param is dropped twice — the fn-return
|
||||||
|
dec is left unchanged and no tail-position analysis is required.
|
||||||
|
- *No use-after-free.* An `aggregate >= 1` param is provably dead
|
||||||
|
past the branch construct: the checker rejects use-after-consume
|
||||||
|
(`use-after-consume`), so a param consumed on any branch cannot
|
||||||
|
be used afterwards. The per-branch drop is therefore
|
||||||
|
path-terminal regardless of whether the construct sits in tail
|
||||||
|
position.
|
||||||
|
- *Heap-RC-ADT type precondition.* The drop fires only for a param
|
||||||
|
whose static type resolves to a real per-type heap drop fn
|
||||||
|
(`field_drop_call != "ailang_rc_dec"`). Closure (`Type::Fn`),
|
||||||
|
static-`Str`, and `Type::Var` params route to the bare
|
||||||
|
`ailang_rc_dec`, which would underflow on a static closure-pair /
|
||||||
|
static-`Str` constant (`.rodata`, no rc-header); they are
|
||||||
|
skipped. A heap-capturing closure in this exact position leaks
|
||||||
|
rather than double-frees — soundness is preferred over
|
||||||
|
completeness (tracked in the backlog).
|
||||||
|
|
||||||
|
The pre-tail-call Own-param dec (the drop of an `Own` param a
|
||||||
|
tail-call arm neither forwards nor consumes) shares this
|
||||||
|
per-branch model: its gate-source is the per-arm `consume_count`
|
||||||
|
(`MArm.consume`), which for an `aggregate == 0` param is identical
|
||||||
|
to the old aggregate gate.
|
||||||
|
|
||||||
**Per-fn binder-name injectivity (a precondition of every gate
|
**Per-fn binder-name injectivity (a precondition of every gate
|
||||||
above).** All three drop gates read `consume_count` from the
|
above).** Every drop gate reads `consume_count` from the uniqueness
|
||||||
uniqueness side-table keyed by `(def_name, binder_name)`. Codegen
|
side-table keyed by `(def_name, binder_name)`; the two per-branch
|
||||||
looks up by the binder's source name and must resolve the binding it
|
gates (leak-class drop, pre-tail-call Own-param dec) additionally read
|
||||||
means — so within one `def_name`, every `binder_name` must denote
|
the per-branch consume map carried on `MArm.consume` /
|
||||||
exactly one binding. The desugar pass guarantees this: it
|
`MTerm::If.{then,else}_consume`, correlated to the MIR branch nodes by
|
||||||
|
a traversal-order lock-step cursor in `lower_to_mir` (the AST carries
|
||||||
|
no node id). Codegen looks up by the binder's source name and must
|
||||||
|
resolve the binding it means — so within one `def_name`, every
|
||||||
|
`binder_name` must denote exactly one binding. The desugar pass guarantees this: it
|
||||||
alpha-renames any binder whose name shadows an enclosing binding to a
|
alpha-renames any binder whose name shadows an enclosing binding to a
|
||||||
fresh `<name>$<n>` (`ailang-core::desugar`), so a shadow-rebind idiom
|
fresh `<name>$<n>` (`ailang-core::desugar`), so a shadow-rebind idiom
|
||||||
like `(let buf (new…) (let buf (set buf…) … (get buf)))` becomes
|
like `(let buf (new…) (let buf (set buf…) … (get buf)))` becomes
|
||||||
|
|||||||
Reference in New Issue
Block a user