72d2d9c806
Follow-up to a fan-out audit of the whole test suite (742 tests, 21
read-only assessors + synthesis). Two clusters of the audit were
actioned here; the #66 hashing-removal cohort it surfaced is left for
the #66 scope, and the RED-first doc-rot sweep is deferred.
§2 — true duplicates removed (each kept test is a strict superset or
identical fixture+assertions of the deleted one):
- ailang-check param_in_reject_message_names_allowed_set (kept the
_renders_allowed_set_in_ailang_syntax superset)
- ailang-check cross_module_pat_ctor_typedriven_resolves (kept ct2_;
shared cross_module_ws helper retained, 2 other callers)
- ailang-core mq1_qualified_instancedef_class_accepted (kept ct1_)
- ail e2e bool_to_str_emits_true_branch (kept the rc-stats superset)
- ail loop_recur_str adt-leg guard (the standalone heap pin owns it;
dropped the dangling header reference too)
- ailang-check over_strict_mode_silent_when_param_is_borrow (its
assertion is subsumed by explicit_fn_with_no_uses_is_clean; its own
doc claimed a "borrowing body" the Lit body never provided)
§4 — meta-guards that had gone partially blind re-sighted:
- spec_drift::spec_mentions_every_def_kind now pushes Class/Instance
exemplars and asserts their `(class `/`(instance` anchors; the
"wait for 22b.4" exclusion was stale (anchors shipped in e809f45).
- schema_coverage now tracks Term::New (VariantTag + EXPECTED +
visitor insert); the new_*/raw_buf_* fixtures already exercise it,
so the "no fixture emits new" exclusion was stale.
- prose doc_wrap_widow_control_skips_when_combined_too_long was
tautological: its 32/32/42-char words gave combined=75<=80, so
widow control actually fired. Rebuilt the fixture (short head + two
40-char words → combined=81>80) so it hits the real skip branch,
and added the missing "last line stays 1 word" assertion. The
obvious 2-word fixture does NOT work — it skips via the
prev_words<2 branch, not the combined-over-budget branch.
ailang-check 124->121, ailang-core 55->54, e2e 102->101,
loop_recur_str 4->3; spec_drift 8 green, schema_coverage green,
prose lib 63 green.
2686 lines
112 KiB
Rust
2686 lines
112 KiB
Rust
//! 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. 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
|
|
//! [`crate::check_fn`] type-checking succeeded for the def.
|
|
//! - **No uniqueness inference.** That is Iter 18c.3. The check only
|
|
//! tracks what is *spelled* in the source: a binder is consumed when
|
|
//! it appears in a non-borrow position; cloned via [`Term::Clone`];
|
|
//! borrowed via fn-call into a `Borrow` parameter.
|
|
//!
|
|
//! ## Position model
|
|
//!
|
|
//! Each [`Term::Var`] occurrence is in some "position", determined by
|
|
//! its parent term:
|
|
//!
|
|
//! - **Consume** — default. The use takes ownership.
|
|
//! - **Borrow** — the use lends a reference; ownership stays with the
|
|
//! binder.
|
|
//!
|
|
//! Position propagation:
|
|
//! - `Term::App.callee` — Borrow (applying a function value reads it;
|
|
//! the value stays live for further applications and is dropped at
|
|
//! scope close). A global fn-ref callee is untracked, so this is a
|
|
//! no-op there; a tracked function-typed binder is not consumed by
|
|
//! 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 / 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
|
|
//! `Own` param this leaves the binder uneconsumed, which is a known
|
|
//! false-negative for "param declared own but never moved" — out of
|
|
//! scope for 18c.2 per the assignment).
|
|
//! - `Term::Let.value`, `Term::Seq.lhs`, `Term::If.cond` — Consume.
|
|
//! - `Term::Ctor.args[*]` — Consume (the new value owns them).
|
|
//! - `Term::Do.args[*]` — Borrow (effect-op observes; ownership
|
|
//! stays with the caller).
|
|
//! - `Term::Lam` — captures are conservatively Consume.
|
|
//!
|
|
//! ## Within-call borrow lifetime (consume-while-borrowed)
|
|
//!
|
|
//! For `App { callee, args, .. }` whose callee is a `Var` resolving to
|
|
//! a fn-typed binding with explicit `param_modes`: when the i-th arg is
|
|
//! a bare `Term::Var { name }` consumed in Borrow position, we
|
|
//! **increment** `borrow_count[name]` *before* evaluating
|
|
//! `args[i+1..]`. After all args are evaluated and the call has run,
|
|
//! we **decrement** them again. So a sibling subterm that consumes the
|
|
//! same binder while it is still borrowed by an earlier sibling
|
|
//! triggers the `consume-while-borrowed` diagnostic.
|
|
//!
|
|
//! For a `Borrow` *parameter* of the enclosing fn, the binder starts
|
|
//! the body with `borrow_count = 1` (the caller's outer borrow). Any
|
|
//! attempt to consume it inside the body therefore also triggers
|
|
//! `consume-while-borrowed`.
|
|
//!
|
|
//! ## Suggested rewrites
|
|
//!
|
|
//! Each diagnostic carries a non-empty
|
|
//! [`crate::diagnostic::Diagnostic::suggested_rewrites`]. The form-A
|
|
//! replacement is parseable by [`ailang_surface::parse_term`] — that's
|
|
//! the contract guarded by the round-trip test in `tests/`.
|
|
//!
|
|
//! - `use-after-consume` on `Var x`: replacement `(clone <name>)` —
|
|
//! the author should clone an *earlier* use so this later use stays
|
|
//! the unique consume.
|
|
//! - `consume-while-borrowed` on `Var x`: replacement `(clone <name>)`
|
|
//! — the author should clone here so the original keeps the borrow.
|
|
//!
|
|
//! ## Iter 19a: `over-strict-mode` lint
|
|
//!
|
|
//! After the linearity walk has finished for a fn whose every param
|
|
//! mode is explicit, we re-inspect each param annotated `(own T)`:
|
|
//!
|
|
//! - The uniqueness pass ([`crate::uniqueness::infer_module`]) gives
|
|
//! us the param's `consume_count` and, importantly, the
|
|
//! `consume_count` of every pattern-binder introduced when the
|
|
//! param is matched on.
|
|
//! - If `consume_count(p) == 0` AND, for every `match p ...` in the
|
|
//! body, every pattern-binder bound by an arm of that match has
|
|
//! `consume_count == 0`, then the fn could have been written with
|
|
//! `(borrow T)` instead and would be cheaper to call (no inc/dec
|
|
//! pair, no rec-cascade at fn return). Emit `over-strict-mode` as
|
|
//! a `Severity::Warning` with a suggested fn-type rewrite.
|
|
//!
|
|
//! ### Iter 19a.1: precise sub-binder analysis
|
|
//!
|
|
//! 19a originally shipped a conservative cut ("any `match` whose
|
|
//! scrutinee is `p` silences the lint") because the precise check
|
|
//! seemed to require a new side-table. In practice the side-table
|
|
//! is already there: the uniqueness pass populates `consume_count`
|
|
//! for every binder including pattern-binders. 19a.1 replaces the
|
|
//! cut with a precise per-arm walk: for each `(match p ...)`, we
|
|
//! ask whether any **heap-typed** pattern-binder of any arm has
|
|
//! `consume_count > 0`. If yes, `(own T)` is genuinely needed and
|
|
//! the lint stays silent. If no, the param is purely read by the
|
|
//! match and the lint fires — exactly the shape of fixtures like
|
|
//! `rc_own_param_drop.head_or_zero` (which only returns the head
|
|
//! and never moves the tail).
|
|
//!
|
|
//! ### Why "heap-typed" matters
|
|
//!
|
|
//! A pattern-binder of primitive type (`Int` / `Bool` / `Str` /
|
|
//! `Unit`) being consumed does not force the scrutinee to be `Own`
|
|
//! — primitives are copied out by value, no RC operation is
|
|
//! involved. The uniqueness pass bumps `consume_count` for every
|
|
//! Consume-position read regardless of type, so the lint must
|
|
//! filter primitive-typed binders out via the pattern's parent
|
|
//! ctor field types. `head_or_zero`'s body
|
|
//! `match xs { Cons(h, t) => h }` returns `h: Int`; without the
|
|
//! filter, `h.consume_count == 1` would silence the lint.
|
|
//!
|
|
//! ### Bugfix `over-strict-mode-ctor-rebuild-consume`
|
|
//!
|
|
//! The heap-typed-sub-binder check above does not see the consume
|
|
//! when an arm destructures `p`'s ctor into *primitive* fields and
|
|
//! then feeds them into a `Term::Ctor` that rebuilds an allocation —
|
|
//! e.g. `match p { State(acc, n) => State(acc, n) }`, or the nested
|
|
//! `match st { State(acc, n) => match tick { Tick(px) => State(..) } }`
|
|
//! body of `embed_backtest_step_tick`. The rebuild dismantles `p`'s
|
|
//! box and re-packages its payload into a fresh allocation; with
|
|
//! `(borrow ...)` the scrutinee's box could not be taken apart, so
|
|
//! `(own ...)` is genuinely required and the lint must stay silent.
|
|
//! `pattern_has_consumed_heap_binder` filters the primitive binders
|
|
//! out, so it could not catch this. The fix adds a second
|
|
//! recognition path (`collect_pattern_binders` +
|
|
//! `ctor_uses_any_binder`):
|
|
//! if a destructured binder of the matched arm — primitive or not —
|
|
//! is referenced by any `Term::Ctor` in the arm body, the param is
|
|
//! consumed. An earlier draft of this note mis-attributed the
|
|
//! false-positive to deeply-nested `match`-on-sub-binder; that was a
|
|
//! misdiagnosis. The defect is the unrecognised ctor-rebuild
|
|
//! consume, independent of match nesting, and it caused a *spurious*
|
|
//! warning (over-strict), never a missed one.
|
|
|
|
use crate::diagnostic::Diagnostic;
|
|
use crate::uniqueness::{infer_module as infer_uniqueness, UniquenessTable};
|
|
use ailang_core::ast::{Arm, Ctor, Def, FnDef, Module, NewArg, ParamMode, Pattern, Term, Type};
|
|
use crate::{collect_pattern_binders, strip_forall};
|
|
use ailang_surface::{term_to_form_a, type_to_form_a};
|
|
use std::collections::HashMap;
|
|
|
|
/// Per-fn inferred type of each `Term::Let` binder, teed out of the
|
|
/// typecheck pass (`synth`'s `Let` arm) so the linearity walk — which
|
|
/// runs after typechecking and does not re-infer — can read a
|
|
/// `let`-binder's type where the source pins no annotation. Keyed
|
|
/// `(def_name, binder_name)`.
|
|
pub(crate) type LetBinderTypes = HashMap<(String, String), Type>;
|
|
|
|
/// a `Type::Con` whose name is `Int`/`Bool`/`Str`/`Unit`
|
|
/// is a primitive value type — no RC, no heap. Reading a primitive
|
|
/// pattern-binder bumps `consume_count` in the uniqueness table but
|
|
/// does not force the scrutinee param to be `Own`. The lint filters
|
|
/// those out so a fn like `match xs { Cons(h, t) => h }` (where `h`
|
|
/// is `Int`) can still fire `over-strict-mode` when `t.consume_count
|
|
/// == 0`.
|
|
fn is_heap_type(t: &Type) -> bool {
|
|
match t {
|
|
Type::Con { name, .. } => !ailang_core::primitives::is_primitive_name(name),
|
|
// Type::Var (a polymorphic param) is conservatively heap —
|
|
// it could instantiate to a heap type at the call site.
|
|
Type::Var { .. } => true,
|
|
Type::Fn { .. } => true,
|
|
Type::Forall { .. } => true,
|
|
}
|
|
}
|
|
|
|
/// `true` iff `t` is an unboxed value type (`Int`/`Bool`/`Float`/`Unit`).
|
|
/// The `Str`-excluding counterpart of `is_heap_type`'s primitive check —
|
|
/// see `ailang_core::primitives::is_value_type`.
|
|
fn type_is_value(t: &Type) -> bool {
|
|
matches!(t, Type::Con { name, .. } if ailang_core::primitives::is_value_type(name))
|
|
}
|
|
|
|
/// Position in which a [`Term::Var`] is being used. See module-level
|
|
/// docs for the propagation rules.
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
|
enum Position {
|
|
/// The use takes ownership (default position).
|
|
Consume,
|
|
/// The use lends a non-owning reference for the duration of the
|
|
/// enclosing call (or, for `Term::Clone` / `Term::Match`, for the
|
|
/// duration of the read).
|
|
Borrow,
|
|
}
|
|
|
|
/// Mutable per-binder linearity state, keyed by binder name. Names
|
|
/// shadow lexically — see [`Checker::with_binder`] for the
|
|
/// save/restore pattern.
|
|
#[derive(Debug, Default, Clone)]
|
|
struct BinderState {
|
|
/// `true` once the binder has been used in a Consume position.
|
|
/// Subsequent uses (in any position) trigger `use-after-consume`.
|
|
consumed: bool,
|
|
/// Number of currently-live borrows of this binder. Incremented
|
|
/// when a Borrow-position use starts (in a fn-call arg slot or as
|
|
/// the initial state of a `Borrow` parameter); decremented when
|
|
/// the borrow ends. Consume while `> 0` triggers
|
|
/// `consume-while-borrowed`.
|
|
borrow_count: u32,
|
|
/// `true` if this binder has an unboxed value type
|
|
/// (`Int`/`Bool`/`Float`/`Unit`). A value type has no refcount and
|
|
/// is never consumed, so `use_var` skips all consume bookkeeping
|
|
/// for it. Invariant per binder; set at the introduction site
|
|
/// (param / pattern / lam) where the type is locally available.
|
|
is_value: bool,
|
|
/// For a function-typed binder (a HOF predicate param, or a
|
|
/// `let`/`lam`-bound function value), its declared parameter modes
|
|
/// — so `callee_arg_modes` can resolve `(app p h)` where `p` is a
|
|
/// local `(borrow …)` predicate and walk `h` as a borrow, not a
|
|
/// consume. `None` for a non-function binder.
|
|
fn_param_modes: Option<Vec<ParamMode>>,
|
|
}
|
|
|
|
/// top-level entry. Walks every fn in `m` and emits
|
|
/// linearity diagnostics for the all-explicit-mode fns. Other fns are
|
|
/// skipped without traversal.
|
|
///
|
|
/// Output ordering: defs in declaration order; within a def, diagnostics
|
|
/// in source-order discovery (depth-first left-to-right walk).
|
|
/// Test-only convenience entry: linearity check on a single Module
|
|
/// with no extra-visible siblings. Production code (`check_workspace`)
|
|
/// uses [`check_module_with_visible`] directly to give the walker
|
|
/// visibility into other workspace modules — in particular, the
|
|
/// auto-injected prelude's polymorphic free fns and class methods.
|
|
#[cfg(test)]
|
|
pub(crate) fn check_module(m: &Module) -> Vec<Diagnostic> {
|
|
check_module_with_visible(m, &[], &LetBinderTypes::new())
|
|
}
|
|
|
|
/// workspace-aware entry. `visible_extra` is a slice of
|
|
/// modules whose top-level fns and class methods should be visible
|
|
/// to `callee_arg_modes` (so a Borrow-mode user fn that forwards its
|
|
/// borrow params into an imported polymorphic free fn — e.g. the
|
|
/// prelude's `gt` / `ne` / `lt` — sees the imported fn's
|
|
/// `param_modes` and does not false-fire `consume-while-borrowed`).
|
|
///
|
|
/// Symmetric to the iter 23.4-prep extension that made class methods
|
|
/// visible. Bodies of the visible-extra modules are NOT walked here
|
|
/// — they are checked when their own module is processed.
|
|
pub(crate) fn check_module_with_visible(
|
|
m: &Module,
|
|
visible_extra: &[&Module],
|
|
let_binder_types: &LetBinderTypes,
|
|
) -> Vec<Diagnostic> {
|
|
let mut globals: HashMap<String, Type> = HashMap::new();
|
|
// ctor name → field types, used by the
|
|
// `over-strict-mode` lint to filter out primitive-typed
|
|
// pattern-binders (Int / Bool / Str / Unit) — their consume
|
|
// does not force ownership of the scrutinee.
|
|
let mut ctors: HashMap<String, Vec<Type>> = HashMap::new();
|
|
|
|
// register builtin signatures so `callee_arg_modes`
|
|
// resolves them when a user fn forwards a Borrow-mode param
|
|
// into a builtin like `str_clone : (Str borrow) -> Str`. The
|
|
// previously-registered class-method types (below, in the
|
|
// `Def::Class` arm) cover one half of the same problem; this
|
|
// registration covers the builtin half. Symmetric to the
|
|
// uniqueness pass's iter 24.1 builtin registration.
|
|
let mut builtins_env = crate::Env::default();
|
|
crate::builtins::install(&mut builtins_env);
|
|
for (name, ty) in &builtins_env.globals {
|
|
globals.insert(name.clone(), strip_forall(ty).clone());
|
|
}
|
|
|
|
// register fns + class methods from visible-extra
|
|
// modules first, so a name-clash inside `m` overwrites the
|
|
// imported entry (matching the synth-side scope rule that
|
|
// local-defs shadow implicit imports).
|
|
for em in visible_extra {
|
|
for def in &em.defs {
|
|
match def {
|
|
Def::Fn(f) => {
|
|
globals.insert(f.name.clone(), strip_forall(&f.ty).clone());
|
|
}
|
|
Def::Class(c) => {
|
|
for cm in &c.methods {
|
|
globals.insert(cm.name.clone(), strip_forall(&cm.ty).clone());
|
|
}
|
|
}
|
|
// Const / Type / Instance: not relevant for callee-arg-mode
|
|
// resolution (Const can't be a callee; Type's ctors are
|
|
// construction sites not fn calls; Instance bodies are
|
|
// walked separately as Def::Fn post-mono).
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
for def in &m.defs {
|
|
match def {
|
|
Def::Fn(f) => {
|
|
let ty = strip_forall(&f.ty);
|
|
globals.insert(f.name.clone(), ty.clone());
|
|
}
|
|
Def::Const(c) => {
|
|
globals.insert(c.name.clone(), strip_forall(&c.ty).clone());
|
|
}
|
|
Def::Type(td) => {
|
|
for c in &td.ctors {
|
|
let Ctor { name, fields } = c;
|
|
ctors.insert(name.clone(), fields.clone());
|
|
}
|
|
}
|
|
// class methods register their types into
|
|
// the globals map so `callee_arg_modes` can see their
|
|
// `param_modes`. Without this, a borrow-mode fn that
|
|
// forwards its borrow params into a class-method call
|
|
// fires `consume-while-borrowed` false positives.
|
|
// Instance defs stay a no-op: instance method bodies are
|
|
// walked separately via `Def::Fn` after monomorphisation.
|
|
Def::Class(c) => {
|
|
for m in &c.methods {
|
|
globals.insert(m.name.clone(), strip_forall(&m.ty).clone());
|
|
}
|
|
}
|
|
Def::Instance(_) => {}
|
|
}
|
|
}
|
|
|
|
// the over-strict-mode lint reads `consume_count` from
|
|
// the uniqueness side table. Build it once for the whole module;
|
|
// reusing the existing pass keeps the "what counts as a consume"
|
|
// definition in a single place.
|
|
let uniq = infer_uniqueness(m);
|
|
|
|
let mut diags = Vec::new();
|
|
for def in &m.defs {
|
|
if let Def::Fn(f) = def {
|
|
check_fn(f, &globals, &ctors, &uniq, let_binder_types, &mut diags);
|
|
}
|
|
}
|
|
diags
|
|
}
|
|
|
|
/// Pull a function-typed value's declared parameter modes out of its
|
|
/// type, unwrapping a leading `forall`. Returns `None` for any
|
|
/// non-function type and for a fn-type with no explicit modes (legacy
|
|
/// all-implicit), so a caller that gets `None` falls back to the
|
|
/// global symbol table exactly as before.
|
|
fn fn_modes_of(t: &Type) -> Option<Vec<ParamMode>> {
|
|
match strip_forall(t) {
|
|
Type::Fn { param_modes, .. } if !param_modes.is_empty() => Some(param_modes.clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Per-fn check. Skips nullary fns (no binders to track) — see
|
|
/// module-level scope rules.
|
|
fn check_fn(
|
|
f: &FnDef,
|
|
globals: &HashMap<String, Type>,
|
|
ctors: &HashMap<String, Vec<Type>>,
|
|
uniq: &UniquenessTable,
|
|
let_binder_types: &LetBinderTypes,
|
|
diags: &mut Vec<Diagnostic>,
|
|
) {
|
|
let inner = strip_forall(&f.ty);
|
|
let (param_tys, param_modes) = match inner {
|
|
Type::Fn { params, param_modes, .. } => (params.as_slice(), param_modes.as_slice()),
|
|
_ => return,
|
|
};
|
|
|
|
// 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, which would silently mis-skip the check otherwise.
|
|
if param_modes.len() != f.params.len() || param_tys.len() != f.params.len() {
|
|
return;
|
|
}
|
|
|
|
let mut checker = Checker {
|
|
globals,
|
|
diags,
|
|
def_name: &f.name,
|
|
ctors,
|
|
let_binder_types,
|
|
binders: HashMap::new(),
|
|
aliases: HashMap::new(),
|
|
};
|
|
|
|
// Install fn parameters as binders, with initial `borrow_count = 1`
|
|
// for `Borrow` params (the caller's outer borrow stays live for the
|
|
// body's whole duration) and `0` for `Own` params. A value-type
|
|
// param starts `is_value = true` and is exempt from consume-tracking.
|
|
for (i, (name, mode)) in f.params.iter().zip(param_modes.iter()).enumerate() {
|
|
let initial = BinderState {
|
|
consumed: false,
|
|
borrow_count: match mode {
|
|
ParamMode::Borrow => 1,
|
|
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(fn_modes_of),
|
|
};
|
|
checker.binders.insert(name.clone(), initial);
|
|
}
|
|
|
|
checker.walk(&f.body, Position::Consume);
|
|
|
|
// `over-strict-mode` lint. After the linearity
|
|
// walk has completed (and any errors have been recorded), inspect
|
|
// each `Own`-annotated param: if `consume_count(p) == 0` AND no
|
|
// pattern-binder of any `(match p ...)` arm in the body has
|
|
// `consume_count > 0`, the param could be re-annotated `Borrow`.
|
|
// Emit a Warning with a form-A rewrite.
|
|
//
|
|
// The precise sub-binder check (19a.1) replaces 19a's
|
|
// conservative "any match on `p` silences" cut: matching on `p`
|
|
// 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()))
|
|
.map(|info| info.consume_count)
|
|
.unwrap_or(0);
|
|
if consume_count != 0 {
|
|
continue;
|
|
}
|
|
if any_sub_binder_consumed_for(&f.body, pname, uniq, &f.name, ctors) {
|
|
// A pattern-binder of some `(match p ...)` arm is
|
|
// consumed → `(own T)` is genuinely needed. Stay silent.
|
|
continue;
|
|
}
|
|
diags.push(make_over_strict_mode(&f.name, pname, inner, i));
|
|
}
|
|
}
|
|
|
|
/// Per-fn linearity walker.
|
|
struct Checker<'a> {
|
|
/// Top-level symbol → type. Used to look up the param_modes of an
|
|
/// `App` callee that resolves to a global fn def.
|
|
globals: &'a HashMap<String, Type>,
|
|
/// Diagnostic accumulator (shared with the module-level walk).
|
|
diags: &'a mut Vec<Diagnostic>,
|
|
/// Name of the fn currently being checked (becomes
|
|
/// [`Diagnostic::def`]).
|
|
def_name: &'a str,
|
|
/// ctor name → field types. Used to type pattern binders so a
|
|
/// value-typed sub-binder (`Cons(h, t)` with `h: Int`) is exempted
|
|
/// from consume-tracking. Mirrors the `ctors` map built in
|
|
/// `check_module_with_visible`.
|
|
ctors: &'a HashMap<String, Vec<Type>>,
|
|
/// `(def, let-binder) → inferred type`, teed from the typecheck
|
|
/// pass. Read at the `Term::Let` arm to seed `is_value` for a
|
|
/// value-typed `let`-binder (the source pins no annotation there).
|
|
let_binder_types: &'a LetBinderTypes,
|
|
/// Live binder state, keyed by name. Modified in place; lexical
|
|
/// scoping is restored by [`Checker::with_binder`].
|
|
binders: HashMap<String, BinderState>,
|
|
/// `let`-alias → root binder name. A `(let a t …)` whose value is
|
|
/// a bare `Var t` resolving to a tracked binder records `a →
|
|
/// root(t)` here for the body scope; every binder-state lookup
|
|
/// resolves a name through this map first, so the alias shares the
|
|
/// root's consume/borrow state. Scoped: removed when the `let`
|
|
/// body ends.
|
|
aliases: HashMap<String, String>,
|
|
}
|
|
|
|
impl<'a> Checker<'a> {
|
|
/// The core position-aware walk. Recurses through `t` and updates
|
|
/// per-binder state according to the rules in the module-level
|
|
/// docs.
|
|
fn walk(&mut self, t: &Term, pos: Position) {
|
|
match t {
|
|
Term::Lit { .. } => {}
|
|
Term::Var { name } => self.use_var(name, pos),
|
|
Term::App { callee, args, .. } => {
|
|
// Applying a function value READS it (a borrow): the value
|
|
// stays live for further applications and is dropped at
|
|
// scope close like any other binder. For a global fn-ref
|
|
// callee this is a no-op (globals are untracked, so
|
|
// `use_var` returns early either way); for a tracked
|
|
// function-typed binder (a HOF param) it stops application
|
|
// from consuming the binder. The `consumed` check still
|
|
// runs in Borrow position, so applying an already-consumed
|
|
// function value still fires use-after-consume.
|
|
self.walk(callee, Position::Borrow);
|
|
|
|
// Determine arg modes from the callee's resolved type.
|
|
let arg_modes = self.callee_arg_modes(callee, args.len());
|
|
|
|
// Track which binders we lent so we can release them
|
|
// after the call.
|
|
let mut lent: Vec<String> = Vec::new();
|
|
|
|
for (i, arg) in args.iter().enumerate() {
|
|
let arg_pos = match arg_modes.get(i) {
|
|
Some(ParamMode::Borrow) => Position::Borrow,
|
|
// Own / unknown → Consume.
|
|
_ => Position::Consume,
|
|
};
|
|
self.walk(arg, arg_pos);
|
|
|
|
// If we lent a *bare* binder reference at this arg
|
|
// slot, the borrow stays live for the rest of the
|
|
// call (i.e. while subsequent args evaluate, and
|
|
// until the callee returns). Bump `borrow_count`
|
|
// now so a sibling Consume of the same binder
|
|
// triggers `consume-while-borrowed`.
|
|
if matches!(arg_pos, Position::Borrow) {
|
|
if let Term::Var { name } = arg {
|
|
let root = self.resolve_alias(name);
|
|
if let Some(s) = self.binders.get_mut(&root) {
|
|
s.borrow_count = s.borrow_count.saturating_add(1);
|
|
lent.push(root);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Call returns: all lent borrows end.
|
|
for name in lent {
|
|
if let Some(s) = self.binders.get_mut(&name) {
|
|
s.borrow_count = s.borrow_count.saturating_sub(1);
|
|
}
|
|
}
|
|
}
|
|
Term::Let { name, value, body } => {
|
|
// Class 2: a bare-Var value aliasing a tracked binder is
|
|
// a borrow-preserving rename, not a consume. Record
|
|
// `name → root` and skip the consume walk of the value;
|
|
// all binder-state lookups resolve through `aliases`.
|
|
let alias_root = match value.as_ref() {
|
|
Term::Var { name: vname } => {
|
|
let root = self.resolve_alias(vname);
|
|
if self.binders.contains_key(&root) { Some(root) } else { None }
|
|
}
|
|
_ => None,
|
|
};
|
|
if let Some(root) = alias_root {
|
|
// Shadow any outer binder / alias of `name` for the
|
|
// body, install the alias, walk, then restore both.
|
|
let prev_binder = self.binders.remove(name);
|
|
let prev_alias = self.aliases.insert(name.clone(), root);
|
|
self.walk(body, pos);
|
|
self.aliases.remove(name);
|
|
if let Some(a) = prev_alias {
|
|
self.aliases.insert(name.clone(), a);
|
|
}
|
|
if let Some(b) = prev_binder {
|
|
self.binders.insert(name.clone(), b);
|
|
}
|
|
} else {
|
|
self.walk(value, Position::Consume);
|
|
let teed = self
|
|
.let_binder_types
|
|
.get(&(self.def_name.to_string(), name.clone()));
|
|
let is_value = teed.map(type_is_value).unwrap_or(false);
|
|
let fn_param_modes = teed.and_then(fn_modes_of);
|
|
self.with_binder(
|
|
name,
|
|
BinderState { is_value, fn_param_modes, ..BinderState::default() },
|
|
|this| {
|
|
this.walk(body, pos);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
Term::LetRec { name, body, in_term, .. } => {
|
|
// The body is the recursive fn's own body; treating it as
|
|
// an opaque closure is sufficient for 18c.2 — its
|
|
// captures are conservatively traversed in `Consume`
|
|
// mode but with the recursive `name` masked (so the
|
|
// recursive self-reference does not flag as a
|
|
// use-after-consume of the in_term's view).
|
|
self.with_binder(name, BinderState::default(), |this| {
|
|
this.walk(body, Position::Consume);
|
|
this.walk(in_term, pos);
|
|
});
|
|
}
|
|
Term::If { cond, then, else_ } => {
|
|
self.walk(cond, Position::Consume);
|
|
// Branch independence: each arm starts from the pre-branch
|
|
// state. If any arm consumes/borrows a binder, that
|
|
// outcome is visible to the post-`If` continuation
|
|
// (conservative merge: union of consumed flags, max of
|
|
// borrow_count).
|
|
let saved = self.binders.clone();
|
|
self.walk(then, pos);
|
|
let after_then = std::mem::replace(&mut self.binders, saved);
|
|
self.walk(else_, pos);
|
|
merge_states(&mut self.binders, &after_then);
|
|
}
|
|
Term::Match { scrutinee, arms } => {
|
|
// Scrutinee is read (Borrow), not moved.
|
|
self.walk(scrutinee, Position::Borrow);
|
|
// Determine whether the scrutinee is a borrowed binder:
|
|
// a bare `Var` resolving (through `let`-aliases) to a
|
|
// tracked binder whose `borrow_count > 0`. A heap-typed
|
|
// sub-binder extracted from a borrowed scrutinee aliases
|
|
// the caller-owned interior, so it must inherit the
|
|
// borrow (mirroring the `Borrow`-param seeding in
|
|
// `check_fn`). Any non-`Var` scrutinee is owned/fresh.
|
|
let scrutinee_borrowed = match scrutinee.as_ref() {
|
|
Term::Var { name } => {
|
|
let root = self.resolve_alias(name);
|
|
self.binders
|
|
.get(&root)
|
|
.map(|s| s.borrow_count > 0)
|
|
.unwrap_or(false)
|
|
}
|
|
_ => false,
|
|
};
|
|
let saved = self.binders.clone();
|
|
let mut acc: Option<HashMap<String, BinderState>> = None;
|
|
for arm in arms {
|
|
self.binders = saved.clone();
|
|
self.walk_arm(arm, pos, scrutinee_borrowed);
|
|
match acc.as_mut() {
|
|
None => acc = Some(self.binders.clone()),
|
|
Some(m) => merge_states(m, &self.binders),
|
|
}
|
|
}
|
|
if let Some(merged) = acc {
|
|
self.binders = merged;
|
|
} else {
|
|
self.binders = saved;
|
|
}
|
|
}
|
|
Term::Seq { lhs, rhs } => {
|
|
self.walk(lhs, Position::Consume);
|
|
self.walk(rhs, pos);
|
|
}
|
|
Term::Ctor { args, .. } => {
|
|
// Ctor args are consumed (the new value owns them).
|
|
for a in args {
|
|
self.walk(a, Position::Consume);
|
|
}
|
|
}
|
|
Term::Do { args, .. } => {
|
|
// Effect-op args are borrowed: the op observes the value
|
|
// but ownership stays with the caller. Symmetric to the
|
|
// doc-comment bullet at :42-43.
|
|
for a in args {
|
|
self.walk(a, Position::Borrow);
|
|
}
|
|
}
|
|
Term::Lam { params, param_tys, body, .. } => {
|
|
// Lam captures cross the linearity boundary: any free
|
|
// var of the body is implicitly consumed by closure
|
|
// construction (we don't yet model captured-borrow
|
|
// discipline; that is 18c.3 territory). For 18c.2 we
|
|
// walk the body with the lam's own params as fresh
|
|
// binders; a value-typed lam param is exempt from
|
|
// consume-tracking.
|
|
let mut saved_for_params: HashMap<String, Option<BinderState>> = HashMap::new();
|
|
for (i, p) in params.iter().enumerate() {
|
|
saved_for_params.insert(p.clone(), self.binders.remove(p));
|
|
let st = BinderState {
|
|
is_value: param_tys.get(i).map(type_is_value).unwrap_or(false),
|
|
fn_param_modes: param_tys.get(i).and_then(fn_modes_of),
|
|
..BinderState::default()
|
|
};
|
|
self.binders.insert(p.clone(), st);
|
|
}
|
|
self.walk(body, Position::Consume);
|
|
for p in params {
|
|
self.binders.remove(p);
|
|
if let Some(prev) = saved_for_params.remove(p).flatten() {
|
|
self.binders.insert(p.clone(), prev);
|
|
}
|
|
}
|
|
}
|
|
Term::Clone { value } => {
|
|
// (clone X) reads X (Borrow) regardless of outer pos:
|
|
// the resulting *value* is owned, but the inner term is
|
|
// only borrowed. So `(clone xs)` does NOT consume `xs`.
|
|
self.walk(value, Position::Borrow);
|
|
}
|
|
Term::ReuseAs { source, body } => {
|
|
// linearity for `(reuse-as SRC BODY)`:
|
|
// - `SRC` must be a bare `Var { name }` of an
|
|
// in-scope binder (else `reuse-as-source-not-bare-var`).
|
|
// - The binder must currently have `consumed == false`
|
|
// (else `use-after-consume` at this site).
|
|
// - Visiting reuse-as marks the binder consumed (the
|
|
// reuse semantics is "this is where the source is
|
|
// freed/overwritten").
|
|
// - `BODY` walks normally — its sub-uses are subject
|
|
// to the usual position rules; e.g. a Consume of
|
|
// the same binder inside `BODY` will fire
|
|
// use-after-consume because reuse-as already ate it.
|
|
let resolved = match source.as_ref() {
|
|
Term::Var { name } => Some(self.resolve_alias(name)),
|
|
_ => None,
|
|
};
|
|
match resolved {
|
|
Some(root) if self.binders.contains_key(&root) => {
|
|
let already_consumed = self
|
|
.binders
|
|
.get(&root)
|
|
.map(|s| s.consumed)
|
|
.unwrap_or(false);
|
|
if already_consumed {
|
|
self.diags
|
|
.push(make_use_after_consume_at_reuse_as(self.def_name, &root, body));
|
|
} else if let Some(s) = self.binders.get_mut(&root) {
|
|
// Mark the source consumed at the reuse-as site.
|
|
s.consumed = true;
|
|
}
|
|
}
|
|
_ => {
|
|
self.diags.push(make_reuse_as_source_not_bare_var(
|
|
self.def_name,
|
|
source.as_ref(),
|
|
body,
|
|
));
|
|
}
|
|
}
|
|
// Walk the body normally; its sub-uses go through the
|
|
// standard position rules.
|
|
self.walk(body, pos);
|
|
}
|
|
Term::Loop { binders, body } => {
|
|
for b in binders {
|
|
self.walk(&b.init, Position::Consume);
|
|
}
|
|
self.walk(body, pos);
|
|
}
|
|
Term::Recur { args } => {
|
|
for a in args {
|
|
self.walk(a, Position::Consume);
|
|
}
|
|
}
|
|
// prep.2 (kernel-extension-mechanics): `(new T args...)` is
|
|
// a Ctor-like construction call — each value-arg is
|
|
// consumed by the new value. Type-args carry no runtime
|
|
// term and contribute no use.
|
|
Term::New { args, .. } => {
|
|
for arg in args {
|
|
if let NewArg::Value(v) = arg {
|
|
self.walk(v, Position::Consume);
|
|
}
|
|
}
|
|
}
|
|
Term::Intrinsic => {}
|
|
}
|
|
}
|
|
|
|
/// Walk one match arm: introduce the pattern's bindings as fresh
|
|
/// `Live` binders, walk the body, then drop them. Pattern bindings
|
|
/// are linearity-fresh in 18c.2; we do not yet track ownership
|
|
/// transfer from the scrutinee (that requires modelling pattern
|
|
/// borrow vs. consume, which is 18c.3 work).
|
|
fn walk_arm(&mut self, arm: &Arm, pos: Position, scrutinee_borrowed: bool) {
|
|
let names = collect_pattern_binders(&arm.pat);
|
|
let mut value_names: Vec<String> = Vec::new();
|
|
collect_value_pattern_binders(&arm.pat, None, self.ctors, &mut value_names);
|
|
let mut saved: HashMap<String, Option<BinderState>> = HashMap::new();
|
|
for n in &names {
|
|
saved.insert(n.clone(), self.binders.remove(n));
|
|
let is_value = value_names.contains(n);
|
|
let st = BinderState {
|
|
is_value,
|
|
// A heap-typed sub-binder of a borrowed scrutinee aliases
|
|
// the caller-owned interior; seed `borrow_count = 1` so
|
|
// consuming it fires `consume-while-borrowed`, mirroring
|
|
// the `Borrow`-param seeding in `check_fn`. Value-typed
|
|
// sub-binders (e.g. `Int h`) are copied out — no heap
|
|
// alias — so they stay freely consumable at `0`.
|
|
borrow_count: if scrutinee_borrowed && !is_value { 1 } else { 0 },
|
|
..BinderState::default()
|
|
};
|
|
self.binders.insert(n.clone(), st);
|
|
}
|
|
self.walk(&arm.body, pos);
|
|
for n in &names {
|
|
self.binders.remove(n);
|
|
if let Some(prev) = saved.remove(n).flatten() {
|
|
self.binders.insert(n.clone(), prev);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Record a use of `name` in `pos`. If `name` is not a tracked
|
|
/// binder (i.e. it's a global / built-in), we ignore the use.
|
|
///
|
|
/// Note: Borrow-position uses do **not** increment `borrow_count`
|
|
/// here. The counter tracks borrows whose lifetime extends across
|
|
/// sibling subterms — only the [`Term::App`] walker manages those
|
|
/// (incrementing before the next sibling, decrementing when the
|
|
/// call returns). A scrutinee or `Term::Clone` borrow does not
|
|
/// span siblings, so it just checks `consumed` and returns.
|
|
/// Resolve a name to the binder it ultimately denotes, following
|
|
/// `let`-alias links. A real binder shadows an alias: at each step,
|
|
/// if the current name is a tracked binder we stop there, so an
|
|
/// inner `let`/pattern/`lam` binder of the same name takes
|
|
/// precedence over an outer alias without any change to the
|
|
/// binder-introduction sites. Returns an owned `String` so the
|
|
/// immutable borrow of `self.aliases` ends before any
|
|
/// `self.binders` mutation by the caller.
|
|
fn resolve_alias(&self, name: &str) -> String {
|
|
let mut cur = name.to_string();
|
|
loop {
|
|
if self.binders.contains_key(&cur) {
|
|
return cur;
|
|
}
|
|
match self.aliases.get(&cur) {
|
|
Some(next) => cur = next.clone(),
|
|
None => return cur,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn use_var(&mut self, name: &str, pos: Position) {
|
|
let root = self.resolve_alias(name);
|
|
let state = match self.binders.get_mut(&root) {
|
|
Some(s) => s,
|
|
None => return,
|
|
};
|
|
|
|
// Already-consumed: any further use is a use-after-consume,
|
|
// regardless of position.
|
|
if state.consumed {
|
|
self.diags
|
|
.push(make_use_after_consume(self.def_name, &root));
|
|
return;
|
|
}
|
|
|
|
match pos {
|
|
Position::Borrow => {
|
|
// No state change; the App walker is responsible for
|
|
// bumping `borrow_count` when a Borrow arg lives across
|
|
// sibling args, and for the per-fn-param initial value.
|
|
}
|
|
Position::Consume => {
|
|
// A value type has no refcount and is never consumed;
|
|
// multi-use in any position is legal.
|
|
if state.is_value {
|
|
return;
|
|
}
|
|
if state.borrow_count > 0 {
|
|
self.diags
|
|
.push(make_consume_while_borrowed(self.def_name, &root));
|
|
return;
|
|
}
|
|
state.consumed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Resolve the param-mode vector of a fn-call's callee.
|
|
///
|
|
/// For `callee = Term::Var { name }`, a local function-typed binder
|
|
/// (a HOF predicate param, or a `let`/`lam`-bound function value)
|
|
/// carrying `fn_param_modes` is resolved first, then a global fn
|
|
/// type (with explicit `param_modes`). All other callee shapes
|
|
/// return an empty vec → all args default to Consume. This is the
|
|
/// conservative, "no false negatives on the borrow-arg case"
|
|
/// behaviour: if we can't see the modes, we assume Consume and may
|
|
/// miss a borrow.
|
|
fn callee_arg_modes(&self, callee: &Term, n_args: usize) -> Vec<ParamMode> {
|
|
let name = match callee {
|
|
Term::Var { name } => name,
|
|
_ => return vec![],
|
|
};
|
|
let root = self.resolve_alias(name);
|
|
// A local function-typed binder (HOF predicate param, or a
|
|
// `let`/`lam`-bound function value) carries its declared arg
|
|
// modes on its `BinderState`. Prefer those over the global
|
|
// table so `(app p h)` with `p` a local `(borrow …)` predicate
|
|
// borrows `h` instead of consuming it. A local binder shadows a
|
|
// global of the same name, which is the correct lexical scope.
|
|
if let Some(s) = self.binders.get(&root) {
|
|
if let Some(modes) = &s.fn_param_modes {
|
|
return modes.clone();
|
|
}
|
|
}
|
|
let ty = match self.globals.get(name) {
|
|
Some(t) => t,
|
|
None => {
|
|
// Type-scoped kernel ops (e.g. `RawBuf.get`) are
|
|
// registered under their BARE def name (`get`) by
|
|
// `check_module_with_visible`. When the direct lookup
|
|
// misses and the name is a class/type-shaped qualifier,
|
|
// retry under the bare method name so the borrow-receiver
|
|
// modes resolve — same treatment the rest of the checker
|
|
// gives type-scoped polymorphic ops. A lowercase
|
|
// cross-module-fn qualifier (`std_list.length`) is NOT
|
|
// class-shaped and resolves by its own path, so it is
|
|
// gated out here.
|
|
let (method_name, qualifier_opt) = crate::parse_method_qualifier(name);
|
|
if qualifier_opt.is_some() && crate::qualifier_is_class_shape(&qualifier_opt) {
|
|
match self.globals.get(method_name) {
|
|
Some(t) => t,
|
|
None => return vec![],
|
|
}
|
|
} else {
|
|
return vec![];
|
|
}
|
|
}
|
|
};
|
|
let inner = strip_forall(ty);
|
|
match inner {
|
|
Type::Fn { param_modes, .. } => {
|
|
if param_modes.is_empty() {
|
|
// All-implicit case (legacy): no mode info → all
|
|
// Consume, by the rule above.
|
|
vec![ParamMode::Own; n_args]
|
|
} else {
|
|
param_modes.clone()
|
|
}
|
|
}
|
|
_ => vec![],
|
|
}
|
|
}
|
|
|
|
/// Save the current state of `name`, install `fresh` for the
|
|
/// duration of `body`, then restore. Used by `Let` / `LetRec` /
|
|
/// `Lam` / pattern-arm to introduce a fresh binder while
|
|
/// preserving lexical scope.
|
|
fn with_binder<F: FnOnce(&mut Self)>(&mut self, name: &str, fresh: BinderState, body: F) {
|
|
let prev = self.binders.remove(name);
|
|
self.binders.insert(name.to_string(), fresh);
|
|
body(self);
|
|
self.binders.remove(name);
|
|
if let Some(p) = prev {
|
|
self.binders.insert(name.to_string(), p);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Collect the names of pattern binders that have an unboxed value
|
|
/// type, by walking the pattern alongside the ctor field types.
|
|
/// Mirrors `pattern_has_consumed_heap_binder_at`'s descent: a
|
|
/// `Pattern::Var` directly under a ctor field of value type is added;
|
|
/// a top-level `Pattern::Var` (whole-scrutinee binder, `declared_ty ==
|
|
/// None`) is conservatively treated as heap and never added.
|
|
fn collect_value_pattern_binders(
|
|
pat: &Pattern,
|
|
declared_ty: Option<&Type>,
|
|
ctors: &HashMap<String, Vec<Type>>,
|
|
out: &mut Vec<String>,
|
|
) {
|
|
match pat {
|
|
Pattern::Wild | Pattern::Lit { .. } => {}
|
|
Pattern::Var { name } => {
|
|
if let Some(t) = declared_ty {
|
|
if type_is_value(t) {
|
|
out.push(name.clone());
|
|
}
|
|
}
|
|
}
|
|
Pattern::Ctor { ctor, fields } => {
|
|
let field_tys: &[Type] = ctors.get(ctor).map(|v| v.as_slice()).unwrap_or(&[]);
|
|
for (i, f) in fields.iter().enumerate() {
|
|
collect_value_pattern_binders(f, field_tys.get(i), ctors, out);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Conservative merge of two branch states: a binder is consumed in
|
|
/// the merged state iff it was consumed in either branch; borrow_count
|
|
/// becomes the max of the two. Names present only in one map are
|
|
/// carried through.
|
|
fn merge_states(into: &mut HashMap<String, BinderState>, other: &HashMap<String, BinderState>) {
|
|
for (name, s) in other {
|
|
let entry = into.entry(name.clone()).or_default();
|
|
entry.consumed = entry.consumed || s.consumed;
|
|
entry.borrow_count = entry.borrow_count.max(s.borrow_count);
|
|
// is_value is invariant per binder; the OR recovers it when the
|
|
// binder reached `into` via a fresh `or_default()`.
|
|
entry.is_value = entry.is_value || s.is_value;
|
|
}
|
|
}
|
|
|
|
/// Build a `use-after-consume` diagnostic for `binder` in `def`.
|
|
/// Suggested rewrite: clone the offending use (the LATER use is the
|
|
/// flagged one; turning IT into `(clone <binder>)` doesn't help, but
|
|
/// the suggested rewrite is the intent — "this use should have been
|
|
/// `(clone X)`, or an earlier use should have been"). The form-A
|
|
/// snippet is `(clone <binder>)`, parseable by
|
|
/// [`ailang_surface::parse_term`].
|
|
fn make_use_after_consume(def: &str, binder: &str) -> Diagnostic {
|
|
let replacement = term_to_form_a(&Term::Clone {
|
|
value: Box::new(Term::Var { name: binder.to_string() }),
|
|
});
|
|
Diagnostic::error(
|
|
"use-after-consume",
|
|
format!("`{binder}` is used after it was already consumed"),
|
|
)
|
|
.with_def(def)
|
|
.with_ctx(serde_json::json!({"binder": binder}))
|
|
.with_suggested_rewrite(
|
|
format!(
|
|
"wrap an earlier use of `{binder}` in `(clone)` so this later use stays the unique consume"
|
|
),
|
|
replacement,
|
|
)
|
|
}
|
|
|
|
/// build a `reuse-as-source-not-bare-var` diagnostic.
|
|
/// Fires when a `Term::ReuseAs.source` is anything other than a bare
|
|
/// `Term::Var { name }` referring to an in-scope binder. The
|
|
/// suggested rewrite drops the meaningless wrapper and keeps the
|
|
/// `body` alone.
|
|
fn make_reuse_as_source_not_bare_var(def: &str, source: &Term, body: &Term) -> Diagnostic {
|
|
let got = match source {
|
|
Term::Lit { .. } => "lit",
|
|
Term::Var { .. } => "var",
|
|
Term::App { .. } => "app",
|
|
Term::Let { .. } => "let",
|
|
Term::LetRec { .. } => "let-rec",
|
|
Term::If { .. } => "if",
|
|
Term::Do { .. } => "do",
|
|
Term::Ctor { .. } => "ctor",
|
|
Term::Match { .. } => "match",
|
|
Term::Lam { .. } => "lam",
|
|
Term::Seq { .. } => "seq",
|
|
Term::Clone { .. } => "clone",
|
|
Term::ReuseAs { .. } => "reuse-as",
|
|
Term::Loop { .. } => "loop",
|
|
Term::Recur { .. } => "recur",
|
|
Term::New { .. } => "new",
|
|
Term::Intrinsic => "intrinsic",
|
|
};
|
|
let replacement = term_to_form_a(body);
|
|
Diagnostic::error(
|
|
"reuse-as-source-not-bare-var",
|
|
format!(
|
|
"reuse-as source must be a bare variable referring to an in-scope binder, got {got}"
|
|
),
|
|
)
|
|
.with_def(def)
|
|
.with_ctx(serde_json::json!({"got": got}))
|
|
.with_suggested_rewrite(
|
|
"drop the malformed reuse-as wrapper; keep the body alone",
|
|
replacement,
|
|
)
|
|
}
|
|
|
|
/// `use-after-consume` raised at a `(reuse-as <var> <body>)`
|
|
/// site whose `<var>` binder has already been consumed elsewhere. Same
|
|
/// code as the regular use-after-consume; the suggested rewrite is to
|
|
/// drop the (now unsatisfiable) reuse hint and keep the body — the
|
|
/// allocation will happen via the normal allocator.
|
|
fn make_use_after_consume_at_reuse_as(def: &str, binder: &str, body: &Term) -> Diagnostic {
|
|
let replacement = term_to_form_a(body);
|
|
Diagnostic::error(
|
|
"use-after-consume",
|
|
format!("`{binder}` is used after it was already consumed"),
|
|
)
|
|
.with_def(def)
|
|
.with_ctx(serde_json::json!({"binder": binder}))
|
|
.with_suggested_rewrite(
|
|
format!(
|
|
"drop the reuse-as hint on `{binder}`; the binder is already consumed so reuse cannot fire"
|
|
),
|
|
replacement,
|
|
)
|
|
}
|
|
|
|
/// precise sub-binder check for the over-strict-mode
|
|
/// lint. Returns `true` iff some `(match p ...)` somewhere in `t`,
|
|
/// where `p` is the param `pname`, has an arm whose pattern-binders
|
|
/// include at least one binder with `consume_count > 0` in the
|
|
/// uniqueness side table.
|
|
///
|
|
/// "Match on `p`" includes `(match p ...)` and
|
|
/// `(match (clone p) ...)` — both expose `p`'s sub-binders to the
|
|
/// arms.
|
|
///
|
|
/// Walks every other Term variant looking for further matches; a
|
|
/// `match p ...` may sit arbitrarily deep inside `If` / `Let` /
|
|
/// `App` / etc.
|
|
///
|
|
/// **Ctor-rebuild consume (bugfix
|
|
/// `over-strict-mode-ctor-rebuild-consume`):** besides a heap-typed
|
|
/// sub-binder being moved out, the param is also genuinely consumed
|
|
/// when an arm destructures `p`'s ctor and feeds the extracted
|
|
/// fields — *including purely primitive ones* — into a `Term::Ctor`
|
|
/// that builds a fresh allocation (e.g.
|
|
/// `match p { State(acc, n) => State(acc, n) }`, or the nested
|
|
/// `match st { State(acc, n) => match tick { Tick(px) => State(..) } }`
|
|
/// shape of `embed_backtest_step_tick`). The rebuild dismantles
|
|
/// `p`'s box and re-packages its payload, which `(borrow ...)` could
|
|
/// not do, so `(own ...)` is required. `pattern_has_consumed_heap_binder`
|
|
/// misses this because the destructured fields are primitive; the
|
|
/// `collect_pattern_binders` + `ctor_uses_any_binder` pair below
|
|
/// recognises it. The earlier draft of this comment mis-attributed
|
|
/// the defect
|
|
/// to a nested `match` on a sub-binder of `p`; the real mechanism is
|
|
/// the unrecognised ctor-rebuild consume, independent of match
|
|
/// nesting.
|
|
fn any_sub_binder_consumed_for(
|
|
t: &Term,
|
|
pname: &str,
|
|
uniq: &UniquenessTable,
|
|
def_name: &str,
|
|
ctors: &HashMap<String, Vec<Type>>,
|
|
) -> bool {
|
|
match t {
|
|
Term::Lit { .. } | Term::Var { .. } => false,
|
|
Term::Match { scrutinee, arms } => {
|
|
if scrutinee_matches_param(scrutinee, pname) {
|
|
for arm in arms {
|
|
if pattern_has_consumed_heap_binder(&arm.pat, uniq, def_name, ctors) {
|
|
return true;
|
|
}
|
|
// Ctor-rebuild consume: destructuring `p`'s ctor
|
|
// and feeding the extracted fields (incl.
|
|
// primitive ones) into a `Term::Ctor` builds a
|
|
// fresh allocation out of `p`'s dismantled
|
|
// payload. That genuinely consumes the `(own)`
|
|
// scrutinee — `(borrow)` could not dismantle it.
|
|
// `pattern_has_consumed_heap_binder` misses this
|
|
// because the destructured fields are primitive.
|
|
let destructured = collect_pattern_binders(&arm.pat);
|
|
if !destructured.is_empty()
|
|
&& ctor_uses_any_binder(&arm.body, &destructured)
|
|
{
|
|
return true;
|
|
}
|
|
// Recurse into the arm body: a deeper
|
|
// `match p ...` could also count.
|
|
if any_sub_binder_consumed_for(&arm.body, pname, uniq, def_name, ctors) {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
} else {
|
|
// The scrutinee isn't `p`, but `p` could still be
|
|
// matched-on inside the scrutinee or inside any arm.
|
|
if any_sub_binder_consumed_for(scrutinee, pname, uniq, def_name, ctors) {
|
|
return true;
|
|
}
|
|
arms.iter()
|
|
.any(|a| any_sub_binder_consumed_for(&a.body, pname, uniq, def_name, ctors))
|
|
}
|
|
}
|
|
Term::App { callee, args, .. } => {
|
|
any_sub_binder_consumed_for(callee, pname, uniq, def_name, ctors)
|
|
|| args
|
|
.iter()
|
|
.any(|a| any_sub_binder_consumed_for(a, pname, uniq, def_name, ctors))
|
|
}
|
|
Term::Let { value, body, .. } => {
|
|
any_sub_binder_consumed_for(value, pname, uniq, def_name, ctors)
|
|
|| any_sub_binder_consumed_for(body, pname, uniq, def_name, ctors)
|
|
}
|
|
Term::LetRec { body, in_term, .. } => {
|
|
any_sub_binder_consumed_for(body, pname, uniq, def_name, ctors)
|
|
|| any_sub_binder_consumed_for(in_term, pname, uniq, def_name, ctors)
|
|
}
|
|
Term::If { cond, then, else_ } => {
|
|
any_sub_binder_consumed_for(cond, pname, uniq, def_name, ctors)
|
|
|| any_sub_binder_consumed_for(then, pname, uniq, def_name, ctors)
|
|
|| any_sub_binder_consumed_for(else_, pname, uniq, def_name, ctors)
|
|
}
|
|
Term::Seq { lhs, rhs } => {
|
|
any_sub_binder_consumed_for(lhs, pname, uniq, def_name, ctors)
|
|
|| any_sub_binder_consumed_for(rhs, pname, uniq, def_name, ctors)
|
|
}
|
|
Term::Ctor { args, .. } | Term::Do { args, .. } => args
|
|
.iter()
|
|
.any(|a| any_sub_binder_consumed_for(a, pname, uniq, def_name, ctors)),
|
|
Term::Lam { body, .. } => any_sub_binder_consumed_for(body, pname, uniq, def_name, ctors),
|
|
Term::Clone { value } => any_sub_binder_consumed_for(value, pname, uniq, def_name, ctors),
|
|
Term::ReuseAs { source, body } => {
|
|
any_sub_binder_consumed_for(source, pname, uniq, def_name, ctors)
|
|
|| any_sub_binder_consumed_for(body, pname, uniq, def_name, ctors)
|
|
}
|
|
Term::Loop { binders, body } => {
|
|
binders.iter().any(|b| {
|
|
any_sub_binder_consumed_for(&b.init, pname, uniq, def_name, ctors)
|
|
}) || any_sub_binder_consumed_for(body, pname, uniq, def_name, ctors)
|
|
}
|
|
Term::Recur { args } => args.iter().any(|a| {
|
|
any_sub_binder_consumed_for(a, pname, uniq, def_name, ctors)
|
|
}),
|
|
// prep.2 (kernel-extension-mechanics): recurse through
|
|
// NewArg::Value subterms; NewArg::Type carries no runtime term.
|
|
Term::New { args, .. } => args.iter().any(|arg| match arg {
|
|
NewArg::Value(v) => any_sub_binder_consumed_for(v, pname, uniq, def_name, ctors),
|
|
NewArg::Type(_) => false,
|
|
}),
|
|
Term::Intrinsic => false,
|
|
}
|
|
}
|
|
|
|
/// returns `true` if `pat` introduces any **heap-typed**
|
|
/// binder whose recorded `consume_count` is `> 0`. Primitive-typed
|
|
/// pattern-binders (`Int` / `Bool` / `Str` / `Unit`) never count
|
|
/// because reading a primitive does not force the scrutinee to be
|
|
/// `Own` — the value is copied, not moved out of the scrutinee's
|
|
/// allocation. This is the key reason `match xs { Cons(h, t) => h }`
|
|
/// (head_or_zero) fires the lint: `h: Int` is filtered out, `t:
|
|
/// IntList` has `consume_count == 0`.
|
|
///
|
|
/// Walks `Pattern::Ctor` recursively. After 16a's desugar pass,
|
|
/// top-level patterns under a Ctor field are flattened into separate
|
|
/// matches — but the recursion is defensive (cheap and correct under
|
|
/// any pattern shape; nested binders are looked up by their own name
|
|
/// in `uniq`, regardless of their pattern depth).
|
|
fn pattern_has_consumed_heap_binder(
|
|
pat: &Pattern,
|
|
uniq: &UniquenessTable,
|
|
def_name: &str,
|
|
ctors: &HashMap<String, Vec<Type>>,
|
|
) -> bool {
|
|
pattern_has_consumed_heap_binder_at(pat, None, uniq, def_name, ctors)
|
|
}
|
|
|
|
/// helper carrying the optional declared type of `pat`.
|
|
/// At the top of an arm pattern, `declared_ty` is `None` (we don't
|
|
/// track the scrutinee's type in the lint); the type becomes known
|
|
/// when we descend into a `Pattern::Ctor`'s fields, where the
|
|
/// ctor's `Ctor::fields[i]` gives `fields[i]`'s type.
|
|
///
|
|
/// For a `Pattern::Var { name }` with `declared_ty == None`, we
|
|
/// conservatively treat the binder as heap (we don't know its
|
|
/// type from the pattern alone). For `declared_ty == Some(t)`, we
|
|
/// only count the binder if `is_heap_type(t)`.
|
|
fn pattern_has_consumed_heap_binder_at(
|
|
pat: &Pattern,
|
|
declared_ty: Option<&Type>,
|
|
uniq: &UniquenessTable,
|
|
def_name: &str,
|
|
ctors: &HashMap<String, Vec<Type>>,
|
|
) -> bool {
|
|
match pat {
|
|
Pattern::Wild | Pattern::Lit { .. } => false,
|
|
Pattern::Var { name } => {
|
|
// Conservative when type unknown: assume heap.
|
|
let counts = match declared_ty {
|
|
Some(t) => is_heap_type(t),
|
|
None => true,
|
|
};
|
|
if !counts {
|
|
return false;
|
|
}
|
|
uniq.get(&(def_name.to_string(), name.clone()))
|
|
.map(|info| info.consume_count > 0)
|
|
.unwrap_or(false)
|
|
}
|
|
Pattern::Ctor { ctor, fields } => {
|
|
let field_tys: &[Type] = ctors.get(ctor).map(|v| v.as_slice()).unwrap_or(&[]);
|
|
fields.iter().enumerate().any(|(i, f)| {
|
|
let ty = field_tys.get(i);
|
|
pattern_has_consumed_heap_binder_at(f, ty, uniq, def_name, ctors)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Bugfix `over-strict-mode-ctor-rebuild-consume`: `true` iff some
|
|
/// `Term::Ctor` reachable in `t` builds a fresh allocation whose
|
|
/// argument subtrees mention at least one of `binders` (the names
|
|
/// the matched arm destructured out of `p`'s ctor). That means the
|
|
/// dismantled payload of the `(own)` scrutinee — possibly via an
|
|
/// intervening expression such as `(+ acc px)` — flows into a newly
|
|
/// allocated ctor, which `(borrow ...)` could not do (a borrowed
|
|
/// value's box may not be taken apart and re-packaged). So `(own)`
|
|
/// is genuinely required and the lint must stay silent.
|
|
///
|
|
/// Conservative toward *not* suppressing: the ctor's args must
|
|
/// actually reference a destructured binder. A ctor that ignores
|
|
/// `p`'s payload (e.g. `match p { _ => Other(42) }`, or
|
|
/// `match p { P(x) => Other(42) }`) does **not** trip this and the
|
|
/// lint still warns. The binder scan is by name; a destructured
|
|
/// primitive binder shadowed and rebound before reaching the ctor
|
|
/// is not a corpus shape, and any resulting imprecision is in the
|
|
/// over-strict (extra-silence) direction the cause explicitly
|
|
/// permits, never under-strict.
|
|
fn ctor_uses_any_binder(t: &Term, binders: &[String]) -> bool {
|
|
match t {
|
|
Term::Ctor { args, .. } => {
|
|
// A freshly-built allocation: does its payload draw from
|
|
// the destructured binders (at any depth)? Also recurse
|
|
// — a ctor may be nested inside another ctor's args.
|
|
args.iter().any(|a| term_mentions_any_binder(a, binders))
|
|
|| args.iter().any(|a| ctor_uses_any_binder(a, binders))
|
|
}
|
|
Term::Lit { .. } | Term::Var { .. } => false,
|
|
Term::Match { scrutinee, arms } => {
|
|
ctor_uses_any_binder(scrutinee, binders)
|
|
|| arms.iter().any(|a| ctor_uses_any_binder(&a.body, binders))
|
|
}
|
|
Term::App { callee, args, .. } => {
|
|
ctor_uses_any_binder(callee, binders)
|
|
|| args.iter().any(|a| ctor_uses_any_binder(a, binders))
|
|
}
|
|
Term::Let { value, body, .. } => {
|
|
ctor_uses_any_binder(value, binders) || ctor_uses_any_binder(body, binders)
|
|
}
|
|
Term::LetRec { body, in_term, .. } => {
|
|
ctor_uses_any_binder(body, binders) || ctor_uses_any_binder(in_term, binders)
|
|
}
|
|
Term::If { cond, then, else_ } => {
|
|
ctor_uses_any_binder(cond, binders)
|
|
|| ctor_uses_any_binder(then, binders)
|
|
|| ctor_uses_any_binder(else_, binders)
|
|
}
|
|
Term::Seq { lhs, rhs } => {
|
|
ctor_uses_any_binder(lhs, binders) || ctor_uses_any_binder(rhs, binders)
|
|
}
|
|
Term::Do { args, .. } => args.iter().any(|a| ctor_uses_any_binder(a, binders)),
|
|
Term::Lam { body, .. } => ctor_uses_any_binder(body, binders),
|
|
Term::Clone { value } => ctor_uses_any_binder(value, binders),
|
|
Term::ReuseAs { source, body } => {
|
|
ctor_uses_any_binder(source, binders) || ctor_uses_any_binder(body, binders)
|
|
}
|
|
Term::Loop { binders: lb, body } => {
|
|
lb.iter().any(|b| ctor_uses_any_binder(&b.init, binders))
|
|
|| ctor_uses_any_binder(body, binders)
|
|
}
|
|
Term::Recur { args } => args.iter().any(|a| ctor_uses_any_binder(a, binders)),
|
|
// prep.2 (kernel-extension-mechanics): recurse through
|
|
// NewArg::Value subterms. NewArg::Type carries no runtime
|
|
// ctor invocation.
|
|
Term::New { args, .. } => args.iter().any(|arg| match arg {
|
|
NewArg::Value(v) => ctor_uses_any_binder(v, binders),
|
|
NewArg::Type(_) => false,
|
|
}),
|
|
Term::Intrinsic => false,
|
|
}
|
|
}
|
|
|
|
/// Bugfix `over-strict-mode-ctor-rebuild-consume`: deep free-variable
|
|
/// scan — does `t` mention any name in `binders` anywhere in its
|
|
/// subtree (as a `Var`, possibly under `Clone` / `App` / arithmetic /
|
|
/// nested ctor / …)? Used to test whether a `Term::Ctor`'s argument
|
|
/// draws, directly or through an intervening expression, from the
|
|
/// destructured payload of the matched `(own)` param.
|
|
fn term_mentions_any_binder(t: &Term, binders: &[String]) -> bool {
|
|
match t {
|
|
Term::Var { name } => binders.iter().any(|b| b == name),
|
|
Term::Lit { .. } => false,
|
|
Term::Ctor { args, .. } | Term::Do { args, .. } | Term::Recur { args } => {
|
|
args.iter().any(|a| term_mentions_any_binder(a, binders))
|
|
}
|
|
Term::App { callee, args, .. } => {
|
|
term_mentions_any_binder(callee, binders)
|
|
|| args.iter().any(|a| term_mentions_any_binder(a, binders))
|
|
}
|
|
Term::Match { scrutinee, arms } => {
|
|
term_mentions_any_binder(scrutinee, binders)
|
|
|| arms.iter().any(|a| term_mentions_any_binder(&a.body, binders))
|
|
}
|
|
Term::Let { value, body, .. } => {
|
|
term_mentions_any_binder(value, binders) || term_mentions_any_binder(body, binders)
|
|
}
|
|
Term::LetRec { body, in_term, .. } => {
|
|
term_mentions_any_binder(body, binders) || term_mentions_any_binder(in_term, binders)
|
|
}
|
|
Term::If { cond, then, else_ } => {
|
|
term_mentions_any_binder(cond, binders)
|
|
|| term_mentions_any_binder(then, binders)
|
|
|| term_mentions_any_binder(else_, binders)
|
|
}
|
|
Term::Seq { lhs, rhs } => {
|
|
term_mentions_any_binder(lhs, binders) || term_mentions_any_binder(rhs, binders)
|
|
}
|
|
Term::Lam { body, .. } => term_mentions_any_binder(body, binders),
|
|
Term::Clone { value } => term_mentions_any_binder(value, binders),
|
|
Term::ReuseAs { source, body } => {
|
|
term_mentions_any_binder(source, binders) || term_mentions_any_binder(body, binders)
|
|
}
|
|
Term::Loop { binders: lb, body } => {
|
|
lb.iter().any(|b| term_mentions_any_binder(&b.init, binders))
|
|
|| term_mentions_any_binder(body, binders)
|
|
}
|
|
// prep.2 (kernel-extension-mechanics): NewArg::Value subterms
|
|
// may mention any of the destructured binders; NewArg::Type
|
|
// carries no runtime term.
|
|
Term::New { args, .. } => args.iter().any(|arg| match arg {
|
|
NewArg::Value(v) => term_mentions_any_binder(v, binders),
|
|
NewArg::Type(_) => false,
|
|
}),
|
|
Term::Intrinsic => false,
|
|
}
|
|
}
|
|
|
|
/// a scrutinee "is" `pname` if it's a bare `Var { pname }`
|
|
/// or `Clone(Var { pname })`. Anything else (a fresh App result, a
|
|
/// let-binder, …) does not put `pname` directly in scrutinee
|
|
/// position.
|
|
fn scrutinee_matches_param(scrutinee: &Term, pname: &str) -> bool {
|
|
match scrutinee {
|
|
Term::Var { name } => name == pname,
|
|
Term::Clone { value } => scrutinee_matches_param(value, pname),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// build the `over-strict-mode` warning. `inner` is the
|
|
/// fn-type with the original mode; we synthesise a relaxed copy with
|
|
/// `param_modes[i] = Borrow` and render it through
|
|
/// [`type_to_form_a`] to produce the suggested rewrite.
|
|
fn make_over_strict_mode(def: &str, binder: &str, inner: &Type, i: usize) -> Diagnostic {
|
|
let relaxed = relax_param_to_borrow(inner, i);
|
|
let replacement = type_to_form_a(&relaxed);
|
|
Diagnostic::warning(
|
|
"over-strict-mode",
|
|
format!(
|
|
"param `{binder}` is annotated `(own ...)` but the body does not consume it; `(borrow ...)` would suffice"
|
|
),
|
|
)
|
|
.with_def(def)
|
|
.with_ctx(serde_json::json!({
|
|
"binder": binder,
|
|
"current_mode": "own",
|
|
"suggested_mode": "borrow",
|
|
}))
|
|
.with_suggested_rewrite(
|
|
format!("relax param `{binder}` mode from `(own ...)` to `(borrow ...)`"),
|
|
replacement,
|
|
)
|
|
}
|
|
|
|
/// clone of `inner` (assumed `Type::Fn`) with
|
|
/// `param_modes[i]` rewritten to `ParamMode::Borrow`. Other slots
|
|
/// are preserved bit-for-bit. Falls back to returning `inner`
|
|
/// unchanged if `inner` is not a `Type::Fn` (defensive — caller
|
|
/// should have screened this).
|
|
fn relax_param_to_borrow(inner: &Type, i: usize) -> Type {
|
|
match inner {
|
|
Type::Fn {
|
|
params,
|
|
param_modes,
|
|
ret,
|
|
ret_mode,
|
|
effects,
|
|
} => {
|
|
// `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;
|
|
}
|
|
Type::Fn {
|
|
params: params.clone(),
|
|
param_modes: new_modes,
|
|
ret: ret.clone(),
|
|
ret_mode: *ret_mode,
|
|
effects: effects.clone(),
|
|
}
|
|
}
|
|
other => other.clone(),
|
|
}
|
|
}
|
|
|
|
/// Build a `consume-while-borrowed` diagnostic for `binder` in `def`.
|
|
/// Suggested rewrite: clone here so the original retains the borrow.
|
|
fn make_consume_while_borrowed(def: &str, binder: &str) -> Diagnostic {
|
|
let replacement = term_to_form_a(&Term::Clone {
|
|
value: Box::new(Term::Var { name: binder.to_string() }),
|
|
});
|
|
Diagnostic::error(
|
|
"consume-while-borrowed",
|
|
format!("`{binder}` is consumed while a borrow of it is still live"),
|
|
)
|
|
.with_def(def)
|
|
.with_ctx(serde_json::json!({"binder": binder}))
|
|
.with_suggested_rewrite(
|
|
format!(
|
|
"consume `(clone {binder})` here so the original keeps its outstanding borrow"
|
|
),
|
|
replacement,
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use ailang_core::ast::{FnDef, Literal};
|
|
use std::collections::BTreeMap;
|
|
|
|
fn fn_with_modes(name: &str, modes: Vec<ParamMode>, body: Term) -> Def {
|
|
Def::Fn(FnDef {
|
|
name: name.into(),
|
|
ty: Type::Fn {
|
|
params: modes
|
|
.iter()
|
|
.map(|_| Type::Con { name: "List".into(), args: vec![] })
|
|
.collect(),
|
|
param_modes: modes.clone(),
|
|
ret: Box::new(Type::int()),
|
|
ret_mode: ParamMode::Own,
|
|
effects: vec![],
|
|
},
|
|
params: (0..modes.len()).map(|i| format!("p{i}")).collect(),
|
|
body,
|
|
suppress: vec![],
|
|
doc: None,
|
|
export: None,
|
|
})
|
|
}
|
|
|
|
/// A fn with one function-typed param `p0 : (mode (Int -> Int))`,
|
|
/// returning Int. Used to exercise HOF application linearity.
|
|
fn fn_with_fn_param(name: &str, mode: ParamMode, body: Term) -> Def {
|
|
Def::Fn(FnDef {
|
|
name: name.into(),
|
|
ty: Type::Fn {
|
|
params: vec![Type::Fn {
|
|
params: vec![Type::int()],
|
|
param_modes: vec![ParamMode::Own],
|
|
ret: Box::new(Type::int()),
|
|
ret_mode: ParamMode::Own,
|
|
effects: vec![],
|
|
}],
|
|
param_modes: vec![mode],
|
|
ret: Box::new(Type::int()),
|
|
ret_mode: ParamMode::Own,
|
|
effects: vec![],
|
|
},
|
|
params: vec!["p0".into()],
|
|
body,
|
|
suppress: vec![],
|
|
doc: None,
|
|
export: None,
|
|
})
|
|
}
|
|
|
|
/// Applying a function param more than once is a borrow each time,
|
|
/// not a consume: `(app p0 (app p0 0))` must NOT fire
|
|
/// use-after-consume. (Today the App callee is walked Consume, so
|
|
/// the first application consumes `p0` and the second fires
|
|
/// use-after-consume — this test is RED until Fix 2.)
|
|
#[test]
|
|
fn own_fn_param_applied_twice_is_clean() {
|
|
let body = Term::App {
|
|
callee: Box::new(Term::Var { name: "p0".into() }),
|
|
args: vec![Term::App {
|
|
callee: Box::new(Term::Var { name: "p0".into() }),
|
|
args: vec![Term::Lit { lit: Literal::Int { value: 0 } }],
|
|
tail: false,
|
|
}],
|
|
tail: false,
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![fn_with_fn_param("f", ParamMode::Own, body)],
|
|
};
|
|
let diags = check_module(&m);
|
|
assert!(
|
|
!diags.iter().any(|d| d.code == "use-after-consume"),
|
|
"applying a function param is a borrow, not a consume; got {diags:?}"
|
|
);
|
|
}
|
|
|
|
/// fn f(p0: (borrow (List -> Bool, inner arg Borrow)), h: (own List)).
|
|
/// The inner predicate's arg is `Borrow`, so applying `p0` to `h`
|
|
/// borrows `h` (it stays reusable). Used by the class-1 tests.
|
|
fn fn_with_borrow_pred_and_heap(name: &str, body: Term) -> Def {
|
|
let list = Type::Con { name: "List".into(), args: vec![] };
|
|
Def::Fn(FnDef {
|
|
name: name.into(),
|
|
ty: Type::Fn {
|
|
params: vec![
|
|
Type::Fn {
|
|
params: vec![list.clone()],
|
|
param_modes: vec![ParamMode::Borrow],
|
|
ret: Box::new(Type::bool_()),
|
|
ret_mode: ParamMode::Own,
|
|
effects: vec![],
|
|
},
|
|
list.clone(),
|
|
],
|
|
param_modes: vec![ParamMode::Borrow, ParamMode::Own],
|
|
ret: Box::new(Type::int()),
|
|
ret_mode: ParamMode::Own,
|
|
effects: vec![],
|
|
},
|
|
params: vec!["p0".into(), "h".into()],
|
|
body,
|
|
suppress: vec![],
|
|
doc: None,
|
|
export: None,
|
|
})
|
|
}
|
|
|
|
/// Class 1 (param path): a local borrow-mode predicate param `p0`
|
|
/// applied to a heap binder `h` (`(seq (app p0 h) h)`) does NOT
|
|
/// consume `h` — `p0`'s declared `(borrow …)` arg mode makes the
|
|
/// application a borrow, so `h` is reusable. RED until the
|
|
/// local-fn-param-mode resolution lands.
|
|
#[test]
|
|
fn borrow_fn_param_applied_does_not_consume_heap_arg() {
|
|
let body = Term::Seq {
|
|
lhs: Box::new(Term::App {
|
|
callee: Box::new(Term::Var { name: "p0".into() }),
|
|
args: vec![Term::Var { name: "h".into() }],
|
|
tail: false,
|
|
}),
|
|
rhs: Box::new(Term::Var { name: "h".into() }),
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![fn_with_borrow_pred_and_heap("f", body)],
|
|
};
|
|
let diags = check_module(&m);
|
|
assert!(
|
|
!diags.iter().any(|d| d.code == "use-after-consume"),
|
|
"applying a borrow-mode predicate param borrows its arg; got {diags:?}"
|
|
);
|
|
}
|
|
|
|
/// Class 1 (let path): a `let`-bound function value whose type is
|
|
/// teed as a `(borrow …)` predicate resolves its modes from the
|
|
/// LetBinderTypes table — applying it to heap binder `h` borrows
|
|
/// `h`. The let-value term is a trivial literal; the hand-built
|
|
/// table is the unit under test.
|
|
#[test]
|
|
fn borrow_fn_let_binder_applied_does_not_consume_heap_arg() {
|
|
let body = Term::Let {
|
|
name: "g".into(),
|
|
value: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }),
|
|
body: Box::new(Term::Seq {
|
|
lhs: Box::new(Term::App {
|
|
callee: Box::new(Term::Var { name: "g".into() }),
|
|
args: vec![Term::Var { name: "p0".into() }],
|
|
tail: false,
|
|
}),
|
|
rhs: Box::new(Term::Var { name: "p0".into() }),
|
|
}),
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![fn_with_modes("f", vec![ParamMode::Own], body)],
|
|
};
|
|
let list = Type::Con { name: "List".into(), args: vec![] };
|
|
let mut lbt = LetBinderTypes::new();
|
|
lbt.insert(
|
|
("f".into(), "g".into()),
|
|
Type::Fn {
|
|
params: vec![list.clone()],
|
|
param_modes: vec![ParamMode::Borrow],
|
|
ret: Box::new(Type::bool_()),
|
|
ret_mode: ParamMode::Own,
|
|
effects: vec![],
|
|
},
|
|
);
|
|
let diags = check_module_with_visible(&m, &[], &lbt);
|
|
assert!(
|
|
!diags.iter().any(|d| d.code == "use-after-consume"),
|
|
"a let-bound borrow predicate borrows its arg via the teed modes; got {diags:?}"
|
|
);
|
|
}
|
|
|
|
/// A fn with one `(own (con Int))` value-type param `p0`, returning
|
|
/// Int. Used to exercise the value-type exemption.
|
|
fn fn_with_int_own(name: &str, body: Term) -> Def {
|
|
Def::Fn(FnDef {
|
|
name: name.into(),
|
|
ty: Type::Fn {
|
|
params: vec![Type::Con { name: "Int".into(), args: vec![] }],
|
|
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,
|
|
})
|
|
}
|
|
|
|
/// A value-type param read in two consume positions
|
|
/// (`(seq p0 p0)`) must NOT fire use-after-consume: an `Int` has no
|
|
/// refcount and is never consumed. RED until Fix 1.
|
|
#[test]
|
|
fn value_param_multi_read_is_clean() {
|
|
let body = Term::Seq {
|
|
lhs: Box::new(Term::Var { name: "p0".into() }),
|
|
rhs: Box::new(Term::Var { name: "p0".into() }),
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![fn_with_int_own("f", body)],
|
|
};
|
|
let diags = check_module(&m);
|
|
assert!(
|
|
!diags.iter().any(|d| d.code == "use-after-consume"),
|
|
"a value-type param is never consumed; multi-read is legal; got {diags:?}"
|
|
);
|
|
}
|
|
|
|
/// A value-typed `let`-binder (type teed as `Float`) read twice
|
|
/// (`(let x 0 (seq x x))`) must NOT fire use-after-consume — the
|
|
/// `is_value` flag is seeded from the let-binder type table. RED
|
|
/// until the class-3 fix.
|
|
#[test]
|
|
fn value_let_binder_multi_read_is_clean() {
|
|
let body = Term::Let {
|
|
name: "x".into(),
|
|
value: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }),
|
|
body: Box::new(Term::Seq {
|
|
lhs: Box::new(Term::Var { name: "x".into() }),
|
|
rhs: Box::new(Term::Var { name: "x".into() }),
|
|
}),
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![fn_with_int_own("f", body)],
|
|
};
|
|
let mut lbt = LetBinderTypes::new();
|
|
lbt.insert(("f".into(), "x".into()), Type::Con { name: "Float".into(), args: vec![] });
|
|
let diags = check_module_with_visible(&m, &[], &lbt);
|
|
assert!(
|
|
!diags.iter().any(|d| d.code == "use-after-consume"),
|
|
"a value-typed let-binder is never consumed; multi-read is legal; got {diags:?}"
|
|
);
|
|
}
|
|
|
|
/// Type-gating guard: a HEAP-typed `let`-binder (teed as `List`)
|
|
/// read twice MUST still fire use-after-consume — the exemption is
|
|
/// value-type-only, not blanket. (Regression guard, green after fix.)
|
|
#[test]
|
|
fn heap_let_binder_multi_read_still_errors() {
|
|
let body = Term::Let {
|
|
name: "x".into(),
|
|
value: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }),
|
|
body: Box::new(Term::Seq {
|
|
lhs: Box::new(Term::Var { name: "x".into() }),
|
|
rhs: Box::new(Term::Var { name: "x".into() }),
|
|
}),
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![fn_with_int_own("f", body)],
|
|
};
|
|
let mut lbt = LetBinderTypes::new();
|
|
lbt.insert(("f".into(), "x".into()), Type::Con { name: "List".into(), args: vec![] });
|
|
let diags = check_module_with_visible(&m, &[], &lbt);
|
|
assert!(
|
|
diags.iter().any(|d| d.code == "use-after-consume"),
|
|
"a heap-typed let-binder consumed twice must still fire; got {diags:?}"
|
|
);
|
|
}
|
|
|
|
/// Alias soundness: `(let a p0 (seq a p0))` over an OWN heap binder
|
|
/// `p0` — consuming the alias `a` then the root `p0` is a real
|
|
/// double-consume and MUST still fire use-after-consume. The
|
|
/// redirect shares one root state; it does not mask the second
|
|
/// consume (the clone alternative would). Empty table → the alias
|
|
/// path triggers structurally on the bare-Var value.
|
|
#[test]
|
|
fn let_alias_of_owned_double_consume_still_errors() {
|
|
let body = Term::Let {
|
|
name: "a".into(),
|
|
value: Box::new(Term::Var { name: "p0".into() }),
|
|
body: Box::new(Term::Seq {
|
|
lhs: Box::new(Term::Var { name: "a".into() }),
|
|
rhs: Box::new(Term::Var { name: "p0".into() }),
|
|
}),
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![fn_with_modes("f", vec![ParamMode::Own], body)],
|
|
};
|
|
let diags = check_module(&m);
|
|
assert!(
|
|
diags.iter().any(|d| d.code == "use-after-consume"),
|
|
"consuming an alias then its root is a real double-consume; got {diags:?}"
|
|
);
|
|
}
|
|
|
|
/// Type-gating guard: a HEAP param (`List`) consumed twice in a ctor
|
|
/// (`(term-ctor Pair Pair p0 p0)`) MUST still fire use-after-consume
|
|
/// after the fix — the exemption is value-type-only, not blanket.
|
|
/// (Green today and after the fix; a regression guard, not RED-first.)
|
|
#[test]
|
|
fn heap_param_multi_consume_still_errors() {
|
|
let body = Term::Ctor {
|
|
type_name: "Pair".into(),
|
|
ctor: "Pair".into(),
|
|
args: vec![
|
|
Term::Var { name: "p0".into() },
|
|
Term::Var { name: "p0".into() },
|
|
],
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![fn_with_modes("f", vec![ParamMode::Own], body)],
|
|
};
|
|
let diags = check_module(&m);
|
|
assert!(
|
|
diags.iter().any(|d| d.code == "use-after-consume"),
|
|
"a heap param consumed twice must still error; got {diags:?}"
|
|
);
|
|
}
|
|
|
|
/// A borrow function param applied AND passed (the recursive-HOF
|
|
/// shape `map_int`) must be clean: application is a borrow, and the
|
|
/// param starts borrowed, so neither use-after-consume nor
|
|
/// consume-while-borrowed should fire. Covered by Fix 2 (App
|
|
/// borrow); this asserts the borrow-param variant. RED until Fix 2.
|
|
#[test]
|
|
fn borrow_fn_param_applied_is_clean() {
|
|
let body = Term::App {
|
|
callee: Box::new(Term::Var { name: "p0".into() }),
|
|
args: vec![Term::Lit { lit: Literal::Int { value: 0 } }],
|
|
tail: false,
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![fn_with_fn_param("f", ParamMode::Borrow, body)],
|
|
};
|
|
let diags = check_module(&m);
|
|
assert!(
|
|
!diags.iter().any(|d| {
|
|
d.code == "use-after-consume" || d.code == "consume-while-borrowed"
|
|
}),
|
|
"applying a borrow function param is a clean read; got {diags:?}"
|
|
);
|
|
}
|
|
|
|
/// A single `Own` param consumed exactly once (the body returns it)
|
|
/// is clean — no `over-strict-mode`, no use-after-consume. (Pre-0062
|
|
/// this was the `Implicit`-exemption case; post-cutover there is no
|
|
/// `Implicit`, so it pins the ordinary single-consume path.)
|
|
#[test]
|
|
fn own_param_consumed_once_is_clean() {
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![fn_with_modes(
|
|
"f",
|
|
vec![ParamMode::Own],
|
|
Term::Var { name: "p0".into() },
|
|
)],
|
|
};
|
|
assert!(check_module(&m).is_empty());
|
|
}
|
|
|
|
/// 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_borrow_own_unused_own_fires_over_strict() {
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![fn_with_modes(
|
|
"f",
|
|
vec![ParamMode::Borrow, ParamMode::Own],
|
|
Term::Lit { lit: Literal::Int { value: 0 } },
|
|
)],
|
|
};
|
|
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`:
|
|
/// clean. (Pre-19a this test exercised the same shape with `Own`
|
|
/// and asserted clean; 19a's `over-strict-mode` lint now fires on
|
|
/// `Own` + zero-consume — moved into a dedicated positive test
|
|
/// below. With `Borrow`, the lint does not apply.)
|
|
#[test]
|
|
fn explicit_fn_with_no_uses_is_clean() {
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![fn_with_modes(
|
|
"f",
|
|
vec![ParamMode::Borrow],
|
|
Term::Lit { lit: Literal::Int { value: 0 } },
|
|
)],
|
|
};
|
|
assert!(check_module(&m).is_empty());
|
|
}
|
|
|
|
/// a `(reuse-as 42 (Cons ...))` whose source is a literal
|
|
/// (not a bare `Var`) is flagged with `reuse-as-source-not-bare-var`.
|
|
/// The suggested rewrite drops the wrapper and keeps the body.
|
|
#[test]
|
|
fn reuse_as_with_non_var_source_is_reported() {
|
|
// Body shape: (reuse-as 42 (term-ctor Wrap Wrap p0))
|
|
// The source `42` is a literal — invalid for reuse-as.
|
|
let body = Term::ReuseAs {
|
|
source: Box::new(Term::Lit { lit: Literal::Int { value: 42 } }),
|
|
body: Box::new(Term::Ctor {
|
|
type_name: "Wrap".into(),
|
|
ctor: "Wrap".into(),
|
|
args: vec![Term::Var { name: "p0".into() }],
|
|
}),
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![fn_with_modes("f", vec![ParamMode::Own], body)],
|
|
};
|
|
let diags = check_module(&m);
|
|
assert_eq!(diags.len(), 1, "expected exactly one diagnostic, got {diags:?}");
|
|
let d = &diags[0];
|
|
assert_eq!(d.code, "reuse-as-source-not-bare-var");
|
|
assert_eq!(d.def.as_deref(), Some("f"));
|
|
assert_eq!(d.ctx, serde_json::json!({"got": "lit"}));
|
|
assert!(
|
|
!d.suggested_rewrites.is_empty(),
|
|
"diagnostic should carry a suggested rewrite"
|
|
);
|
|
// The replacement must round-trip through parse_term.
|
|
let rep = &d.suggested_rewrites[0].replacement;
|
|
ailang_surface::parse_term(rep)
|
|
.unwrap_or_else(|e| panic!("suggested rewrite must parse: {rep} ({e})"));
|
|
}
|
|
|
|
/// a `(reuse-as p0 ...)` where `p0` was already
|
|
/// consumed by an earlier sibling fires `use-after-consume` at the
|
|
/// reuse-as site (not `reuse-as-source-not-bare-var`).
|
|
#[test]
|
|
fn reuse_as_after_consume_is_use_after_consume() {
|
|
// Body shape: (seq p0 (reuse-as p0 (term-ctor Wrap Wrap p0)))
|
|
// The first `p0` (in `seq.lhs`) consumes the binder; the
|
|
// reuse-as in the rhs then sees an already-consumed binder.
|
|
let body = Term::Seq {
|
|
lhs: Box::new(Term::Var { name: "p0".into() }),
|
|
rhs: Box::new(Term::ReuseAs {
|
|
source: Box::new(Term::Var { name: "p0".into() }),
|
|
body: Box::new(Term::Ctor {
|
|
type_name: "Wrap".into(),
|
|
ctor: "Wrap".into(),
|
|
args: vec![Term::Lit { lit: Literal::Int { value: 0 } }],
|
|
}),
|
|
}),
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![fn_with_modes("f", vec![ParamMode::Own], body)],
|
|
};
|
|
let diags = check_module(&m);
|
|
assert_eq!(diags.len(), 1, "expected exactly one diagnostic, got {diags:?}");
|
|
let d = &diags[0];
|
|
assert_eq!(d.code, "use-after-consume");
|
|
assert_eq!(d.ctx, serde_json::json!({"binder": "p0"}));
|
|
}
|
|
|
|
// ---- Iter 19a: over-strict-mode -----------------------------------
|
|
|
|
use crate::diagnostic::Severity;
|
|
|
|
/// a fn with `(own T)` whose body merely
|
|
/// borrows the param via a `Borrow`-mode call → fires
|
|
/// `over-strict-mode` (Warning) with binder=p0, current=own,
|
|
/// suggested=borrow, and a non-empty rewrite.
|
|
#[test]
|
|
fn over_strict_mode_fires_when_param_only_borrowed() {
|
|
// Module shape:
|
|
// (fn read (params (borrow (con List))) ... ) ; helper, body irrelevant
|
|
// (fn f (params (own (con List))) ; over-strict
|
|
// body = (app read p0))
|
|
let read_def = Def::Fn(FnDef {
|
|
name: "read".into(),
|
|
ty: Type::Fn {
|
|
params: vec![Type::Con { name: "List".into(), args: vec![] }],
|
|
param_modes: vec![ParamMode::Borrow],
|
|
ret: Box::new(Type::int()),
|
|
ret_mode: ParamMode::Own,
|
|
effects: vec![],
|
|
},
|
|
params: vec!["xs".into()],
|
|
body: Term::Lit { lit: Literal::Int { value: 0 } },
|
|
suppress: vec![],
|
|
doc: None,
|
|
export: None,
|
|
});
|
|
let f_body = Term::App {
|
|
callee: Box::new(Term::Var { name: "read".into() }),
|
|
args: vec![Term::Var { name: "p0".into() }],
|
|
tail: false,
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![read_def, fn_with_modes("f", vec![ParamMode::Own], f_body)],
|
|
};
|
|
let diags = check_module(&m);
|
|
// Filter to over-strict-mode in case the helper triggers anything
|
|
// (it shouldn't — `read` is `Borrow` and param is unused).
|
|
let osm: Vec<_> = diags.iter().filter(|d| d.code == "over-strict-mode").collect();
|
|
assert_eq!(osm.len(), 1, "expected one over-strict-mode, got {diags:?}");
|
|
let d = osm[0];
|
|
assert_eq!(d.severity, Severity::Warning);
|
|
assert_eq!(d.def.as_deref(), Some("f"));
|
|
assert_eq!(
|
|
d.ctx,
|
|
serde_json::json!({
|
|
"binder": "p0",
|
|
"current_mode": "own",
|
|
"suggested_mode": "borrow",
|
|
})
|
|
);
|
|
assert!(
|
|
!d.suggested_rewrites.is_empty(),
|
|
"diagnostic should carry a suggested rewrite"
|
|
);
|
|
// 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("(params (borrow (con List)))"),
|
|
"rewrite should relax the param to `(borrow (con List))`; got {rep}"
|
|
);
|
|
}
|
|
|
|
/// a fn with `(own T)` whose body consumes
|
|
/// the param directly does not fire `over-strict-mode`.
|
|
#[test]
|
|
fn over_strict_mode_silent_when_body_consumes_param() {
|
|
// Body: (seq p0 0) — `p0` is consumed in seq.lhs.
|
|
let body = Term::Seq {
|
|
lhs: Box::new(Term::Var { name: "p0".into() }),
|
|
rhs: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }),
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![fn_with_modes("f", vec![ParamMode::Own], body)],
|
|
};
|
|
let diags = check_module(&m);
|
|
assert!(
|
|
!diags.iter().any(|d| d.code == "over-strict-mode"),
|
|
"no over-strict-mode expected; got {diags:?}"
|
|
);
|
|
}
|
|
|
|
/// a fn with `(own T)` whose body is
|
|
/// `(match p0 ...)` and never consumes `p0` directly nor any of
|
|
/// `p0`'s sub-binders DOES fire `over-strict-mode` under the
|
|
/// precise sub-binder analysis. This is the test that 19a left
|
|
/// pinned as a TODO marker; 19a.1 flipped it from a negative to
|
|
/// a positive assertion.
|
|
#[test]
|
|
fn over_strict_mode_fires_when_match_arm_uses_no_sub_binder() {
|
|
// Body: (match p0 ((pat-wild) 0))
|
|
// — no sub-binders are bound; the param is only read.
|
|
let body = Term::Match {
|
|
scrutinee: Box::new(Term::Var { name: "p0".into() }),
|
|
arms: vec![Arm {
|
|
pat: Pattern::Wild,
|
|
body: Term::Lit { lit: Literal::Int { value: 0 } },
|
|
}],
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![fn_with_modes("f", vec![ParamMode::Own], body)],
|
|
};
|
|
let diags = check_module(&m);
|
|
let osm: Vec<_> = diags.iter().filter(|d| d.code == "over-strict-mode").collect();
|
|
assert_eq!(
|
|
osm.len(),
|
|
1,
|
|
"precise rule: expected one over-strict-mode warning when the body matches on `p0` and binds no sub-consume; got {diags:?}"
|
|
);
|
|
assert_eq!(osm[0].severity, Severity::Warning);
|
|
assert_eq!(osm[0].def.as_deref(), Some("f"));
|
|
}
|
|
|
|
/// helper to build an `IntList = Nil | Cons(Int,
|
|
/// IntList)` ADT def. Used by the head_or_zero-shape tests
|
|
/// (positive + negative variants below) so the lint can look up
|
|
/// each ctor field's type and filter out primitives.
|
|
fn intlist_typedef() -> Def {
|
|
Def::Type(ailang_core::ast::TypeDef {
|
|
name: "IntList".into(),
|
|
vars: vec![],
|
|
ctors: vec![
|
|
Ctor { name: "Nil".into(), fields: vec![] },
|
|
Ctor {
|
|
name: "Cons".into(),
|
|
fields: vec![
|
|
Type::int(),
|
|
Type::Con { name: "IntList".into(), args: vec![] },
|
|
],
|
|
},
|
|
],
|
|
doc: None,
|
|
drop_iterative: false,
|
|
param_in: BTreeMap::new(),
|
|
})
|
|
}
|
|
|
|
/// helper to build a fn whose single param is
|
|
/// `(own IntList)` and whose body is `body`. Distinct from
|
|
/// `fn_with_modes` (which uses a generic `List` type for params)
|
|
/// so the test exercises a real ctor-field-types lookup.
|
|
fn fn_own_intlist(name: &str, body: Term) -> Def {
|
|
Def::Fn(FnDef {
|
|
name: name.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!["xs".into()],
|
|
body,
|
|
suppress: vec![],
|
|
doc: None,
|
|
export: None,
|
|
})
|
|
}
|
|
|
|
/// a fn with `(own IntList)` whose body
|
|
/// is `match xs { Cons(h, t) => t }` consumes the *heap-typed*
|
|
/// pattern-binder `t`. The lint must NOT fire — the
|
|
/// sub-consume forces ownership.
|
|
#[test]
|
|
fn over_strict_mode_silent_when_match_arm_consumes_sub_binder() {
|
|
// Body: (match xs ((pat-ctor Cons (pat-var h) (pat-var t)) t))
|
|
let body = Term::Match {
|
|
scrutinee: Box::new(Term::Var { name: "xs".into() }),
|
|
arms: vec![Arm {
|
|
pat: Pattern::Ctor {
|
|
ctor: "Cons".into(),
|
|
fields: vec![
|
|
Pattern::Var { name: "h".into() },
|
|
Pattern::Var { name: "t".into() },
|
|
],
|
|
},
|
|
body: Term::Var { name: "t".into() },
|
|
}],
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![intlist_typedef(), fn_own_intlist("f", body)],
|
|
};
|
|
let diags = check_module(&m);
|
|
assert!(
|
|
!diags.iter().any(|d| d.code == "over-strict-mode"),
|
|
"no over-strict-mode expected when a heap-typed arm-binder is consumed; got {diags:?}"
|
|
);
|
|
}
|
|
|
|
/// helper: a fn `sink : (own IntList) -> Int` whose only role is
|
|
/// to give an `(own)` arg slot for a consume. Its body matches and
|
|
/// moves out the heap-typed tail (`match ys { Cons(_, t) => sink(t),
|
|
/// Nil => 0 }`) so the sink genuinely consumes its `(own)` param —
|
|
/// no spurious `over-strict-mode` warning on `sink` itself to
|
|
/// confound the test.
|
|
fn fn_sink_own_intlist(name: &str) -> Def {
|
|
let body = Term::Match {
|
|
scrutinee: Box::new(Term::Var { name: "ys".into() }),
|
|
arms: vec![
|
|
Arm {
|
|
pat: Pattern::Ctor { ctor: "Nil".into(), fields: vec![] },
|
|
body: Term::Lit { lit: Literal::Int { value: 0 } },
|
|
},
|
|
Arm {
|
|
pat: Pattern::Ctor {
|
|
ctor: "Cons".into(),
|
|
fields: vec![
|
|
Pattern::Wild,
|
|
Pattern::Var { name: "rest".into() },
|
|
],
|
|
},
|
|
body: Term::App {
|
|
callee: Box::new(Term::Var { name: name.to_string() }),
|
|
args: vec![Term::Var { name: "rest".into() }],
|
|
tail: false,
|
|
},
|
|
},
|
|
],
|
|
};
|
|
Def::Fn(FnDef {
|
|
name: name.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!["ys".into()],
|
|
body,
|
|
suppress: vec![],
|
|
doc: None,
|
|
export: None,
|
|
})
|
|
}
|
|
|
|
/// helper: a fn whose single param is `(borrow IntList)` and whose
|
|
/// body is `body`. The borrow param starts the walk with
|
|
/// `borrow_count = 1`, so any in-body consume of `xs` — or of a
|
|
/// heap sub-binder extracted from `xs` — must fire
|
|
/// `consume-while-borrowed`.
|
|
fn fn_borrow_intlist(name: &str, body: Term) -> Def {
|
|
Def::Fn(FnDef {
|
|
name: name.into(),
|
|
ty: Type::Fn {
|
|
params: vec![Type::Con { name: "IntList".into(), args: vec![] }],
|
|
param_modes: vec![ParamMode::Borrow],
|
|
ret: Box::new(Type::int()),
|
|
ret_mode: ParamMode::Own,
|
|
effects: vec![],
|
|
},
|
|
params: vec!["xs".into()],
|
|
body,
|
|
suppress: vec![],
|
|
doc: None,
|
|
export: None,
|
|
})
|
|
}
|
|
|
|
/// SOUNDNESS PROPERTY (#58): a heap-typed pattern sub-binder
|
|
/// extracted from a `(borrow)`-mode match scrutinee inherits the
|
|
/// scrutinee's borrowed status, so consuming it must fire
|
|
/// `consume-while-borrowed`.
|
|
///
|
|
/// `leak : (borrow IntList)` matches `xs` and, in the `Cons(h, t)`
|
|
/// arm, passes the heap-typed tail `t` to `sink`, an
|
|
/// `(own IntList)` sink. `t` aliases the interior of the
|
|
/// caller-owned, only-borrowed `xs`; moving it into an `(own)`
|
|
/// param double-frees at runtime (SIGSEGV). The linearity pass
|
|
/// already rejects consuming the *whole* borrowed param (sibling
|
|
/// fixture `cut55_2d`), but here the sub-binder `t` is seeded as a
|
|
/// fresh `BinderState` with `borrow_count = 0`, so the consume goes
|
|
/// unflagged.
|
|
///
|
|
/// RED today: `check_module` returns no `consume-while-borrowed`
|
|
/// diagnostic. The primitive sub-binder `h` (`Int`) is correctly
|
|
/// exempt (copied out, no heap alias); the bug is specific to the
|
|
/// heap-typed `t`.
|
|
#[test]
|
|
fn consume_heap_sub_binder_of_borrow_scrutinee_is_rejected() {
|
|
// leak body: (match xs
|
|
// ((pat-ctor Nil) 0)
|
|
// ((pat-ctor Cons (pat-var h) (pat-var t)) (app sink t)))
|
|
let body = Term::Match {
|
|
scrutinee: Box::new(Term::Var { name: "xs".into() }),
|
|
arms: vec![
|
|
Arm {
|
|
pat: Pattern::Ctor { ctor: "Nil".into(), fields: vec![] },
|
|
body: Term::Lit { lit: Literal::Int { value: 0 } },
|
|
},
|
|
Arm {
|
|
pat: Pattern::Ctor {
|
|
ctor: "Cons".into(),
|
|
fields: vec![
|
|
Pattern::Var { name: "h".into() },
|
|
Pattern::Var { name: "t".into() },
|
|
],
|
|
},
|
|
body: Term::App {
|
|
callee: Box::new(Term::Var { name: "sink".into() }),
|
|
args: vec![Term::Var { name: "t".into() }],
|
|
tail: false,
|
|
},
|
|
},
|
|
],
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![
|
|
intlist_typedef(),
|
|
fn_sink_own_intlist("sink"),
|
|
fn_borrow_intlist("leak", body),
|
|
],
|
|
};
|
|
let diags = check_module(&m);
|
|
let cwb: Vec<_> = diags
|
|
.iter()
|
|
.filter(|d| d.code == "consume-while-borrowed" && d.def.as_deref() == Some("leak"))
|
|
.collect();
|
|
assert_eq!(
|
|
cwb.len(),
|
|
1,
|
|
"consuming the heap-typed tail `t` of a borrowed scrutinee must fire \
|
|
consume-while-borrowed (it aliases caller-owned heap → double-free); got {diags:?}"
|
|
);
|
|
assert_eq!(
|
|
cwb[0].ctx,
|
|
serde_json::json!({ "binder": "t" }),
|
|
"the diagnostic must name the consumed sub-binder `t`; got {diags:?}"
|
|
);
|
|
}
|
|
|
|
/// RED (over-strict-mode FP on ctor-rebuild): a `State(Float,
|
|
/// Int)` ADT def. Both fields are primitive — the lint's
|
|
/// `pattern_has_consumed_heap_binder` filters them out — yet a
|
|
/// body that destructures `State` and rebuilds it still consumes
|
|
/// the scrutinee's allocation.
|
|
fn state_typedef() -> Def {
|
|
Def::Type(ailang_core::ast::TypeDef {
|
|
name: "State".into(),
|
|
vars: vec![],
|
|
ctors: vec![Ctor {
|
|
name: "State".into(),
|
|
fields: vec![Type::float(), Type::int()],
|
|
}],
|
|
doc: None,
|
|
drop_iterative: false,
|
|
param_in: BTreeMap::new(),
|
|
})
|
|
}
|
|
|
|
/// RED helper: a fn whose single param is `(own State)` and whose
|
|
/// body is `body`. Mirrors `fn_own_intlist` but over `State` so
|
|
/// the ctor-field-type lookup resolves to `(Float, Int)`.
|
|
fn fn_own_state(name: &str, body: Term) -> Def {
|
|
Def::Fn(FnDef {
|
|
name: name.into(),
|
|
ty: Type::Fn {
|
|
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::Own,
|
|
effects: vec![],
|
|
},
|
|
params: vec!["st".into()],
|
|
body,
|
|
suppress: vec![],
|
|
doc: None,
|
|
export: None,
|
|
})
|
|
}
|
|
|
|
/// RED, negative property: a fn with `(own State)` whose body is
|
|
/// `match st { State(acc, n) => State(acc, n) }` — it
|
|
/// destructures the owned scrutinee and **rebuilds the same
|
|
/// ctor** from the extracted fields. The old `State` allocation
|
|
/// is dismantled by the rebuild, so `(own State)` is genuinely
|
|
/// required; `(borrow State)` would NOT suffice. The lint must
|
|
/// therefore stay silent.
|
|
///
|
|
/// This is the minimal isolating shape behind the
|
|
/// `embed_backtest_step_tick` over-strict-mode false positive:
|
|
/// `any_sub_binder_consumed_for` only recognises a consume when a
|
|
/// *heap-typed* pattern-binder is moved out. When every
|
|
/// destructured field is primitive (`acc: Float`, `n: Int`) but
|
|
/// they feed a `term-ctor` rebuilding the scrutinee's own ctor,
|
|
/// the rebuild's re-consumption of the payload is invisible to
|
|
/// the check and the lint spuriously fires. No nested `match` and
|
|
/// no second param are needed to trigger it — the defect is the
|
|
/// unrecognised ctor-rebuild consume, not match nesting.
|
|
#[test]
|
|
fn over_strict_mode_silent_when_ctor_rebuilt_from_primitive_fields() {
|
|
// Body: (match st
|
|
// ((pat-ctor State (pat-var acc) (pat-var n))
|
|
// (term-ctor State (var acc) (var n))))
|
|
let body = Term::Match {
|
|
scrutinee: Box::new(Term::Var { name: "st".into() }),
|
|
arms: vec![Arm {
|
|
pat: Pattern::Ctor {
|
|
ctor: "State".into(),
|
|
fields: vec![
|
|
Pattern::Var { name: "acc".into() },
|
|
Pattern::Var { name: "n".into() },
|
|
],
|
|
},
|
|
body: Term::Ctor {
|
|
type_name: "State".into(),
|
|
ctor: "State".into(),
|
|
args: vec![
|
|
Term::Var { name: "acc".into() },
|
|
Term::Var { name: "n".into() },
|
|
],
|
|
},
|
|
}],
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![state_typedef(), fn_own_state("f", body)],
|
|
};
|
|
let diags = check_module(&m);
|
|
assert!(
|
|
!diags.iter().any(|d| d.code == "over-strict-mode"),
|
|
"no over-strict-mode expected: the `term-ctor State` rebuild \
|
|
re-consumes the destructured payload of the `(own State)` \
|
|
scrutinee, so `(own)` is genuinely required; got {diags:?}"
|
|
);
|
|
}
|
|
|
|
/// a fn with `(own IntList)` whose body
|
|
/// is `match xs { Nil => 0, Cons(h, t) => h }` returns the
|
|
/// **primitive-typed** `h` (`Int`) and never moves the
|
|
/// heap-typed `t`. The lint MUST fire — `(borrow IntList)`
|
|
/// would have sufficed. This is exactly
|
|
/// `rc_own_param_drop.head_or_zero`'s shape.
|
|
#[test]
|
|
fn over_strict_mode_fires_when_match_arm_binders_unused() {
|
|
// Body: (match xs
|
|
// ((pat-ctor Nil) 0)
|
|
// ((pat-ctor Cons (pat-var h) (pat-var t)) h))
|
|
let body = Term::Match {
|
|
scrutinee: Box::new(Term::Var { name: "xs".into() }),
|
|
arms: vec![
|
|
Arm {
|
|
pat: Pattern::Ctor {
|
|
ctor: "Nil".into(),
|
|
fields: vec![],
|
|
},
|
|
body: Term::Lit { lit: Literal::Int { value: 0 } },
|
|
},
|
|
Arm {
|
|
pat: Pattern::Ctor {
|
|
ctor: "Cons".into(),
|
|
fields: vec![
|
|
Pattern::Var { name: "h".into() },
|
|
Pattern::Var { name: "t".into() },
|
|
],
|
|
},
|
|
body: Term::Var { name: "h".into() },
|
|
},
|
|
],
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![intlist_typedef(), fn_own_intlist("f", body)],
|
|
};
|
|
let diags = check_module(&m);
|
|
let osm: Vec<_> = diags.iter().filter(|d| d.code == "over-strict-mode").collect();
|
|
assert_eq!(
|
|
osm.len(),
|
|
1,
|
|
"expected one over-strict-mode warning (head_or_zero shape); got {diags:?}"
|
|
);
|
|
assert_eq!(osm[0].severity, Severity::Warning);
|
|
assert_eq!(osm[0].def.as_deref(), Some("f"));
|
|
assert_eq!(
|
|
osm[0].ctx,
|
|
serde_json::json!({
|
|
"binder": "xs",
|
|
"current_mode": "own",
|
|
"suggested_mode": "borrow",
|
|
})
|
|
);
|
|
}
|
|
|
|
/// `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
|
|
/// is two diagnostics (one per param) because
|
|
/// `linearity::check_module`'s globals-construction loop skips
|
|
/// `Def::Class`, so `callee_arg_modes` cannot see the method's
|
|
/// `param_modes` and defaults each call argument to `Consume`.
|
|
#[test]
|
|
fn class_method_borrow_call_does_not_fire_consume_while_borrowed() {
|
|
use ailang_core::ast::{ClassDef, ClassMethod};
|
|
|
|
let class_eq = Def::Class(ClassDef {
|
|
name: "TestEq".into(),
|
|
param: "a".into(),
|
|
superclass: None,
|
|
methods: vec![ClassMethod {
|
|
name: "teq".into(),
|
|
ty: Type::Fn {
|
|
params: vec![
|
|
Type::Var { name: "a".into() },
|
|
Type::Var { name: "a".into() },
|
|
],
|
|
param_modes: vec![ParamMode::Borrow, ParamMode::Borrow],
|
|
ret: Box::new(Type::bool_()),
|
|
ret_mode: ParamMode::Own,
|
|
effects: vec![],
|
|
},
|
|
default: None,
|
|
}],
|
|
doc: None,
|
|
});
|
|
|
|
let ne = Def::Fn(FnDef {
|
|
name: "tne".into(),
|
|
ty: Type::Fn {
|
|
params: vec![
|
|
Type::Var { name: "a".into() },
|
|
Type::Var { name: "a".into() },
|
|
],
|
|
param_modes: vec![ParamMode::Borrow, ParamMode::Borrow],
|
|
ret: Box::new(Type::bool_()),
|
|
ret_mode: ParamMode::Own,
|
|
effects: vec![],
|
|
},
|
|
params: vec!["x".into(), "y".into()],
|
|
body: Term::App {
|
|
callee: Box::new(Term::Var { name: "teq".into() }),
|
|
args: vec![
|
|
Term::Var { name: "x".into() },
|
|
Term::Var { name: "y".into() },
|
|
],
|
|
tail: false,
|
|
},
|
|
suppress: vec![],
|
|
doc: None,
|
|
export: None,
|
|
});
|
|
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "m".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![class_eq, ne],
|
|
};
|
|
|
|
let diags = check_module(&m);
|
|
assert!(
|
|
diags.is_empty(),
|
|
"expected no diagnostics but got {:#?}",
|
|
diags
|
|
);
|
|
}
|
|
}
|