Files
AILang/crates/ailang-core/src/primitives.rs
T
Brummel aaa70d4c35 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
2026-06-01 15:38:32 +02:00

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));
}
}
}