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