audit(0064/cutover): record shipped drop machinery + retire dead desugar arm

Cycle-close tidy for the #55 cutover (its audit drift-resolution).
Architect drift review found the design ledger out of step with the
codegen that the cutover shipped, plus two debt items; regression green
(733 passed / 0 failed across the workspace).

design/contracts/0008-memory-model.md — the Codegen contract described
the drop-emission *gates* but not the drop machinery the cutover landed.
Added three subsections recording the present state (honesty rule):
- Per-monomorph drop fns for polymorphic ADTs (crates/ailang-codegen/
  src/dropmono.rs: collect_drop_monos / DropAdtMeta, the suffix scheme,
  value-field-skip on the substituted field type). Ratified by
  alloc_rc_value_type_field_is_not_rc_dec_dropped.
- String-literal rep promotion at owned drop sites (StrRep::Static→Heap
  on a Str literal flowing into an Own slot the callee drops; gated
  strictly on Own). Ratified by own_str_literal_arg_is_dropped_exactly_once.
- Same-constructor arm grouping in match desugar (build_chain /
  build_ctor_group bind ctor fields once). Ratified by
  lit_pat_ctor_tail_drop_single_drop + lit_pat_nil_scrutinee_single_drop.

crates/ailang-core/src/desugar.rs — build_chain routes every Ctor-head
arm (including a length-1 run) through build_ctor_group, so
desugar_one_arm's Pattern::Ctor arm was unreachable dead code
duplicating the bind-once lowering. Replaced it with unreachable! naming
the invariant, and corrected two stale doc comments (the module-header
step 4 and the desugar_one_arm doc still described it as handling ctor
arms, and mislabeled the Lit lowering as Term::Match — it is Term::If).
Full workspace stayed green, confirming the arm was dead.

crates/ailang-check/src/linearity.rs — renamed test implicit_fn_is_exempt
→ own_param_consumed_once_is_clean. Post-0062 there is no Implicit and no
exemption; the test pins the ordinary single-Own-consume clean path.

Did the desugar + linearity edits inline rather than via an agent: I had
already loaded build_chain/build_ctor_group/desugar_one_arm to prove the
arm dead, and a sub-agent would have redone the same reading.
This commit is contained in:
2026-06-02 00:45:06 +02:00
parent b129af1363
commit 19757480b7
3 changed files with 99 additions and 48 deletions
+67
View File
@@ -324,6 +324,73 @@ returns that are not `Type::Con` (e.g. unresolved type vars on
a polymorphic call's pre-monomorphisation site; the
monomorphised copies resolve to concrete drop fns).
#### Per-monomorph drop fns for polymorphic ADTs
A polymorphic ADT (`Box a`, `Pair a b`) has no single correct
drop fn: a ctor field typed at a type-var lowers to `ptr` and is
`rc_dec`'d, but at a value-type instantiation (`Box Int`) that
field is an inline `i64`, so dec'ing it dereferences a scalar.
Codegen therefore emits **one drop fn per (polymorphic-ADT,
concrete instantiation)** and makes the dec-vs-skip decision on
the *substituted* field type: a value-type field
(`Int`/`Bool`/`Float`/`Unit`) is skipped (inline scalar, no RC);
a heap field is dec'd through its own per-monomorph drop symbol
so the cascade composes.
The workspace-global collection lives in
`crates/ailang-codegen/src/dropmono.rs` (`collect_drop_monos`,
`DropAdtMeta`), built once before the per-module codegen loop and
shared by reference with every `Emitter`. Symbol naming:
- A concrete-monomorphic ADT (declared `vars` empty: `IntList`,
`Ordering`) keeps its un-suffixed `drop_<m>_<T>` /
`partial_drop_<m>_<T>` symbol byte-for-byte (the `ir_snapshot`
goldens pin these).
- A polymorphic ADT instantiation gets a mono suffix mangled
exactly as `ailang_check::mono::mono_symbol_n` mangles fn
symbols (`drop_<m>_Pair__Int_Int`; compound args hash-route to
stay bounded).
- Intrinsic-storage types (`RawBuf`) are polymorphic but use the
flat intrinsic drop path and are NOT suffixed (their drop is
element-type independent; the golden pins `drop_<m>_RawBuf`).
A drop fn and its call sites consult the same `DropAdtMeta`
(emission + manglers in `drop.rs`, call-site manglers in
`match_lower.rs`), so they always agree on the symbol — a
mismatch would be a link error. Ratified by
`alloc_rc_value_type_field_is_not_rc_dec_dropped` in
`crates/ail/tests/drop_value_field_no_segfault_pin.rs`.
#### String-literal rep promotion at owned drop sites
A `Str` literal carries a `StrRep`: `Static` (header-less rodata
constant) or `Heap` (rc-headered owned slab). An `Own`-mode drop
path emits `ailang_rc_dec` on the value; dec'ing a
`StrRep::Static` reads `payload - 8` (the length field) as a fake
refcount and `free()`s a static address — a segfault. Codegen
therefore promotes a `Str` literal that flows into an `Own` slot
the callee will drop from `Static` to `Heap` (codegen then emits
`ailang_str_clone` → refcount 1, and the single dec is sound).
The promotion is gated **strictly on `Own`**: a `Borrow` slot
fires no `rc_dec`, so a borrow-position literal stays
`StrRep::Static` (no spurious clone).
`crates/ailang-check/src/lower_to_mir.rs` applies this at four
sites — the `Term::App` owned-arg site (`is_str_ty` + `Own`) and
the loop-carried seed/tail/recur sites of a `Str`-returning loop
body. Ratified by `own_str_literal_arg_is_dropped_exactly_once`
in `crates/ail/tests/e2e.rs`.
#### Same-constructor arm grouping in match desugar
`crates/ailang-core/src/desugar.rs` groups consecutive match
arms that share a head constructor (`build_chain` /
`build_ctor_group`) into a single match over that constructor's
fields, binding the fields once. This keeps a literal sub-pattern
(`(Cons (pat-lit K) _)`) from re-matching — and re-dropping — the
same owned scrutinee tail across the literal-discriminating arms.
Ratified by `lit_pat_ctor_tail_drop_single_drop` and
`lit_pat_nil_scrutinee_single_drop` in `crates/ail/tests/e2e.rs`.
#### Arg-position policy for compound AST nodes
The uniqueness and linearity passes walk arguments of compound