feat(lang): eliminate the Implicit ownership default — totality + the drop-soundness it demasks (#55)

Deletes `ParamMode::Implicit`. `ParamMode` is now `{Own, Borrow}`:
every fn-type slot on every signature carries an explicit `own` or
`borrow`, no defaulted position survives anywhere (model 0008 §2,
spec 0062). The parser rejects a bare fn-type slot; `borrow-return`
and `borrow-over-value` reject at the signature; the corpus is
migrated to minimal-ownership modes (consumed ⇒ own, read-only-heap
⇒ borrow, value ⇒ trivial-own). The documented `Implicit`-ret-mode
leak is fixed: an owned heap return now drops exactly once (live=0,
acceptance criterion 5).

This was the easy half. Removing the default ACTIVATED a family of
drop paths that `Implicit` had silently skipped — the pre-cutover
language was leaking (and in places mis-dropping) here rather than
crashing, because an Implicit scrutinee turned the drop off. Making
the modes explicit (Own) turned those paths on and exposed two
latent-bug clusters, all fixed RED-first as part of this cutover:

Drop-soundness family (four legs):
  A. lit-sub-pattern double-free — the desugar re-matched the same
     owned scrutinee in the lit fall-through; fixed by grouping
     consecutive same-ctor arms into one match (bind fields once),
     in ailang-core desugar.
  B. Cons-husk leak on non-tail arm bodies — the lit-sub-pattern
     desugar rebound the owned scrutinee via `Let $mp = xs`, which
     bumped consume_count and suppressed the existing fn-return
     partial_drop. Fixed by not rebinding a bare-Var scrutinee
     (one husk-freeing mechanism, not two).
  C. polymorphic `drop_<T>` rc_dec'd monomorphised value fields —
     the per-ADT drop fn was emitted once from the polymorphic
     TypeDef, defaulting type-var fields to ptr and rc_dec'ing
     inline Ints (segfault). Fixed with per-monomorph drop
     functions (new ailang-codegen::dropmono): the drop set is
     collected from the lowered MIR, value-type fields are skipped,
     heap fields still freed once; monomorphic-concrete ADTs keep
     their byte-identical un-suffixed drop symbol.
  D. static Str literal passed to an `(own Str)` param — the
     literal lowers to a header-less rodata constant; the callee's
     now-active rc_dec read its length field as a refcount and
     freed a static address (segfault). Fixed with the missing
     fourth StrRep::Static→Heap promotion in lower_to_mir's App arm,
     gated on Own mode (borrow args stay static, no regression).

over-strict-mode lint over-fired: it suggested `(borrow V)` for
value-typed params (which `borrow-over-value` rejects — own is the
only legal mode there) and fired on `(intrinsic)` bodies (whose
consumption the linearity walk cannot observe). Tightened to skip
both; contract 0008 updated to the narrowed firing scope.

Irreversible step — canonical-form hash reset (model 0008 §6,
acceptance criterion 6). Every signature now carries explicit modes,
so the hashable canonical JSON changed for every module. RATIFY:
the corpus-wide hash-pin reset (hash_pin, prelude_module_hash_pin,
mono_hash_stability, eq_ord_e2e, embed_export_hash_stable, the
ct4/iter*/loop_recur schema-extension pins) and the list ir_snapshot
golden were regenerated once, deliberately, as the intended one-time
consequence of removing the mode elision from the canonical form —
not a regression. Each regenerated hash verified deterministic across
two runs.

Also fixes a pre-existing latent failure surfaced by the verification
gate, unrelated to this cutover: the `every_contract_names_a_resolvable_
ratifying_test` resolver (design_index_pin) could not resolve the
" + " dual-link ratifying-test form (`uniqueness.rs + linearity.rs`)
that the #57 audit-close (dfdc65f) introduced — it shipped red on that
commit. Resolver taught the dual-link form, mirroring its sibling.

Verification: cargo test --workspace = 731 passed, 0 failed (twice,
stable); e2e 102 passed, no binary exits non-zero (corpus crash-free);
grep-clean for Implicit/fn_implicit/mode_eq across crates; every drop
fix confirmed via emitted IR + AILANG_RC_STATS balance on the head==K,
head!=K, and Nil paths. Three BLOCKEDs en route (the unsound first
husk-dec attempt, the over-strict derivation premise, the leg-B fix
direction) were each treated as a real design/spec gap and rediagnosed,
not patched over.

Supersedes #54 (return-position-only leak patch). Precondition #57
(linearity hardening) was already met. Spec docs/specs/0062, plan
docs/plans/0121.

closes #55
This commit is contained in:
2026-06-02 00:03:46 +02:00
parent 05c3c018de
commit 76b21c00eb
342 changed files with 3196 additions and 1503 deletions
+35 -40
View File
@@ -1177,26 +1177,18 @@ impl<'a> Parser<'a> {
effects = self.parse_effects_clause()?;
}
self.expect_rparen("fn-type")?;
// If every entry is Implicit, store as `vec![]` so canonical
// JSON serialisation omits the field — preserves pre-18a
// hashes for any fixture that still uses bare types.
let stored_modes = if param_modes.iter().all(|m| matches!(m, ParamMode::Implicit)) {
Vec::new()
} else {
param_modes
};
Ok(Type::Fn {
params,
ret: Box::new(ret),
effects,
param_modes: stored_modes,
param_modes,
ret_mode,
})
}
/// parse one fn-type slot — a type, optionally wrapped
/// in `(borrow T)` or `(own T)`. The mode is `Implicit` for a
/// bare type, `Borrow` for `(borrow T)`, `Own` for `(own T)`.
/// parse one fn-type slot — a type wrapped in `(borrow T)` or
/// `(own T)`. A bare type carries no mode and is rejected
/// (spec 0062): every slot must declare its ownership.
fn parse_param_with_mode(&mut self) -> Result<(Type, ParamMode), ParseError> {
if let Some(head) = self.peek_head_ident() {
match head {
@@ -1217,8 +1209,12 @@ impl<'a> Parser<'a> {
_ => {}
}
}
let t = self.parse_type()?;
Ok((t, ParamMode::Implicit))
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
Err(ParseError::Production {
production: "fn-type-slot",
message: "fn-type slot requires a mode: write (own T) or (borrow T)".into(),
pos,
})
}
fn parse_effects_clause(&mut self) -> Result<Vec<String>, ParseError> {
@@ -1942,7 +1938,7 @@ mod tests {
r#"
(module hello
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(type (fn-type (params) (ret (own (con Unit))) (effects IO)))
(params)
(body (do io/print_str "Hello, AILang."))))
"#,
@@ -1958,7 +1954,7 @@ mod tests {
r#"
(module m
(fn id
(type (fn-type (params (con Int)) (ret (con Int))))
(type (fn-type (params (own (con Int))) (ret (own (con Int)))))
(params x)
(body x)))
"#,
@@ -1969,9 +1965,8 @@ mod tests {
/// `(borrow T)` and `(own T)` wrappers in fn-type
/// param/ret slots round-trip into [`ParamMode::Borrow`] /
/// [`ParamMode::Own`] on `Type::Fn`. A bare type stays
/// [`ParamMode::Implicit`] (and its mode is elided from the
/// canonical form).
/// [`ParamMode::Own`] on `Type::Fn`. A bare slot is a parse
/// error (spec 0062): every slot carries an explicit mode.
#[test]
fn parses_borrow_and_own_modes_on_fn_type_slots() {
let m = parse(
@@ -1979,7 +1974,7 @@ mod tests {
(module m
(fn f
(type (fn-type
(params (borrow (con Int)) (con Bool))
(params (borrow (con Int)) (own (con Bool)))
(ret (own (con Int)))))
(params x y)
(body x)))
@@ -1994,8 +1989,8 @@ mod tests {
Type::Fn { param_modes, ret_mode, .. } => {
assert_eq!(
param_modes,
&vec![ParamMode::Borrow, ParamMode::Implicit],
"first param parsed as `(borrow ...)`, second as bare"
&vec![ParamMode::Borrow, ParamMode::Own],
"first param parsed as `(borrow ...)`, second as `(own ...)`"
);
assert_eq!(*ret_mode, ParamMode::Own, "ret parsed as `(own ...)`");
}
@@ -2032,7 +2027,7 @@ mod tests {
r#"
(module m
(fn id
(type (fn-type (params (con Int)) (ret (con Int))))
(type (fn-type (params (own (con Int))) (ret (own (con Int)))))
(params x)
(body (clone x))))
"#,
@@ -2059,7 +2054,7 @@ mod tests {
r#"
(module m
(fn id
(type (fn-type (params (con Int)) (ret (con Int))))
(type (fn-type (params (own (con Int))) (ret (own (con Int)))))
(params x)
(body (clone))))
"#,
@@ -2116,7 +2111,7 @@ mod tests {
r#"
(module m
(fn f
(type (fn-type (params (con Int)) (ret (con Int))))
(type (fn-type (params (own (con Int))) (ret (own (con Int)))))
(params x)
(body (reuse-as))))
"#,
@@ -2136,7 +2131,7 @@ mod tests {
r#"
(module m
(fn f
(type (fn-type (params (con Int)) (ret (con Int))))
(type (fn-type (params (own (con Int))) (ret (own (con Int)))))
(params x)
(body (reuse-as x))))
"#,
@@ -2258,12 +2253,12 @@ mod tests {
r#"
(module m
(fn main
(type (fn-type (params) (ret (con Int))))
(type (fn-type (params) (ret (own (con Int)))))
(params)
(body
(let-rec f
(params x)
(type (fn-type (params (con Int)) (ret (con Int))))
(type (fn-type (params (own (con Int))) (ret (own (con Int)))))
(body x)
(in (app f 1))))))
"#,
@@ -2300,7 +2295,7 @@ mod tests {
(module t
(fn f
(suppress (code "over-strict-mode") (because "test reason"))
(type (fn-type (params) (ret (con Int))))
(type (fn-type (params) (ret (own (con Int)))))
(params)
(body 0)))
"#,
@@ -2327,7 +2322,7 @@ mod tests {
(fn f
(suppress (code "over-strict-mode") (because "first"))
(suppress (code "other-code") (because "second"))
(type (fn-type (params) (ret (con Int))))
(type (fn-type (params) (ret (own (con Int)))))
(params)
(body 0)))
"#,
@@ -2354,7 +2349,7 @@ mod tests {
r#"
(module t
(fn f
(type (fn-type (params) (ret (con Int))))
(type (fn-type (params) (ret (own (con Int)))))
(params)
(body 0)))
"#,
@@ -2381,7 +2376,7 @@ mod tests {
(module t
(fn f
(suppress (code "over-strict-mode") (because ""))
(type (fn-type (params) (ret (con Int))))
(type (fn-type (params) (ret (own (con Int)))))
(params)
(body 0)))
"#,
@@ -2406,7 +2401,7 @@ mod tests {
(module t
(fn f
(suppress (code "over-strict-mode") (because "ok") (oops "x"))
(type (fn-type (params) (ret (con Int))))
(type (fn-type (params) (ret (own (con Int)))))
(params)
(body 0)))
"#,
@@ -2425,7 +2420,7 @@ mod tests {
(class Foo
(param a)
(method m
(type (fn-type (params (con a)) (ret (con Int)))))))"#;
(type (fn-type (params (own (con a))) (ret (own (con Int))))))))"#;
let m = parse(src).expect("parse ok");
assert_eq!(m.defs.len(), 1, "one def");
match &m.defs[0] {
@@ -2451,7 +2446,7 @@ mod tests {
(superclass (class Foo) (type a))
(doc "bar extends foo")
(method m
(type (fn-type (params (con a)) (ret (con Int))))
(type (fn-type (params (own (con a))) (ret (own (con Int)))))
(default 0))))"#;
let m = parse(src).expect("parse ok");
let c = match &m.defs[0] {
@@ -2517,7 +2512,7 @@ mod tests {
(fn f
(doc "first")
(doc "second")
(type (fn-type (params) (ret (con Unit))))
(type (fn-type (params) (ret (own (con Unit)))))
(params)
(body (do io/print_str "x"))))
"#,
@@ -2536,8 +2531,8 @@ mod tests {
r#"
(module m
(fn f
(type (fn-type (params) (ret (con Unit))))
(type (fn-type (params) (ret (con Unit))))
(type (fn-type (params) (ret (own (con Unit)))))
(type (fn-type (params) (ret (own (con Unit)))))
(params)
(body (do io/print_str "x"))))
"#,
@@ -2556,7 +2551,7 @@ mod tests {
r#"
(module m
(fn f
(type (fn-type (params) (ret (con Unit))))
(type (fn-type (params) (ret (own (con Unit)))))
(params)
(params)
(body (do io/print_str "x"))))
@@ -2576,7 +2571,7 @@ mod tests {
r#"
(module m
(fn f
(type (fn-type (params) (ret (con Unit))))
(type (fn-type (params) (ret (own (con Unit)))))
(params)
(body (do io/print_str "x"))
(body (do io/print_str "y"))))
+10 -12
View File
@@ -351,13 +351,11 @@ fn write_instance_method(out: &mut String, m: &InstanceMethod, level: usize) {
// ---- types ----------------------------------------------------------------
/// print one fn-type param/ret slot, wrapping with
/// `(borrow ...)` or `(own ...)` when the slot has an explicit
/// mode. `Implicit` is printed bare so pre-18a fixtures round-trip
/// unchanged.
/// print one fn-type param/ret slot, always wrapping with
/// `(own ...)` or `(borrow ...)` — every slot carries an explicit
/// mode (spec 0062).
fn write_fn_type_slot(out: &mut String, t: &Type, mode: ParamMode) {
match mode {
ParamMode::Implicit => write_type(out, t),
ParamMode::Own => {
out.push_str("(own ");
write_type(out, t);
@@ -405,7 +403,7 @@ fn write_type(out: &mut String, t: &Type) {
out.push_str("(fn-type (params");
for (i, p) in params.iter().enumerate() {
out.push(' ');
let mode = param_modes.get(i).copied().unwrap_or(ParamMode::Implicit);
let mode = param_modes.get(i).copied().unwrap_or(ParamMode::Own);
write_fn_type_slot(out, p, mode);
}
out.push_str(") (ret ");
@@ -771,9 +769,9 @@ mod tests {
name: "m".into(),
ty: Type::Fn {
params: vec![Type::Var { name: "a".into() }],
param_modes: vec![ParamMode::Implicit],
param_modes: vec![ParamMode::Own],
ret: Box::new(Type::int()),
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
effects: vec![],
},
default: None,
@@ -802,9 +800,9 @@ mod tests {
name: "m".into(),
ty: Type::Fn {
params: vec![Type::Var { name: "a".into() }],
param_modes: vec![ParamMode::Implicit],
param_modes: vec![ParamMode::Own],
ret: Box::new(Type::int()),
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
effects: vec![],
},
default: Some(Term::Lit { lit: Literal::Int { value: 0 } }),
@@ -832,12 +830,12 @@ mod tests {
}],
body: Box::new(Type::Fn {
params: vec![Type::Var { name: "a".into() }],
param_modes: vec![ParamMode::Implicit],
param_modes: vec![ParamMode::Own],
ret: Box::new(Type::Con {
name: "Str".into(),
args: vec![],
}),
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
effects: vec![],
}),
},