feat(check): harden the ownership analysis for universal activation (0063)
The strict linearity check (use-after-consume / consume-while-borrowed, linearity.rs) runs today only on the ~45 of 258 all-explicit-mode fns; its activation gate skips any fn with a bare/Implicit param. Deleting ParamMode::Implicit (#55) would turn it on universally and surface ~21% false positives in two classes. This closes both, making the core ownership analysis sharp across the whole codebase for the first time — the precondition for #55. Diagnostic-only: no schema, no hash, no codegen change; the two existing diagnostic codes simply fire on a smaller, more correct set. Fix 1 — value-type exemption. A new is_value_type predicate (ailang-core::primitives) names the unboxed set {Int,Bool,Float,Unit}; BinderState gains an is_value flag seeded from the binder's locally available type (param signatures, ctor field types for pattern binders, lam typed-params), and use_var short-circuits the Consume arm for it. A value type has no refcount and is never consumed, so multi-read is always legal. Str is deliberately NOT in the value set, though is_primitive_name includes it: drop.rs:490-492 lowers only Int/Bool/Float/Unit to non-ptr; Str is a ptr, RC-dec'd, so a multi-consume of Str without clone is a real use-after-free. is_value_type is the Str-excluding subset of is_primitive_name, pinned by a unit test. is_heap_type and the over-strict-mode lint are left untouched (separate concern). Fix 2 — application is a borrow. Term::App walks its callee in Position::Borrow (was Consume). Applying a function value reads it; it stays live for further applications. Global fn-refs are untracked (no-op); a tracked function-typed binder (a HOF param) is no longer consumed by application. This fixes both the own-f-param use-after-consume and the borrow-f-param consume-while-borrowed in the recursion-passing HOF shape (map_int f t). Passing a function value as an arg is unchanged — it follows the callee param_modes. Scope decision: value-typed let-binders stay conservatively tracked as heap (their type is inferred, not annotated, and the linearity walk does not re-run inference). This is soundness-safe — over-strict, never unsound — and not a measured corpus shape; lifting it would need a full binder->type table out of the type-checker. Verification: all three false-positive classes reproduced today against explicit-mode fns and shipped as RED->GREEN fixtures (examples/fp_{value,hof,map}.ail); examples/real_consume.ail stays RED (heap double-consume still fires) to prove the exemption is type-gated. 715 workspace tests green; bench/check.py 0/34 and bench/compile_check.py 0/24 regressed. merge_states carries is_value so the flag survives branch merges. RED-first verified per task. closes #56
This commit is contained in:
@@ -17,6 +17,18 @@ pub fn is_primitive_name(name: &str) -> bool {
|
||||
matches!(name, "Int" | "Bool" | "Str" | "Unit" | "Float")
|
||||
}
|
||||
|
||||
/// Returns `true` iff `name` is an **unboxed value type** — no RC, no
|
||||
/// heap slab, copied by value. This is the `Str`-excluding subset of
|
||||
/// [`is_primitive_name`]: `Str` is a primitive zero-arity ctor but is
|
||||
/// heap-allocated (`ptr`, RC-`dec`'d — codegen `drop.rs:490-492` lowers
|
||||
/// only `Int`/`Bool`/`Float`/`Unit` to non-`ptr`). Used by the
|
||||
/// linearity analysis to exempt value-type binders from
|
||||
/// consume-tracking: a value type is never consumed, so multi-use is
|
||||
/// always legal.
|
||||
pub fn is_value_type(name: &str) -> bool {
|
||||
matches!(name, "Int" | "Bool" | "Float" | "Unit")
|
||||
}
|
||||
|
||||
/// Returns the static-lifetime surface name iff `name` is a
|
||||
/// primitive. Used by the mono pass to embed the human-readable
|
||||
/// form in monomorphised symbol names; the static lifetime is what
|
||||
@@ -60,4 +72,24 @@ mod tests {
|
||||
"Float surface name must be \"Float\""
|
||||
);
|
||||
}
|
||||
|
||||
/// `is_value_type` is the unboxed/no-RC subset of the primitives:
|
||||
/// it agrees with `is_primitive_name` on every name EXCEPT `Str`,
|
||||
/// which is a primitive zero-arity ctor but is heap-allocated
|
||||
/// (`ptr`, RC'd) — see drop.rs:490-492.
|
||||
#[test]
|
||||
fn value_type_is_primitive_minus_str() {
|
||||
for name in ["Int", "Bool", "Float", "Unit"] {
|
||||
assert!(is_value_type(name), "{name} must be a value type");
|
||||
assert!(is_primitive_name(name), "{name} must be a primitive");
|
||||
}
|
||||
// The sole divergence: Str is a primitive but NOT a value type.
|
||||
assert!(!is_value_type("Str"), "Str is heap-allocated, not a value type");
|
||||
assert!(is_primitive_name("Str"), "Str is still a primitive zero-arity ctor");
|
||||
// Non-primitives are neither.
|
||||
for name in ["List", "Foo", ""] {
|
||||
assert!(!is_value_type(name));
|
||||
assert!(!is_primitive_name(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user