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
+5 -3
View File
@@ -1897,10 +1897,12 @@ mod tests {
);
}
/// All-Implicit fn → check is skipped, no diagnostics, even if the
/// body would trigger a use-after-consume under explicit modes.
/// A single `Own` param consumed exactly once (the body returns it)
/// is clean — no `over-strict-mode`, no use-after-consume. (Pre-0062
/// this was the `Implicit`-exemption case; post-cutover there is no
/// `Implicit`, so it pins the ordinary single-consume path.)
#[test]
fn implicit_fn_is_exempt() {
fn own_param_consumed_once_is_clean() {
let m = Module {
schema: ailang_core::SCHEMA.into(),
name: "t".into(),
+27 -45
View File
@@ -37,17 +37,21 @@
//! arms returned a non-Unit type to carry a synthetic `_` arm
//! dominating the terminator. The polymorphic `__unreachable__`
//! removes that workaround.
//! 4. Each arm is lowered via `desugar_one_arm`:
//! - `Pattern::Wild` → arm body (catch-all; later arms are dropped).
//! - `Pattern::Var { name }` → `Let { name = scrutinee_var; body }`.
//! - `Pattern::Lit { lit }` → `Term::If { cond = (== s_var lit),
//! then = arm.body, else_ = fall_k }` (Iter 16c). After this
//! 4. `build_chain` lowers the arms head-first:
//! - A maximal run of consecutive arms sharing the head arm's outer
//! ctor + arity is lowered together via `build_ctor_group`: lift
//! each ctor field to a fresh var, emit ONE `Term::Match` over the
//! flat ctor pattern (binding the fields once) plus a wildcard
//! fall-through, and thread the bound vars through every arm's
//! sub-patterns via `wrap_sub`. Binding once keeps the ctor's
//! owned children under a single match scope (no re-match, no
//! re-drop).
//! - A non-ctor head (Wild/Var/Lit) is lowered via `desugar_one_arm`:
//! `Pattern::Wild` → arm body (catch-all; later arms are dropped);
//! `Pattern::Var { name }` → `Let { name = scrutinee_var; body }`;
//! `Pattern::Lit { lit }` → `Term::If { cond = (== s_var lit),
//! then = arm.body, else_ = fall_k }` (Iter 16c). After the Lit
//! rewrite, no `Pattern::Lit` reaches typecheck or codegen.
//! - `Pattern::Ctor { ctor, fields }`: lift each field to a fresh
//! var, build a flat outer pattern, walk fields right-to-left
//! wrapping inner sub-patterns via `wrap_sub`. Emit a
//! `Term::Match` with two arms: the flat ctor and a wildcard
//! fall-through into the rest of the chain.
//!
//! `wrap_sub(fv, sub, body, fall_k)` recurses on `sub`:
//! - `Var(n)` → `Let(n, Var(fv), body)`.
@@ -1259,10 +1263,11 @@ impl Desugarer {
}
}
/// Lowers one arm into a term. Wild/Var arms drop the chain (the
/// arm matches everything); Lit and Ctor arms emit a `Term::Match`
/// with the desugared head pattern as the first arm and a
/// wildcard fall-through to `fall_k`.
/// Lowers one non-constructor arm into a term. Wild/Var arms drop
/// the chain (the arm matches everything); a Lit arm emits a
/// `Term::If` discriminating on `(== s_var lit)` with `fall_k` as
/// the else-branch. Ctor-head arms never reach here — `build_chain`
/// routes them through `build_ctor_group` (bind-once lowering).
fn desugar_one_arm(&mut self, s_var: &Term, arm: &Arm, fall_k: Term) -> Term {
match &arm.pat {
Pattern::Wild => arm.body.clone(),
@@ -1282,37 +1287,14 @@ impl Desugarer {
else_: Box::new(fall_k),
}
}
Pattern::Ctor { ctor, fields } => {
// Lift each field to a fresh var; build the flat outer
// pattern, then walk right-to-left wrapping each inner
// sub-pattern via `wrap_sub` so the deepest field is
// matched first inside-out.
let fresh_vars: Vec<String> = fields.iter().map(|_| self.fresh()).collect();
let flat_fields: Vec<Pattern> = fresh_vars
.iter()
.map(|n| Pattern::Var { name: n.clone() })
.collect();
let mut inner = arm.body.clone();
for (sub, fv) in fields.iter().zip(fresh_vars.iter()).rev() {
inner = self.wrap_sub(fv, sub, inner, &fall_k);
}
Term::Match {
scrutinee: Box::new(s_var.clone()),
arms: vec![
Arm {
pat: Pattern::Ctor {
ctor: ctor.clone(),
fields: flat_fields,
},
body: inner,
},
Arm {
pat: Pattern::Wild,
body: fall_k,
},
],
}
}
// `build_chain` routes every Ctor-head arm — including a
// length-1 run — through `build_ctor_group`, whose bind-once
// lowering is the live path. `desugar_one_arm` therefore only
// ever sees Wild/Var/Lit heads.
Pattern::Ctor { .. } => unreachable!(
"build_chain routes Ctor-head arms through build_ctor_group; \
desugar_one_arm sees only Wild/Var/Lit"
),
}
}