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:
@@ -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"))))
|
||||
|
||||
@@ -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![],
|
||||
}),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user