aaa70d4c35
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
96 lines
4.1 KiB
Rust
96 lines
4.1 KiB
Rust
//! The primitive-name set: `Int`, `Bool`, `Str`, `Unit`. Single
|
|
//! source of truth for "is this name a built-in zero-arity type
|
|
//! constructor?" Every typecheck and codegen site that gates
|
|
//! behaviour on the primitive set goes through this module.
|
|
//!
|
|
//! Adding a primitive: append to BOTH functions below. The lockstep
|
|
//! invariant ([`is_primitive_name`] is `true` iff
|
|
//! [`primitive_surface_name`] is `Some`) is pinned by the unit test
|
|
//! at the bottom of this file — extending only one of the two
|
|
//! fails the test before the bug reaches a consumer.
|
|
|
|
/// Returns `true` iff `name` is one of the built-in zero-arity
|
|
/// primitive type constructors. Cross-crate predicate used by
|
|
/// typecheck-time well-formedness gating, the heap-type filter,
|
|
/// the local-type qualifier, and the codegen substitution path.
|
|
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
|
|
/// makes the symbol-builder's `&'static str` signature work.
|
|
pub fn primitive_surface_name(name: &str) -> Option<&'static str> {
|
|
match name {
|
|
"Int" => Some("Int"),
|
|
"Bool" => Some("Bool"),
|
|
"Str" => Some("Str"),
|
|
"Unit" => Some("Unit"),
|
|
"Float" => Some("Float"),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// The two functions must agree on which names are primitive:
|
|
/// `is_primitive_name(n)` true iff `primitive_surface_name(n)`
|
|
/// is `Some`. The list of names tested mirrors the literal set
|
|
/// inside both functions plus a sample non-primitive.
|
|
#[test]
|
|
fn predicate_and_surface_name_agree() {
|
|
for name in ["Int", "Bool", "Str", "Unit", "Float", "List", "Foo", "", "int"] {
|
|
assert_eq!(
|
|
is_primitive_name(name),
|
|
primitive_surface_name(name).is_some(),
|
|
"lockstep violation for {name:?}"
|
|
);
|
|
}
|
|
// The loop's assert_eq! passes vacuously when both functions
|
|
// return `false`/`None` for a name; the explicit assertions
|
|
// below catch the case where a primitive is silently removed
|
|
// from both functions but stays in the loop list.
|
|
assert!(is_primitive_name("Float"), "Float must be a primitive");
|
|
assert_eq!(
|
|
primitive_surface_name("Float"),
|
|
Some("Float"),
|
|
"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));
|
|
}
|
|
}
|
|
}
|