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
+7 -7
View File
@@ -65,7 +65,7 @@ pub fn install(env: &mut crate::Env) {
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Implicit,
ret_mode: ailang_core::ast::ParamMode::Own,
}),
};
// 2026-05-21 operator-routing-eq-ord: the `poly_a_a_to_bool`
@@ -78,7 +78,7 @@ pub fn install(env: &mut crate::Env) {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Implicit,
ret_mode: ailang_core::ast::ParamMode::Own,
};
for op in ["+", "-", "*", "/"] {
env.globals.insert(op.into(), poly_a_a_to_a());
@@ -98,7 +98,7 @@ pub fn install(env: &mut crate::Env) {
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Implicit,
ret_mode: ailang_core::ast::ParamMode::Own,
},
);
@@ -134,7 +134,7 @@ pub fn install(env: &mut crate::Env) {
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Implicit,
ret_mode: ailang_core::ast::ParamMode::Own,
}),
},
);
@@ -145,7 +145,7 @@ pub fn install(env: &mut crate::Env) {
ret: Box::new(Type::float()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Implicit,
ret_mode: ailang_core::ast::ParamMode::Own,
},
);
env.globals.insert(
@@ -155,7 +155,7 @@ pub fn install(env: &mut crate::Env) {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Implicit,
ret_mode: ailang_core::ast::ParamMode::Own,
},
);
env.globals.insert(
@@ -218,7 +218,7 @@ pub fn install(env: &mut crate::Env) {
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Implicit,
ret_mode: ailang_core::ast::ParamMode::Own,
},
);
+121 -92
View File
@@ -782,6 +782,13 @@ pub enum CheckError {
#[error("borrow-return not permitted for `{0}`: a (borrow …) return is refcount-invisible and can outlive its source; this language version forbids it (the escape/liveness axis that would make it sound is not yet built)")]
BorrowReturnNotPermitted(String),
/// A fn-type slot is `(borrow V)` where `V` is an unboxed value
/// type (`Int`/`Bool`/`Float`/`Unit`). Borrow is meaningless over
/// a value type — it has no refcount and is copied by value
/// (model §3.2). Code: `borrow-over-value`.
#[error("borrow over value type in `{def}`: `{ty}` is an unboxed value type and cannot be borrowed; use `(own {ty})`")]
BorrowOverValueType { def: String, ty: String },
/// an internal invariant in the typechecker / mono pass
/// was violated — surfaced as an error so callers can propagate
/// rather than abort, but in well-formed inputs (typecheck has
@@ -844,6 +851,7 @@ impl CheckError {
CheckError::ParamNotInRestrictedSet { .. } => "param-not-in-restricted-set",
CheckError::IntrinsicOutsideKernelTier { .. } => "intrinsic-outside-kernel-tier",
CheckError::BorrowReturnNotPermitted(_) => "borrow-return-not-permitted",
CheckError::BorrowOverValueType { .. } => "borrow-over-value",
CheckError::Internal(_) => "internal",
}
}
@@ -2109,7 +2117,7 @@ fn check_instance(
.collect();
let subst_ret_ty = substitute_rigids(ret_ty, &subst_map);
let subst_body = substitute_rigids_in_term(body, &subst_map);
let fn_ty = Type::fn_implicit(
let fn_ty = Type::fn_owned(
subst_param_tys,
subst_ret_ty,
effects.clone(),
@@ -2279,10 +2287,14 @@ fn check_fn(
other => (vec![], other.clone()),
};
let (param_tys, ret_ty, ret_mode, declared_effs) = match &inner_ty {
Type::Fn { params, ret, ret_mode, effects, .. } => {
(params.clone(), (**ret).clone(), *ret_mode, effects.clone())
}
let (param_tys, param_modes, ret_ty, ret_mode, declared_effs) = match &inner_ty {
Type::Fn { params, param_modes, ret, ret_mode, effects } => (
params.clone(),
param_modes.clone(),
(**ret).clone(),
*ret_mode,
effects.clone(),
),
other => {
return Err(CheckError::FnTypeRequired(
f.name.clone(),
@@ -2321,6 +2333,23 @@ fn check_fn(
return Err(CheckError::BorrowReturnNotPermitted(f.name.clone()));
}
// spec 0062: borrow over an unboxed value type is meaningless.
// Fired on the signature, before body dataflow. The mono-coercion
// (subst.rs) guarantees no generated value-type-borrow instance
// reaches here.
for (ty, mode) in param_tys.iter().zip(param_modes.iter()) {
if matches!(mode, ParamMode::Borrow) {
if let Type::Con { name, args } = ty {
if args.is_empty() && ailang_core::primitives::is_value_type(name) {
return Err(CheckError::BorrowOverValueType {
def: f.name.clone(),
ty: name.clone(),
});
}
}
}
}
// An `(intrinsic)` body is signature-only: the signature is already
// validated above; codegen supplies the implementation via the
// intercept registry. The body is `Term::Intrinsic` directly (a
@@ -4168,7 +4197,7 @@ pub(crate) fn synth(
ret: Box::new((**ret_ty).clone()),
effects: lam_effects.clone(),
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
})
}
Term::LetRec { name, ty, params, body, in_term } => {
@@ -5485,7 +5514,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec!["a", "b"],
Term::App {
@@ -5515,7 +5544,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::Lit {
@@ -5542,7 +5571,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec![], // !IO missing,
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::Do {
@@ -5575,7 +5604,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::Let {
@@ -5620,7 +5649,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec!["m"],
Term::Match {
@@ -5673,7 +5702,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec!["m"],
Term::Match {
@@ -5716,7 +5745,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::If {
@@ -5754,7 +5783,7 @@ mod tests {
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
}),
},
vec!["x"],
@@ -5780,7 +5809,7 @@ mod tests {
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
}),
},
vec!["x"],
@@ -5794,7 +5823,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::App {
@@ -5811,7 +5840,7 @@ mod tests {
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::App {
@@ -5845,7 +5874,7 @@ mod tests {
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
}),
},
vec!["x"],
@@ -5859,7 +5888,7 @@ mod tests {
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::App {
@@ -5897,14 +5926,14 @@ mod tests {
ret: Box::new(Type::Var { name: "b".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
Type::Var { name: "a".into() },
],
ret: Box::new(Type::Var { name: "b".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
}),
},
vec!["f", "x"],
@@ -5922,7 +5951,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec!["n"],
Term::App {
@@ -5941,7 +5970,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::App {
@@ -5989,7 +6018,7 @@ mod tests {
}),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::Ctor {
@@ -6036,7 +6065,7 @@ mod tests {
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
}),
},
params: vec!["b".into()],
@@ -6063,7 +6092,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::App {
@@ -6083,7 +6112,7 @@ mod tests {
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::App {
@@ -6131,7 +6160,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec!["b"],
Term::Lit { lit: Literal::Int { value: 0 } },
@@ -6165,7 +6194,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::Seq {
@@ -6219,7 +6248,7 @@ mod tests {
ret: Box::new(Type::Con { name: "L".into(), args: vec![] }),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec!["xs"],
// Body: C(0, tail-app loop xs) — the recursion is
@@ -6295,7 +6324,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec!["b"],
Term::Lit { lit: Literal::Int { value: 0 } },
@@ -6325,7 +6354,7 @@ mod tests {
}),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::Ctor {
@@ -6362,7 +6391,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec!["b"],
Term::Match {
@@ -6449,7 +6478,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::Match {
@@ -6541,7 +6570,7 @@ mod tests {
}),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
}),
},
default: None,
@@ -6590,7 +6619,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::Match {
@@ -6681,7 +6710,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
}),
};
let out = qualify_local_types(&input, "p", &local_types);
@@ -6735,7 +6764,7 @@ mod tests {
ret: Box::new(Type::unit()),
effects: vec!["IO".into()],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec!["xs"],
Term::Match {
@@ -6807,7 +6836,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec!["x".into()],
body: Box::new(Term::Lit { lit: Literal::Bool { value: true } }),
@@ -6831,7 +6860,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec!["x".into()],
body: Box::new(Term::Seq {
@@ -6867,7 +6896,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
body,
@@ -6903,7 +6932,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::Let {
@@ -6916,7 +6945,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec!["x".into()],
body: Box::new(Term::App {
@@ -6960,7 +6989,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
"lifted ty should have capture appended; got {:?}",
synth.ty
@@ -7031,7 +7060,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
params: vec!["z".into()],
body: Box::new(Term::App {
@@ -7078,7 +7107,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec!["p"],
outer_body,
@@ -7110,7 +7139,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
"lifted ty should have the match-arm-capture type Int appended; got {:?}",
synth.ty
@@ -7166,7 +7195,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec!["x"],
body,
@@ -7228,7 +7257,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec!["x"],
Term::Match {
@@ -7296,7 +7325,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec!["b"],
Term::Match {
@@ -7346,7 +7375,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec!["x"],
Term::Match {
@@ -7403,7 +7432,7 @@ mod tests {
}),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::Ctor {
@@ -7469,7 +7498,7 @@ mod tests {
}),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::Ctor {
@@ -7534,7 +7563,7 @@ mod tests {
}),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::Ctor {
@@ -7578,7 +7607,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec!["n"],
Term::Var { name: "n".into() },
@@ -7596,7 +7625,7 @@ mod tests {
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
vec![],
Term::App {
@@ -7846,7 +7875,7 @@ mod tests {
param_modes: vec![ParamMode::Borrow],
ret: Box::new(Type::Con { name: "Str".to_string(), args: vec![] }),
effects: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
default: None,
}],
@@ -7913,7 +7942,7 @@ mod tests {
param_modes: vec![ParamMode::Borrow],
ret: Box::new(Type::str_()),
effects: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
default: None,
}],
@@ -7925,7 +7954,7 @@ mod tests {
param_modes: vec![ParamMode::Borrow],
ret: Box::new(Type::str_()),
effects: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
};
let fnmod = Module {
schema: SCHEMA.into(),
@@ -8032,7 +8061,7 @@ mod tests {
param_modes: vec![ParamMode::Borrow],
ret: Box::new(Type::Con { name: "Str".to_string(), args: vec![] }),
effects: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
default: None,
}],
@@ -8172,8 +8201,8 @@ mod tests {
{"kind": "fn", "name": "from_maybe",
"type": {"k": "forall", "vars": ["a"], "constraints": [], "body":
{"k": "fn", "params": [{"k": "var", "name": "a"},
{"k": "con", "name": "Maybe", "args": [{"k": "var", "name": "a"}]}],
"ret": {"k": "var", "name": "a"}, "effects": []}},
{"k": "con", "name": "Maybe", "args": [{"k": "var", "name": "a"}]}], "param_modes": ["own", "borrow"],
"ret": {"k": "var", "name": "a"}, "ret_mode": "own", "effects": []}},
"params": ["default", "m"],
"body": {"t": "var", "name": "default"}}
]
@@ -8191,8 +8220,8 @@ mod tests {
"imports": [{"module": "std_maybe"}],
"defs": [
{"kind": "fn", "name": "f",
"type": {"k": "fn", "params": [{"k": "con", "name": "Int"}],
"ret": {"k": "con", "name": "Int"}, "effects": []},
"type": {"k": "fn", "params": [{"k": "con", "name": "Int"}], "param_modes": ["own"],
"ret": {"k": "con", "name": "Int"}, "ret_mode": "own", "effects": []},
"params": ["x"],
"body": {"t": "app",
"fn": {"t": "var", "name": "Maybe.from_maybe"},
@@ -8240,8 +8269,8 @@ mod tests {
"imports": [{"module": "std_maybe"}],
"defs": [
{"kind": "fn", "name": "f",
"type": {"k": "fn", "params": [],
"ret": {"k": "con", "name": "Int"}, "effects": []},
"type": {"k": "fn", "params": [], "param_modes": [],
"ret": {"k": "con", "name": "Int"}, "ret_mode": "own", "effects": []},
"params": [],
"body": {"t": "var", "name": "Maybe.bogus"}}
]
@@ -8273,8 +8302,8 @@ mod tests {
"imports": [],
"defs": [
{"kind": "fn", "name": "f",
"type": {"k": "fn", "params": [],
"ret": {"k": "con", "name": "Int"}, "effects": []},
"type": {"k": "fn", "params": [], "param_modes": [],
"ret": {"k": "con", "name": "Int"}, "ret_mode": "own", "effects": []},
"params": [],
"body": {"t": "var", "name": "SomethingRandom.x"}}
]
@@ -8314,15 +8343,15 @@ mod tests {
]},
{"kind": "fn", "name": "new",
"type": {"k": "fn",
"params": [{"k": "con", "name": "Int"}],
"ret": {"k": "con", "name": "Counter"},
"params": [{"k": "con", "name": "Int"}], "param_modes": ["own"],
"ret": {"k": "con", "name": "Counter"}, "ret_mode": "own",
"effects": []},
"params": ["n"],
"body": {"t": "ctor", "type": "Counter", "ctor": "MkCounter",
"args": [{"t": "var", "name": "n"}]}},
{"kind": "fn", "name": "main",
"type": {"k": "fn", "params": [],
"ret": {"k": "con", "name": "Counter"},
"type": {"k": "fn", "params": [], "param_modes": [],
"ret": {"k": "con", "name": "Counter"}, "ret_mode": "own",
"effects": []},
"params": [],
"body": {"t": "new", "type": "Counter",
@@ -8369,8 +8398,8 @@ mod tests {
]},
// No `new` def for T.
{"kind": "fn", "name": "main",
"type": {"k": "fn", "params": [],
"ret": {"k": "con", "name": "T"},
"type": {"k": "fn", "params": [], "param_modes": [],
"ret": {"k": "con", "name": "T"}, "ret_mode": "own",
"effects": []},
"params": [],
"body": {"t": "new", "type": "T",
@@ -8423,17 +8452,17 @@ mod tests {
{"kind": "fn", "name": "new",
"type": {"k": "forall", "vars": ["a"], "constraints": [],
"body": {"k": "fn",
"params": [{"k": "var", "name": "a"}],
"params": [{"k": "var", "name": "a"}], "param_modes": ["own"],
"ret": {"k": "con", "name": "T",
"args": [{"k": "var", "name": "a"}]},
"args": [{"k": "var", "name": "a"}]}, "ret_mode": "own",
"effects": []}},
"params": ["x"],
"body": {"t": "ctor", "type": "T", "ctor": "MkT",
"args": [{"t": "var", "name": "x"}]}},
{"kind": "fn", "name": "main",
"type": {"k": "fn", "params": [],
"type": {"k": "fn", "params": [], "param_modes": [],
"ret": {"k": "con", "name": "T",
"args": [{"k": "con", "name": "Int"}]},
"args": [{"k": "con", "name": "Int"}]}, "ret_mode": "own",
"effects": []},
"params": [],
// Wrong: only one NewArg::Value supplied for a sig
@@ -8478,9 +8507,9 @@ mod tests {
// Reference `(con StubT (con Str))` in a fn return
// type — well-formedness check walks the return type.
{"kind": "fn", "name": "bad",
"type": {"k": "fn", "params": [],
"type": {"k": "fn", "params": [], "param_modes": [],
"ret": {"k": "con", "name": "StubT",
"args": [{"k": "con", "name": "Str"}]},
"args": [{"k": "con", "name": "Str"}]}, "ret_mode": "own",
"effects": []},
"params": [],
"body": {"t": "lit", "lit": {"kind": "unit"}}}
@@ -8518,9 +8547,9 @@ mod tests {
"ctors": [{"name": "Stub", "fields": [{"k": "var", "name": "a"}]}],
"param-in": {"a": ["Int", "Float"]}},
{"kind": "fn", "name": "bad",
"type": {"k": "fn", "params": [],
"type": {"k": "fn", "params": [], "param_modes": [],
"ret": {"k": "con", "name": "StubT",
"args": [{"k": "con", "name": "Str"}]},
"args": [{"k": "con", "name": "Str"}]}, "ret_mode": "own",
"effects": []},
"params": [],
"body": {"t": "lit", "lit": {"kind": "unit"}}}
@@ -8563,9 +8592,9 @@ mod tests {
"ctors": [{"name": "Stub", "fields": [{"k": "var", "name": "a"}]}],
"param-in": {"a": ["Int", "Float"]}},
{"kind": "fn", "name": "bad",
"type": {"k": "fn", "params": [],
"type": {"k": "fn", "params": [], "param_modes": [],
"ret": {"k": "con", "name": "StubT",
"args": [{"k": "con", "name": "Str"}]},
"args": [{"k": "con", "name": "Str"}]}, "ret_mode": "own",
"effects": []},
"params": [],
"body": {"t": "lit", "lit": {"kind": "unit"}}}
@@ -8610,9 +8639,9 @@ mod tests {
"ctors": [{"name": "Stub", "fields": [{"k": "var", "name": "a"}]}],
"param-in": {"a": ["Int", "Float"]}},
{"kind": "fn", "name": "good",
"type": {"k": "fn", "params": [],
"type": {"k": "fn", "params": [], "param_modes": [],
"ret": {"k": "con", "name": "StubT",
"args": [{"k": "con", "name": "Int"}]},
"args": [{"k": "con", "name": "Int"}]}, "ret_mode": "own",
"effects": []},
"params": [],
"body": {"t": "lit", "lit": {"kind": "unit"}}}
@@ -8667,8 +8696,8 @@ mod tests {
// shape is irrelevant.
{"kind": "fn", "name": "value",
"type": {"k": "fn",
"params": [{"k": "con", "name": "Counter"}],
"ret": {"k": "con", "name": "Int"},
"params": [{"k": "con", "name": "Counter"}], "param_modes": ["borrow"],
"ret": {"k": "con", "name": "Int"}, "ret_mode": "own",
"effects": []},
"params": ["c"],
"body": {"t": "lit", "lit": {"kind": "int", "value": 0}}},
@@ -8677,8 +8706,8 @@ mod tests {
// Per spec § prep.1 "Canonical form decision" this
// is the canonical form and must check.
{"kind": "fn", "name": "main",
"type": {"k": "fn", "params": [],
"ret": {"k": "con", "name": "Int"},
"type": {"k": "fn", "params": [], "param_modes": [],
"ret": {"k": "con", "name": "Int"}, "ret_mode": "own",
"effects": []},
"params": [],
"body": {"t": "let", "name": "c",
@@ -8750,8 +8779,8 @@ mod tests {
{"kind": "fn", "name": "unwrap_int",
"type": {"k": "fn",
"params": [{"k": "con", "name": "StubT",
"args": [{"k": "con", "name": "Int"}]}],
"ret": {"k": "con", "name": "Int"},
"args": [{"k": "con", "name": "Int"}]}], "param_modes": ["borrow"],
"ret": {"k": "con", "name": "Int"}, "ret_mode": "own",
"effects": []},
"params": ["s"],
"body": {"t": "lit", "lit": {"kind": "int", "value": 0}}}
+2 -2
View File
@@ -561,7 +561,7 @@ impl<'a> Lifter<'a> {
ret: ret.clone(),
effects: effects.clone(),
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
}
}
Type::Forall { .. } => panic!(
@@ -933,7 +933,7 @@ fn substitute_rigids_local(t: &Type, mapping: &BTreeMap<String, Type>) -> Type {
ret: Box::new(substitute_rigids_local(ret, mapping)),
effects: effects.clone(),
param_modes: vec![],
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
},
Type::Forall { vars, constraints, body } => {
let inner: BTreeMap<String, Type> = mapping
+148 -51
View File
@@ -1,13 +1,11 @@
//! Linearity check for fns whose every parameter mode is
//! explicit (`Borrow` or `Own`).
//! Linearity check for fns. Every parameter mode is explicit
//! (`Borrow` or `Own`) — ownership has no default (spec 0062).
//!
//! ## Scope
//!
//! - **Active only** on `Def::Fn` whose `Type::Fn.param_modes` is
//! non-empty AND contains no [`ParamMode::Implicit`]. Any
//! `Implicit`-mode param skips the entire fn — that is the back-compat
//! lane that keeps every existing fixture green (only
//! `borrow_own_demo` ships an all-explicit signature today).
//! non-empty. A nullary fn has no binders to track, so the check
//! skips it.
//! - **Pure diagnostic.** Emits [`Diagnostic`]s with codes
//! `use-after-consume` / `consume-while-borrowed`. No IR change, no
//! codegen change, no runtime change. The check runs after
@@ -34,8 +32,7 @@
//! application.
//! - `Term::App.args[i]` — Borrow if the callee is a `Var` whose
//! resolved type is a `Type::Fn` with `param_modes[i] == Borrow`;
//! otherwise Consume (Own / Implicit / unknown all default to
//! Consume).
//! otherwise Consume (Own / unknown default to Consume).
//! - `Term::Clone.value` — Borrow (clone reads + bumps RC, doesn't
//! move).
//! - `Term::Match.scrutinee` — Borrow (matching is a read; for an
@@ -364,8 +361,8 @@ fn fn_modes_of(t: &Type) -> Option<Vec<ParamMode>> {
}
}
/// Per-fn check. Skips fns whose signature has any `Implicit` param —
/// see module-level scope rules.
/// Per-fn check. Skips nullary fns (no binders to track) — see
/// module-level scope rules.
fn check_fn(
f: &FnDef,
globals: &HashMap<String, Type>,
@@ -380,17 +377,17 @@ fn check_fn(
_ => return,
};
// Activation gate: every param mode must be explicit (Borrow / Own).
// No params at all also disables the check — there are no binders
// to track in the param-list, so the check has nothing to enforce.
if param_modes.is_empty() || param_modes.iter().any(|m| matches!(m, ParamMode::Implicit)) {
// Activation gate: no params at all disables the check — there are
// no binders to track in the param-list, so the check has nothing
// to enforce. Every mode is now explicitly `Own`/`Borrow`
// (spec 0062), so there is no unmoded disjunct left.
if param_modes.is_empty() {
return;
}
// Sanity: param-count has been verified by `check_fn` upstream, but
// double-defend in case the type vec is shorter than the binder list
// — the linearity walk treats over-long binder lists as `Implicit`
// and would silently mis-skip the check otherwise.
// double-defend in case the type vec is shorter than the binder
// list, which would silently mis-skip the check otherwise.
if param_modes.len() != f.params.len() || param_tys.len() != f.params.len() {
return;
}
@@ -414,7 +411,7 @@ fn check_fn(
consumed: false,
borrow_count: match mode {
ParamMode::Borrow => 1,
ParamMode::Own | ParamMode::Implicit => 0,
ParamMode::Own => 0,
},
is_value: param_tys.get(i).map(type_is_value).unwrap_or(false),
fn_param_modes: param_tys.get(i).and_then(|t| fn_modes_of(t)),
@@ -436,10 +433,26 @@ fn check_fn(
// is fine as long as none of the arms moves a sub-binder out.
// The uniqueness pass already populates `consume_count` for
// pattern-binders, so the check is a deterministic walk.
//
// Two guards keep the lint coherent post-cutover:
// - An `(intrinsic)`-bodied fn is skipped wholesale: the walk does
// nothing for `Term::Intrinsic`, so `consume_count == 0` is a
// guaranteed false positive. An intrinsic's param modes are
// hand-authored contracts, not lint-derivable from a body.
// - A value-typed param (`Int`/`Bool`/`Float`/`Unit`) is skipped:
// `(borrow V)` is rejected by `borrow-over-value` (lib.rs), so
// `(own V)` is the only legal mode and a relax-to-borrow
// suggestion would be incoherent.
if matches!(f.body, Term::Intrinsic) {
return;
}
for (i, mode) in param_modes.iter().enumerate() {
if !matches!(mode, ParamMode::Own) {
continue;
}
if param_tys.get(i).map(type_is_value).unwrap_or(false) {
continue;
}
let pname = &f.params[i];
let consume_count = uniq
.get(&(f.name.clone(), pname.clone()))
@@ -518,7 +531,7 @@ impl<'a> Checker<'a> {
for (i, arg) in args.iter().enumerate() {
let arg_pos = match arg_modes.get(i) {
Some(ParamMode::Borrow) => Position::Borrow,
// Own / Implicit / unknown → Consume.
// Own / unknown → Consume.
_ => Position::Consume,
};
self.walk(arg, arg_pos);
@@ -903,7 +916,7 @@ impl<'a> Checker<'a> {
if param_modes.is_empty() {
// All-implicit case (legacy): no mode info → all
// Consume, by the rule above.
vec![ParamMode::Implicit; n_args]
vec![ParamMode::Own; n_args]
} else {
param_modes.clone()
}
@@ -1444,14 +1457,10 @@ fn relax_param_to_borrow(inner: &Type, i: usize) -> Type {
ret_mode,
effects,
} => {
// Materialise param_modes to full length so the relaxed
// mode is positioned correctly even when the original
// vec was elided to "all-implicit empty" (not the case
// here — we only reach this for an `Own` slot — but
// defensive).
let mut new_modes: Vec<ParamMode> = (0..params.len())
.map(|j| param_modes.get(j).copied().unwrap_or(ParamMode::Implicit))
.collect();
// `param_modes.len() == params.len()` (spec 0062: every
// slot is moded), so a direct clone places the relaxed
// mode correctly.
let mut new_modes: Vec<ParamMode> = param_modes.clone();
if i < new_modes.len() {
new_modes[i] = ParamMode::Borrow;
}
@@ -1503,7 +1512,7 @@ mod tests {
.collect(),
param_modes: modes.clone(),
ret: Box::new(Type::int()),
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
effects: vec![],
},
params: (0..modes.len()).map(|i| format!("p{i}")).collect(),
@@ -1529,7 +1538,7 @@ mod tests {
}],
param_modes: vec![mode],
ret: Box::new(Type::int()),
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
effects: vec![],
},
params: vec!["p0".into()],
@@ -1684,7 +1693,7 @@ mod tests {
params: vec![Type::Con { name: "Int".into(), args: vec![] }],
param_modes: vec![ParamMode::Own],
ret: Box::new(Type::int()),
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
effects: vec![],
},
params: vec!["p0".into()],
@@ -1874,17 +1883,18 @@ mod tests {
imports: vec![],
defs: vec![fn_with_modes(
"f",
vec![ParamMode::Implicit],
vec![ParamMode::Own],
Term::Var { name: "p0".into() },
)],
};
assert!(check_module(&m).is_empty());
}
/// Mixed Implicit + Borrow → still skipped (any Implicit disables
/// the check entirely, per the activation gate).
/// Post-cutover (spec 0062): every param is explicitly `Own`/`Borrow`,
/// so a mixed `[Borrow, Own]` fn is checked (no Implicit exemption
/// remains). The unused `Own` param trips `over-strict-mode`.
#[test]
fn mixed_implicit_explicit_is_exempt() {
fn mixed_borrow_own_unused_own_fires_over_strict() {
let m = Module {
schema: ailang_core::SCHEMA.into(),
name: "t".into(),
@@ -1892,11 +1902,15 @@ mod tests {
imports: vec![],
defs: vec![fn_with_modes(
"f",
vec![ParamMode::Borrow, ParamMode::Implicit],
vec![ParamMode::Borrow, ParamMode::Own],
Term::Lit { lit: Literal::Int { value: 0 } },
)],
};
assert!(check_module(&m).is_empty());
let diags = check_module(&m);
assert!(
diags.iter().any(|d| d.code == "over-strict-mode"),
"the unused `Own` param must fire over-strict-mode; got: {diags:#?}"
);
}
/// All-explicit fn that NEVER uses its params, annotated `Borrow`:
@@ -2011,7 +2025,7 @@ mod tests {
params: vec![Type::Con { name: "List".into(), args: vec![] }],
param_modes: vec![ParamMode::Borrow],
ret: Box::new(Type::int()),
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
effects: vec![],
},
params: vec!["xs".into()],
@@ -2052,17 +2066,14 @@ mod tests {
!d.suggested_rewrites.is_empty(),
"diagnostic should carry a suggested rewrite"
);
// The replacement is a fn-type, so it does not have to round-trip
// through `parse_term` (which parses terms). We only assert it's
// a non-empty string that mentions the relaxed `(borrow ...)`.
// The replacement is a fn-type. Post-cutover every slot is moded,
// so the rewrite legitimately carries `(own ...)` on the return
// slot; the property under test is that the over-strict *param*
// slot is relaxed to `(borrow ...)`.
let rep = &d.suggested_rewrites[0].replacement;
assert!(
rep.contains("(borrow"),
"rewrite should mention `(borrow`; got {rep}"
);
assert!(
!rep.contains("(own"),
"rewrite should NOT contain the original `(own`; got {rep}"
rep.contains("(params (borrow (con List)))"),
"rewrite should relax the param to `(borrow (con List))`; got {rep}"
);
}
@@ -2183,7 +2194,7 @@ mod tests {
params: vec![Type::Con { name: "IntList".into(), args: vec![] }],
param_modes: vec![ParamMode::Own],
ret: Box::new(Type::int()),
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
effects: vec![],
},
params: vec!["xs".into()],
@@ -2257,7 +2268,7 @@ mod tests {
params: vec![Type::Con { name: "State".into(), args: vec![] }],
param_modes: vec![ParamMode::Own],
ret: Box::new(Type::Con { name: "State".into(), args: vec![] }),
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
effects: vec![],
},
params: vec!["st".into()],
@@ -2386,6 +2397,92 @@ mod tests {
);
}
/// `over-strict-mode` does NOT fire on a never-consumed `(own V)`
/// param whose type is value-typed (`Int`/`Bool`/`Float`/`Unit`).
/// Property: for a value-typed param the lint's own suggested
/// rewrite — `(borrow V)` — is itself a `borrow-over-value` error
/// (lib.rs), so `(own V)` is the only legal mode and a suggestion
/// to relax it is incoherent. The fn body is real (a literal), so
/// the only reason the param looks "over-strict" is its value type;
/// the lint must stay silent regardless.
#[test]
fn over_strict_mode_silent_when_param_is_value_typed() {
// Body: (lit 0) — `p0 : (own (con Int))` is never used at all.
let body = Term::Lit { lit: Literal::Int { value: 0 } };
let f = Def::Fn(FnDef {
name: "f".into(),
ty: Type::Fn {
params: vec![Type::int()],
param_modes: vec![ParamMode::Own],
ret: Box::new(Type::int()),
ret_mode: ParamMode::Own,
effects: vec![],
},
params: vec!["p0".into()],
body,
suppress: vec![],
doc: None,
export: None,
});
let m = Module {
schema: ailang_core::SCHEMA.into(),
name: "t".into(),
kernel: false,
imports: vec![],
defs: vec![f],
};
let diags = check_module(&m);
assert!(
!diags.iter().any(|d| d.code == "over-strict-mode"),
"no over-strict-mode expected for a value-typed `(own Int)` \
param: `(borrow Int)` is a borrow-over-value error, so `own` \
is the only legal mode; got {diags:?}"
);
}
/// `over-strict-mode` does NOT fire on a fn whose body is
/// `Term::Intrinsic`, even when an `(own HeapType)` param looks
/// never-consumed. Property: the linearity walk does nothing for an
/// intrinsic body (`Term::Intrinsic => {}`), so `consume_count == 0`
/// is a guaranteed false positive — the param modes of an intrinsic
/// are hand-authored contracts, not lint-derivable from a walkable
/// body.
#[test]
fn over_strict_mode_silent_for_intrinsic_body() {
// `(own IntList)` param, body = (intrinsic). IntList is heap-typed,
// so the only reason the lint would stay silent post-fix is the
// intrinsic-body guard, not the value-type guard.
let f = Def::Fn(FnDef {
name: "f".into(),
ty: Type::Fn {
params: vec![Type::Con { name: "IntList".into(), args: vec![] }],
param_modes: vec![ParamMode::Own],
ret: Box::new(Type::int()),
ret_mode: ParamMode::Own,
effects: vec![],
},
params: vec!["p0".into()],
body: Term::Intrinsic,
suppress: vec![],
doc: None,
export: None,
});
let m = Module {
schema: ailang_core::SCHEMA.into(),
name: "t".into(),
kernel: false,
imports: vec![],
defs: vec![intlist_typedef(), f],
};
let diags = check_module(&m);
assert!(
!diags.iter().any(|d| d.code == "over-strict-mode"),
"no over-strict-mode expected for an intrinsic-bodied fn: the \
walk cannot observe consumption, so consume_count==0 is a \
false positive; got {diags:?}"
);
}
/// a borrow-mode fn that forwards its borrow
/// params into a class-method call must not fire
/// `consume-while-borrowed` false positives. The pre-fix behaviour
@@ -2410,7 +2507,7 @@ mod tests {
],
param_modes: vec![ParamMode::Borrow, ParamMode::Borrow],
ret: Box::new(Type::bool_()),
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
effects: vec![],
},
default: None,
@@ -2427,7 +2524,7 @@ mod tests {
],
param_modes: vec![ParamMode::Borrow, ParamMode::Borrow],
ret: Box::new(Type::bool_()),
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
effects: vec![],
},
params: vec!["x".into(), "y".into()],
+25 -5
View File
@@ -260,11 +260,31 @@ fn lower_term(ctx: &mut Ctx, t: &Term) -> Result<MTerm> {
.iter()
.enumerate()
.map(|(i, a)| {
Ok(MArg {
term: lower_term(ctx, a)?,
mode: param_mode_at(&sig, i),
consume_count: 1,
})
let mode = param_mode_at(&sig, i);
let mut term = lower_term(ctx, a)?;
// mir.4 leg D: a `Str` literal passed BY VALUE into an
// owned (`Own`) param the callee drops must be an owned
// heap slab, not a header-less static rodata constant.
// The cutover's now-active Own-param drop path emits
// `ailang_rc_dec` on the argument; dec'ing a
// `StrRep::Static` reads `payload-8` (the length field)
// as a fake refcount and `free()`s a static address ->
// SIGSEGV. Flip the rep to Heap so codegen emits
// `ailang_str_clone` (rc-headered, refcount 1) and the
// callee's single dec is sound. Gated STRICTLY on `Own`:
// a `Borrow` param fires no `rc_dec`, so borrow-arg
// literals MUST stay `StrRep::Static` (no spurious
// clone). This is the fourth Static->Heap promotion,
// siblings to the seed/tail/recur loop-carried ones.
if matches!(mode, Mode::Owned) {
let arg_ty = ctx.synth_pure(a)?;
if is_str_ty(&arg_ty) {
if let MTerm::Str { rep, .. } = &mut term {
*rep = StrRep::Heap;
}
}
}
Ok(MArg { term, mode, consume_count: 1 })
})
.collect::<Result<Vec<_>>>()?;
let m_callee = match class {
+9
View File
@@ -588,6 +588,15 @@ pub fn scoped_base(scope: &Option<String>, name: &str) -> String {
}
}
/// Public view of the per-type-arg mono suffix mangler. Codegen's
/// per-monomorph drop-fn naming (`drop_<m>_Pair__Int_Int`) reuses this
/// exact mangling so a drop symbol's suffix matches the fn-symbol
/// scheme byte-for-byte. Delegates to the same private helper
/// `mono_symbol_n` uses, so its fn-symbol output is unperturbed.
pub fn type_mono_suffix(ty: &Type) -> String {
type_to_mono_suffix(ty)
}
fn type_to_mono_suffix(ty: &Type) -> String {
match primitive_surface_name(ty) {
Some(p) => p.to_string(),
+5 -12
View File
@@ -29,10 +29,8 @@
//! ## Activation gate
//!
//! Same as `linearity::check_module`: only fns whose `Type::Fn.param_modes`
//! is non-empty AND contains no [`ParamMode::Implicit`]. Reuse-as is
//! part of the explicit-mode discipline; firing the check on legacy
//! all-`Implicit` fns would risk false positives on workloads that
//! never opted into the explicit-mode lane.
//! is non-empty (a nullary fn has no binders to track). Every mode is
//! explicit `Own`/`Borrow` (spec 0062).
//!
//! ## Path-ctor resolution
//!
@@ -96,19 +94,14 @@ pub(crate) fn check_module(m: &Module) -> Vec<Diagnostic> {
diags
}
/// Per-fn check. Skips fns whose signature has any `Implicit` param
/// (legacy back-compat lane) or whose param list is empty (no binders
/// Per-fn check. Skips fns whose param list is empty (no binders
/// to track).
fn check_fn(f: &FnDef, types: &HashMap<String, TypeDef>, diags: &mut Vec<Diagnostic>) {
let param_modes: &[ParamMode] = match strip_forall(&f.ty) {
Type::Fn { param_modes, .. } => param_modes.as_slice(),
_ => return,
};
if param_modes.is_empty()
|| param_modes
.iter()
.any(|m| matches!(m, ParamMode::Implicit))
{
if param_modes.is_empty() {
return;
}
@@ -607,7 +600,7 @@ mod tests {
.collect(),
param_modes: modes.clone(),
ret: Box::new(Type::Con { name: "List".into(), args: vec![] }),
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
effects: vec![],
},
params: (0..modes.len()).map(|i| format!("p{i}")).collect(),
+1 -1
View File
@@ -120,7 +120,7 @@ mod tests {
params: vec![],
param_modes: vec![],
ret: Box::new(Type::int()),
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
effects: vec![],
},
params: vec![],
+2 -2
View File
@@ -449,7 +449,7 @@ impl<'a> Walker<'a> {
match inner {
Type::Fn { param_modes, .. } => {
if param_modes.is_empty() {
vec![ParamMode::Implicit; n_args]
vec![ParamMode::Own; n_args]
} else {
param_modes.clone()
}
@@ -510,7 +510,7 @@ mod tests {
params: params.iter().map(|_| Type::int()).collect(),
param_modes: vec![],
ret: Box::new(Type::int()),
ret_mode: ParamMode::Implicit,
ret_mode: ParamMode::Own,
effects: vec![],
},
params: params.into_iter().map(|s| s.to_string()).collect(),