Skip to main content

bock_types/
checker.rs

1//! Bidirectional type inference engine — T-AIR pass.
2//!
3//! This module implements the type checker / inference engine for Bock.
4//! It walks an [`AIRNode`] module produced by the S-AIR lowering pass and:
5//!
6//! - **Synthesizes** types for expressions bottom-up (`infer_expr`).
7//! - **Checks** expressions against an expected type top-down (`check_expr`).
8//! - Annotates every node with a [`TypeInfo`] and records the resolved
9//!   [`Type`] in an internal side-table keyed by [`NodeId`].
10//!
11//! # Architecture
12//!
13//! `check_module` performs a two-sub-pass approach:
14//! 1. **Collect** — all top-level function signatures are entered into the
15//!    type environment so that mutually-recursive calls resolve.
16//! 2. **Check** — each top-level item is fully type-checked.
17//!
18//! During checking, the internal `infer_node` / `check_node` helpers
19//! recursively walk the AIR tree via `&mut AIRNode`, recording types in
20//! the side-table and stamping `type_info` on every node.
21//!
22//! The public `infer_expr` / `check_expr` methods provide read-only access
23//! to the inference result for single nodes (no mutation — useful in tests
24//! and for downstream passes that want to query type of a specific node).
25
26use std::collections::{HashMap, HashSet};
27use std::sync::atomic::{AtomicU32, Ordering};
28
29use bock_air::stubs::{TypeInfo, Value};
30use bock_air::{AIRNode, EnumVariantPayload, NodeId, NodeKind};
31use bock_ast::{BinOp, GenericParam, Literal, TypeConstraint, TypeExpr, TypePath, UnaryOp};
32use bock_errors::{DiagnosticBag, DiagnosticCode, Span};
33
34use crate::traits::{resolve_impl, ImplTable, TraitRef};
35use crate::{
36    unify, EffectRef, FnType, GenericType, PrimitiveType, Substitution, Type, TypeError, TypeVarId,
37};
38
39// ─── Diagnostic codes ─────────────────────────────────────────────────────────
40
41const E_TYPE_MISMATCH: DiagnosticCode = DiagnosticCode {
42    prefix: 'E',
43    number: 4001,
44};
45const E_UNDEFINED_VAR: DiagnosticCode = DiagnosticCode {
46    prefix: 'E',
47    number: 4002,
48};
49const E_ARITY_MISMATCH: DiagnosticCode = DiagnosticCode {
50    prefix: 'E',
51    number: 4003,
52};
53const E_NOT_CALLABLE: DiagnosticCode = DiagnosticCode {
54    prefix: 'E',
55    number: 4004,
56};
57const E_WHERE_CLAUSE: DiagnosticCode = DiagnosticCode {
58    prefix: 'E',
59    number: 4005,
60};
61/// `E4015` — an `==`/`!=` operand (or an `Equatable` bound instantiation) is
62/// not `Equatable` (DQ29, §18.5). Records/enums conform structurally iff every
63/// field / variant payload type conforms; compound built-ins compose
64/// conditionally; classes are excluded (explicit `impl Equatable` only); a
65/// non-Equatable leaf (e.g. an `Fn` field) poisons the whole type. The message
66/// names the offending field path and type; the note suggests the fix
67/// (`impl Equatable for <T>` or removing the comparison). Sibling of the
68/// `Comparable` ordering-operator gate, which reuses `E4005`.
69const E_NOT_EQUATABLE: DiagnosticCode = DiagnosticCode {
70    prefix: 'E',
71    number: 4015,
72};
73/// `E4012` — a `.into()` call (or `from`/`try_from`) could not be resolved:
74/// no `From`/`Into`/`TryFrom` impl exists for the required source and target
75/// types. For `.into()` the target is taken from the expected type, so the
76/// call site must have a reachable expected type (a `let y: U =`, an `fn -> U`
77/// return position, or an argument to a typed parameter); see the v1
78/// annotation-required limitation in the `core.convert` docs.
79const E_NO_CONVERSION: DiagnosticCode = DiagnosticCode {
80    prefix: 'E',
81    number: 4012,
82};
83/// `E4013` — a method that does not exist on the receiver's **concrete** type
84/// was called. This is the general "no such method" error: when the receiver
85/// resolves to a fully-known type (a primitive, a built-in collection, an
86/// `Optional`/`Result`, or a user record/class/enum whose definition is in
87/// scope) and the method is in none of that type's method sets (intrinsic,
88/// canonical-trait, inherent-impl, trait-impl, or bounded-trait), the call is
89/// rejected instead of being silently resolved to a fresh type variable (which
90/// would pass `bock check` yet emit no/garbage codegen — the DQ22 soundness
91/// hole). When a near-miss method name exists the diagnostic carries a "did you
92/// mean `…`?" suggestion (e.g. DQ22's `m.contains(k)` → `contains_key`).
93///
94/// The error is deliberately NOT raised for non-concrete receivers — inference
95/// variables (`Type::TypeVar`), §4.9 `Flexible`/sketch-mode types, the `Error`
96/// poison sentinel, function/tuple receivers, or user types whose definition
97/// is not in scope — so aggressive sketch-mode narrowing keeps resolving
98/// methods by design.
99const E_NO_SUCH_METHOD: DiagnosticCode = DiagnosticCode {
100    prefix: 'E',
101    number: 4013,
102};
103/// `E4014` — a `use` declaration named a module path with **neither a
104/// brace-list nor a wildcard** (a bare `use core.error`). Per §12.2 / DQ8 this
105/// is not a v1 import form; module-qualified access is deferred to v1.x. The
106/// checker rejects it and points at the braced form (`use core.error.{…}`).
107const E_BARE_MODULE_IMPORT: DiagnosticCode = DiagnosticCode {
108    prefix: 'E',
109    number: 4014,
110};
111/// `E6006` — the lambda-handler surface `Effect.handler(...)` is **reserved
112/// until v1.x** (§10.4). v1 supports exactly one handler form: a record with
113/// an `impl <Effect> for <Record>`, installed via `handle <Effect> with
114/// <record>` (module level) or `handling (<Effect> with <record>) { ... }`
115/// (block level). Before this code existed the form surfaced as a doubled,
116/// rule-less `E4002 undefined variable` at the effect name
117/// (Q-diag-effect-violation-errors); it now names the actual rule. Lives in
118/// the `6xxx` effects family because the violated rule is an effect-system
119/// rule, even though the emitting pass is the type checker.
120const E_RESERVED_LAMBDA_HANDLER: DiagnosticCode = DiagnosticCode {
121    prefix: 'E',
122    number: 6006,
123};
124
125// ─── Receiver-type annotation (checker → codegen) ──────────────────────────────
126
127/// AIR metadata key under which the checker stamps a method call's *receiver
128/// type category* so codegen can lower receiver-dependent calls without
129/// re-deriving the type.
130///
131/// The checker resolves a method call's receiver type during inference but
132/// then drops its internal type side-table; codegen sees only the structural
133/// AIR. A bare `(1).compare(2)` and `opt.unwrap_or(d)` are indistinguishable
134/// to a backend without this hint — both desugar to
135/// `Call(FieldAccess(recv, method), [recv, …])`, and the method names overlap
136/// across `Optional`/`Result`/`List`. This key carries the resolved receiver
137/// category from the checker's method-resolution sites to codegen.
138///
139/// The value is a [`Value::String`] produced by [`recv_kind_tag`]; see that
140/// function for the tag grammar. The tag is stamped on the *method-call node*
141/// (the desugared `Call`, or a `MethodCall` that survives to T-AIR), not on the
142/// receiver — so a backend reads `call_node.metadata[RECV_KIND_META_KEY]`.
143pub const RECV_KIND_META_KEY: &str = "recv_kind";
144
145/// Metadata key stamped on a `BinaryOp { op: Add, .. }` node whose two operands
146/// resolved to `List[T]`, marking the `+` as **list concatenation** rather than
147/// numeric/string addition.
148///
149/// Codegen reads this (a `Value::Bool(true)`) to lower the `+` to each target's
150/// list-concat idiom (`[...a, ...b]` on JS/TS, a clone-and-extend on Rust, an
151/// `append(append(...), ...)` helper on Go, native `+` on Python where list `+`
152/// is already concatenation). Without it the operator falls through to the native
153/// `+`, which fails to compile on TS/Rust/Go and silently *string*-concatenates
154/// on JS. The element type is intentionally not recorded — every target's concat
155/// is element-type-agnostic.
156pub const LIST_CONCAT_META_KEY: &str = "list_concat";
157
158/// Metadata key stamped on a `+` `BinaryOp` node whose operands resolve to
159/// `String`, marking it as string concatenation. Bock's `+` on strings is
160/// concatenation, but Rust's `String + String` does not compile (`Add<String>`
161/// is unimplemented — only `String + &str`), so the Rust backend reads this stamp
162/// to lower the operator to `format!("{}{}", l, r)` regardless of whether each
163/// side is an owned `String` or a `&str`. A purely syntactic check in codegen
164/// cannot see that a bare identifier/parameter (`result + sep`) is `String`-typed,
165/// so the type-aware checker records it here. The other backends concatenate
166/// strings with `+` natively and ignore this key.
167pub const STRING_CONCAT_META_KEY: &str = "string_concat";
168
169/// Metadata key stamped on a `BinaryOp { op: Div | Rem, .. }` node whose two
170/// operands both resolve to an **integer** primitive (`Int`, the sized
171/// `Int8`…`Int128` / `UInt8`…`UInt64`). It marks the `/` or `%` as *integer*
172/// division / remainder — distinct from float (true) division — so codegen can
173/// lower it to the cross-target integer semantics fixed by DQ23 (§3.6):
174///
175/// - **`/` truncates toward zero** (`-17 / 5 == -3`, not the floor `-4`), yields
176///   `Int`, and **aborts on a zero divisor** (a Panic ambient effect, §10.5).
177/// - **`%` is the remainder of that truncated division**, taking the sign of the
178///   *dividend* (`-17 % 5 == -2`, `17 % -5 == 2`), and likewise aborts on zero.
179///
180/// Rust and Go already match this with native `/` / `%`, so their backends ignore
181/// the stamp. JS/TS need `Math.trunc` plus an explicit zero-abort (JS `/` is float
182/// division and `Math.trunc(a/0)` yields `Infinity`, not a throw). Python needs an
183/// integer-only toward-zero helper (its `//` *floors* and `int(a/b)` routes
184/// through lossy float division) and a dividend-sign `%` helper (its `%` follows
185/// floor division). A purely syntactic codegen check cannot see that bare
186/// identifiers (`a / b`) are integer-typed, so the type-aware checker records it
187/// here. The value is a `Value::Bool(true)`.
188pub const INT_ARITH_META_KEY: &str = "int_arith";
189
190/// Metadata key stamped on an expression node whose resolved type is `Bool` and
191/// whose value is about to be *stringified* — an `${expr}` interpolation part or
192/// the receiver of a `.to_string()` / `.display()` call. It tells codegen to emit
193/// the **canonical lowercase spelling** `"true"` / `"false"` (§3.5), matching the
194/// Bool literals, rather than the target's native default.
195///
196/// JS/TS template literals and Rust/Go formatting already print lowercase, so
197/// those backends ignore the stamp. Python is the outlier: `f"{b}"` and `str(b)`
198/// print the capitalized `True` / `False`, so the Python backend reads this stamp
199/// to map the value through a lowercase conversion. The interpolation expression
200/// part's resolved type is not otherwise reachable from codegen (it lives only in
201/// the dropped type side-table), so the checker records it on the node directly.
202/// The value is a `Value::Bool(true)`.
203pub const BOOL_STRINGIFY_META_KEY: &str = "bool_stringify";
204
205/// Metadata key stamped on a `BinaryOp { op: Lt | Le | Gt | Ge, .. }` node whose
206/// two operands resolve to a **user** (`Named` record / class) type that
207/// implements `Comparable` (§18.5). It marks the ordering operator as a
208/// *user-type* comparison that must be lowered through the type's
209/// `compare(self, other) -> Ordering` method rather than the target's native
210/// `<` / `<=` / `>` / `>=`:
211///
212/// - `a < b`  ⇒ `a.compare(b) == Less`
213/// - `a > b`  ⇒ `a.compare(b) == Greater`
214/// - `a <= b` ⇒ `a.compare(b) != Greater`
215/// - `a >= b` ⇒ `a.compare(b) != Less`
216///
217/// Every backend lowers native `<` on two user values to a broken form — Python
218/// raises `TypeError`, Rust/Go fail to compile (structs are not ordered), and JS
219/// coerces the objects to `NaN` and silently yields `false`. Reusing the
220/// per-target `Ordering` representation the stdlib already emits (`{ _tag: … }`
221/// in JS/TS, the `_bock_*` singletons in Python, the `Ordering::*` variants in
222/// Rust, the `Ordering*` structs in Go) keeps the lowering aligned with how a
223/// hand-written `a.compare(b)` call already lowers.
224///
225/// The checker is the only pass that can see that two bare identifiers
226/// (`a < b`) are a user `Comparable` type, so it records the marker here; codegen
227/// reads the operator off the node itself. The value is a `Value::Bool(true)`.
228/// Only the *ordering* operators are stamped — `==` / `!=` (Equatable) are a
229/// separate lane and are never stamped here. Primitive comparisons and bounded
230/// generic (`T: Comparable`) comparisons are likewise untouched: they already
231/// lower correctly through the native operator / the trait-bound bridge.
232pub const USER_COMPARE_META_KEY: &str = "user_compare";
233
234/// Metadata key stamped on a `BinaryOp { op: Eq | Ne, .. }` node whose operands
235/// need a non-native equality lowering on at least one target (DQ29, §18.5
236/// structural Equatable). The value is a `Value::String` naming the lane:
237///
238/// - **`"impl"`** — the operand is a user (`Named`) type with an **explicit**
239///   `impl Equatable`. Every backend must dispatch `==`/`!=` through the
240///   impl's `eq(self, other) -> Bool` (negated for `!=`): native equality is
241///   reference identity on JS/TS, field-wise on Python/Go, and a compile error
242///   on Rust — none of which honor the user's `eq`
243///   (Q-js-user-equality-reference / #339 and siblings).
244/// - **`"structural"`** — the operand is a structurally-Equatable shape
245///   (record / enum / tuple) containing no collection. Targets whose native
246///   `==` is already field-wise (Python dataclasses, Go structs/interfaces,
247///   Rust with the [`DERIVE_EQ_META_KEY`] derive) keep the native operator;
248///   JS/TS lower through the `__bockEq` deep-equality runtime helper because
249///   `===` on two objects is reference identity.
250/// - **`"deep"`** — the operand (transitively) involves a `List`/`Map`/`Set`
251///   (or `Optional`/`Result` wrapper). Same JS/TS lowering as `"structural"`;
252///   Go must additionally route through its deep-equality runtime helper
253///   (slices and maps do not support `==` at all — a compile error), with
254///   `Map`/`Set` equality required to be order-independent.
255/// - **`"generic"`** — the operand is an unsolved type variable carrying an
256///   `Equatable` (or `Comparable`, via the supertrait edge) bound inside a
257///   generic fn body. JS/TS lower through `__bockEq` (the concrete
258///   instantiation may be a record); the other targets' native equality is
259///   correct under their bound mapping (`PartialEq` on Rust, `comparable` on
260///   Go, duck-typed `==` on Python).
261///
262/// Primitive operands are never stamped — every target's native `==` is
263/// correct for them (including the IEEE `NaN != NaN` Float semantics, which
264/// the structural lanes inherit per the DQ10 caveat).
265pub const USER_EQ_META_KEY: &str = "user_eq";
266
267/// Metadata key stamped on a `RecordDecl` / `EnumDecl` node that conforms to
268/// `Equatable` **structurally** (DQ29, §18.5): every field / variant payload
269/// type is Equatable and the type declares no explicit `impl Equatable` (the
270/// explicit impl suppresses the structural default, and its `==` routes through
271/// `eq` instead — see [`USER_EQ_META_KEY`]).
272///
273/// Consumed by the Rust backend, which adds `PartialEq` to the type's
274/// `#[derive(..)]` list so native `==`/`!=` (and containment like
275/// `Vec<T> == Vec<T>`) compile. For a generic record/enum the derive's
276/// conditional `where` bounds implement rule 4 (a `Pair[A, B]` instantiation
277/// is Equatable iff `A` and `B` are) natively. A type that is **not**
278/// structurally Equatable (e.g. an `Fn` field) is left underivable — the
279/// checker's operator gate already rejects every `==` over it, so the emitted
280/// code never needs `PartialEq`. The value is a `Value::Bool(true)`.
281pub const DERIVE_EQ_META_KEY: &str = "derive_structural_eq";
282
283/// Metadata key stamped on a `RecordDecl` / `EnumDecl` node that carries an
284/// explicit `impl Equatable` (DQ31, §18.5). The type's custom `eq` IS its
285/// equality, so it must hold the same inside a container as outside.
286///
287/// Consumed by the **Rust** backend, which emits a `impl PartialEq` that
288/// DELEGATES to the custom `eq` (`Equatable::eq(self, other)`), rather than the
289/// structural `#[derive(PartialEq)]` of [`DERIVE_EQ_META_KEY`]. This makes a
290/// `Vec<T>`/`HashMap`/`HashSet` of the type compare through the custom `eq`
291/// natively — the same equality the standalone `==` already routes through
292/// `eq` (the `"impl"` lane). Without it, `Vec<T> == Vec<T>` is `E0369` (the
293/// type derives no `PartialEq`); with a *structural* derive instead it would
294/// pin the WRONG equality into containers. The value is a `Value::Bool(true)`.
295pub const CUSTOM_EQ_META_KEY: &str = "custom_eq_impl";
296
297/// Base node id for AIR nodes the checker synthesizes (the `for`-over-`Iterable`
298/// desugar). Chosen high enough to sit far above the dense, zero-based ids the
299/// lowerer assigns to real nodes, so synthesized nodes never collide with real
300/// ones for the `id`-equality checks codegen performs. A `u32` leaves ample
301/// headroom above this base for the handful of nodes one module's `for` loops
302/// expand to.
303const SYNTH_ID_BASE: NodeId = 0x4000_0000;
304
305/// One enum variant's payload entry in `TypeChecker::enum_variant_payloads`
306/// (DQ29): the variant name and its `(component_label, type)` list — field
307/// names for struct variants, `_0`-style indices for tuple payloads, empty
308/// for unit variants.
309type EnumVariantPayloadTypes = (String, Vec<(String, Type)>);
310
311/// Compute the receiver-kind tag for a resolved receiver [`Type`].
312///
313/// The tag is a compact, codegen-facing string identifying which *family* the
314/// receiver belongs to, so a backend can pick the right lowering for an
315/// overloaded method name (`compare`/`eq`/`unwrap_or`/`is_ok`/…):
316///
317/// | Receiver type            | Tag                |
318/// |--------------------------|--------------------|
319/// | `Type::Primitive(Int)`   | `"Primitive:Int"`  |
320/// | `Type::Primitive(Float)` | `"Primitive:Float"`|
321/// | `Type::Optional(_)`      | `"Optional"`       |
322/// | `Type::Result(_, _)`     | `"Result"`         |
323/// | `List[T]`                | `"List"`           |
324/// | `Set[T]` / `Map[K,V]`    | `"Set"` / `"Map"`  |
325/// | other `Generic`          | `"Generic:<ctor>"` |
326/// | `Named(n)`               | `"User:<n>"`       |
327///
328/// Returns `None` for type-inference variables, function types, and other
329/// receivers a backend never needs to special-case — leaving the call to its
330/// existing structural lowering.
331///
332/// One further tag exists that this function cannot produce because it needs
333/// the bounds table: a *bounded type variable* receiver (`a.compare(b)` where
334/// `a: T` and `T: Comparable`) is stamped `"TraitBound:<Trait>"` by the
335/// checker's `stamp_recv_kind`, signalling that the method dispatches through a
336/// trait bound rather than a concrete type.
337///
338/// The primitive variant carries the specific [`PrimitiveType`] (via its
339/// `Debug` name, e.g. `Int`, `Float`, `String`) because the lowering of e.g.
340/// `compare` differs by primitive (Rust `i64::cmp` vs `f64::partial_cmp`).
341#[must_use]
342pub fn recv_kind_tag(ty: &Type) -> Option<String> {
343    match ty {
344        Type::Primitive(p) => Some(format!("Primitive:{p:?}")),
345        Type::Optional(_) => Some("Optional".to_string()),
346        Type::Result(_, _) => Some("Result".to_string()),
347        Type::Generic(g) => match g.constructor.as_str() {
348            "List" => Some("List".to_string()),
349            "Set" => Some("Set".to_string()),
350            "Map" => Some("Map".to_string()),
351            other => Some(format!("Generic:{other}")),
352        },
353        Type::Named(n) => Some(format!("User:{}", n.name)),
354        _ => None,
355    }
356}
357
358// ─── TypeVarId generator ──────────────────────────────────────────────────────
359
360/// Generates unique [`TypeVarId`]s across a compilation session.
361struct TypeVarGen {
362    counter: AtomicU32,
363}
364
365impl TypeVarGen {
366    fn new() -> Self {
367        Self {
368            counter: AtomicU32::new(0),
369        }
370    }
371
372    fn next(&self) -> TypeVarId {
373        self.counter.fetch_add(1, Ordering::SeqCst)
374    }
375}
376
377// ─── TypeEnv ──────────────────────────────────────────────────────────────────
378
379/// Scoped type environment: maps variable/function names to their [`Type`]s.
380///
381/// Maintains a stack of scopes; inner scopes shadow outer ones.
382pub struct TypeEnv {
383    scopes: Vec<HashMap<String, Type>>,
384}
385
386impl TypeEnv {
387    /// Create a new environment with a single (global) scope.
388    #[must_use]
389    pub fn new() -> Self {
390        Self {
391            scopes: vec![HashMap::new()],
392        }
393    }
394
395    /// Push a new inner scope onto the stack.
396    pub fn push_scope(&mut self) {
397        self.scopes.push(HashMap::new());
398    }
399
400    /// Pop the innermost scope from the stack.
401    ///
402    /// Panics in debug builds if the global scope would be popped.
403    pub fn pop_scope(&mut self) {
404        debug_assert!(self.scopes.len() > 1, "cannot pop the global scope");
405        self.scopes.pop();
406    }
407
408    /// Bind `name` to `ty` in the current (innermost) scope.
409    pub fn define(&mut self, name: impl Into<String>, ty: Type) {
410        let scope = self.scopes.last_mut().expect("at least one scope");
411        scope.insert(name.into(), ty);
412    }
413
414    /// Look up `name`, searching from the innermost scope outward.
415    #[must_use]
416    pub fn lookup(&self, name: &str) -> Option<&Type> {
417        self.scopes.iter().rev().find_map(|s| s.get(name))
418    }
419}
420
421impl Default for TypeEnv {
422    fn default() -> Self {
423        Self::new()
424    }
425}
426
427// ─── Function signature ───────────────────────────────────────────────────────
428
429/// Cached function signature for use during call-site type checking.
430#[derive(Debug, Clone)]
431struct FnSig {
432    /// Names of the generic type parameters (e.g. `["T", "U"]`).
433    generic_params: Vec<String>,
434    /// [`TypeVarId`]s assigned to each generic parameter during signature
435    /// collection. Used by [`TypeChecker::replace_type_vars`] to create
436    /// per-call-site fresh instantiations.
437    generic_var_ids: Vec<TypeVarId>,
438    /// Types of the value parameters (after substituting generic parameters
439    /// with fresh type variables when the function is instantiated).
440    param_types: Vec<Type>,
441    /// Return type.
442    return_type: Type,
443    /// Where-clause constraints (trait bounds on generic parameters).
444    where_clause: Vec<TypeConstraint>,
445}
446
447// ─── TypeChecker ──────────────────────────────────────────────────────────────
448
449/// Bidirectional type checker / inference engine.
450///
451/// # Usage
452/// ```ignore
453/// let mut checker = TypeChecker::new();
454/// checker.check_module(&mut air_module);
455/// // inspect checker.diags for errors
456/// // query checker.type_of(node_id) for resolved types
457/// ```
458pub struct TypeChecker {
459    /// Scoped variable / function type environment.
460    pub env: TypeEnv,
461    /// Accumulated type-variable substitution from unification.
462    pub subst: Substitution,
463    /// Diagnostics emitted during type checking.
464    pub diags: DiagnosticBag,
465    /// Generator for fresh type variable ids.
466    var_gen: TypeVarGen,
467    /// Side-table: resolved type for each AIR node id.
468    types: HashMap<NodeId, Type>,
469    /// Known function signatures (populated during the collection phase).
470    fn_sigs: HashMap<String, FnSig>,
471    /// Stack of expected return types for the current function body.
472    return_ty_stack: Vec<Type>,
473    /// Trait impl table for checking where-clause bounds at call sites.
474    /// When `None`, trait-bound checking is skipped.
475    pub impl_table: Option<ImplTable>,
476    /// Trait impls pulled in from imported modules (Q-xmod-impl), as
477    /// `(trait_name, trait_args, target_type)`. Populated by `seed_imports`
478    /// before `check_module`, then folded into the freshly-built `impl_table`
479    /// in `check_module` so cross-module `.into()` (and `From`/`Into`
480    /// resolution) and cross-module where-clause bounds see impls declared in
481    /// other modules.
482    imported_trait_impls: Vec<(String, Vec<Type>, Type)>,
483    /// Methods from inherent impl blocks: type_name → method_name → fn_type.
484    method_types: HashMap<String, HashMap<String, Type>>,
485    /// A method's OWN generic type-parameter names, keyed
486    /// type_name → method_name → \[param_name, …\]. Populated during
487    /// `collect_sig` for `ImplBlock`/`ClassDecl` methods that declare their own
488    /// `[U, …]` params (distinct from the type's params, which live in
489    /// `record_generic_params`). At a call site these names are substituted with
490    /// fresh inference variables so the method's own params are inferred from the
491    /// arguments — the method-level analogue of free-function call inference
492    /// (Q-checker-method-generic-call-infer). The type's own params stay as
493    /// `Named(_)` placeholders pinned by the receiver via `record_generic_params`.
494    method_generic_params: HashMap<String, HashMap<String, Vec<String>>>,
495    /// Effect operation types: effect_name → [(op_name, fn_type)].
496    /// Populated during `collect_sig` for `EffectDecl` nodes.
497    effect_op_types: HashMap<String, Vec<(String, Type)>>,
498    /// Component effects for composite effects: effect_name → Vec\<component_name\>.
499    effect_components: HashMap<String, Vec<String>>,
500    /// Record field types: record_name → [(field_name, field_type)].
501    /// Populated during `collect_sig` for `RecordDecl` nodes.
502    record_field_types: HashMap<String, Vec<(String, Type)>>,
503    /// Generic type parameter names for records: record_name → Vec\<param_name\>.
504    /// Populated during `collect_sig` for `RecordDecl` nodes with generics.
505    record_generic_params: HashMap<String, Vec<String>>,
506    /// Type alias mappings: alias_name → underlying type.
507    /// Populated during `collect_sig` for `TypeAlias` nodes.
508    type_aliases: HashMap<String, Type>,
509    /// Trait method signatures: trait_name → (method_name → fn_type).
510    /// The fn_type uses `Named("Self")` for the self parameter so
511    /// callers can substitute the concrete receiver type.
512    /// Populated during `collect_sig` for `TraitDecl` nodes.
513    trait_method_types: HashMap<String, HashMap<String, Type>>,
514    /// Trait bounds on type variables: TypeVarId → [trait_name].
515    /// Populated during `check_fn_decl` from inline generic param bounds
516    /// and where-clause constraints. Used by the FieldAccess handler to
517    /// resolve methods on bounded type parameters.
518    type_var_bounds: HashMap<TypeVarId, Vec<String>>,
519    /// Generic-param trait bounds of the function body currently being checked,
520    /// keyed by param NAME (e.g. `"T" → ["Comparable"]`). Populated at the top
521    /// of [`TypeChecker::check_fn_decl`] from both the inline `[T: Trait]` form
522    /// and the `where (T: Trait)` form, and restored on exit (so a nested
523    /// method body sees its own params, not the outer scope's). Consulted by
524    /// [`TypeChecker::check_trait_bounds_at_call`] via
525    /// [`TypeChecker::abstract_param_satisfies_bound`]: when a call inside a
526    /// generic function passes that function's own type parameter (which
527    /// resolves to an abstract `Named(param)` rather than a concrete type) to a
528    /// callee with the same bound, the bound is satisfied *because the enclosing
529    /// scope already requires it* — without this, a self-referential generic
530    /// like `from_list[T: Comparable]` calling `add[T: Comparable](…, x: T)`
531    /// would be falsely rejected since `T` has no concrete impl.
532    current_fn_param_bounds: HashMap<String, Vec<String>>,
533    /// Names of locally-declared `class` types. Populated during `collect_sig`
534    /// for `ClassDecl` nodes. The DQ29 structural-Equatable predicate consults
535    /// this to EXCLUDE classes from the structural default (a class sits on
536    /// the data/identity line and gets `==` only via an explicit
537    /// `impl Equatable`); records and classes otherwise share
538    /// `record_field_types`, so the field table alone cannot distinguish them.
539    class_names: HashSet<String>,
540    /// Variant payload types for locally-declared enums:
541    /// enum_name → \[(variant_name, \[(component_label, type), …\]), …\].
542    /// Component labels are field names for struct variants and `_0`-style
543    /// indices for tuple payloads; unit variants contribute an empty list.
544    /// Generic param references are stored SYMBOLICALLY as `Named(param)` (the
545    /// same convention `record_field_types` uses for generic records) so the
546    /// DQ29 structural-Equatable predicate can substitute instantiation
547    /// arguments at the use site. Populated during `collect_sig` for
548    /// `EnumDecl` nodes; imported enums are absent (their payload types do not
549    /// cross the export ABI), which the predicate treats as conservatively
550    /// conforming.
551    enum_variant_payloads: HashMap<String, Vec<EnumVariantPayloadTypes>>,
552    /// Monotonic node-id source for AIR nodes the checker *synthesizes* (today
553    /// only the `for`-over-`Iterable` desugar, see
554    /// [`TypeChecker::desugar_for_iterable`]). The checker is constructed
555    /// without the session's `NodeIdGen`, so it mints its own ids from a high
556    /// base (`SYNTH_ID_BASE`) chosen to sit far above the dense, zero-based range
557    /// the lowerer assigns — synthesized nodes therefore never collide with real
558    /// nodes for the `id`-equality checks codegen relies on (e.g. the
559    /// desugared-method-call receiver-identity test in the generator).
560    synth_id: std::cell::Cell<NodeId>,
561    /// Per-checker counter that makes each synthesized `for`-loop iterator
562    /// binding name (`__bock_iter_<n>`) unique, so nested desugared `for` loops
563    /// do not shadow one another.
564    synth_iter_var: std::cell::Cell<u32>,
565    /// `(name, span)` pairs for which an undefined-name diagnostic has
566    /// already been emitted. The AIR lowerer desugars a method call
567    /// `recv.m(args)` into `Call { callee: FieldAccess(recv, m), args:
568    /// [recv, args…] }`, **duplicating the receiver node** — so the same
569    /// source expression is inferred twice (once inside the callee's
570    /// `FieldAccess`, once as `args[0]`). For well-typed code the double
571    /// inference is harmless, but an undefined receiver used to produce the
572    /// same `E4002` twice at the identical span
573    /// (Q-diag-effect-violation-errors). One root cause must emit one
574    /// diagnostic (diagnostics-review rubric #6), so emission sites consult
575    /// this set first.
576    reported_undefined: HashSet<(String, Span)>,
577}
578
579impl TypeChecker {
580    /// Create a new, empty type checker.
581    #[must_use]
582    pub fn new() -> Self {
583        Self {
584            env: TypeEnv::new(),
585            subst: Substitution::new(),
586            diags: DiagnosticBag::new(),
587            var_gen: TypeVarGen::new(),
588            types: HashMap::new(),
589            fn_sigs: HashMap::new(),
590            return_ty_stack: Vec::new(),
591            impl_table: None,
592            imported_trait_impls: Vec::new(),
593            method_types: HashMap::new(),
594            method_generic_params: HashMap::new(),
595            effect_op_types: HashMap::new(),
596            effect_components: HashMap::new(),
597            record_field_types: HashMap::new(),
598            record_generic_params: HashMap::new(),
599            type_aliases: HashMap::new(),
600            trait_method_types: HashMap::new(),
601            type_var_bounds: HashMap::new(),
602            current_fn_param_bounds: HashMap::new(),
603            class_names: HashSet::new(),
604            enum_variant_payloads: HashMap::new(),
605            synth_id: std::cell::Cell::new(SYNTH_ID_BASE),
606            synth_iter_var: std::cell::Cell::new(0),
607            reported_undefined: HashSet::new(),
608        }
609    }
610
611    // ── TypeVarId allocation ─────────────────────────────────────────────────
612
613    /// Allocate a fresh type-inference variable.
614    fn fresh_var(&self) -> Type {
615        Type::TypeVar(self.var_gen.next())
616    }
617
618    // ── Synthesized-node helpers (for the `for`-over-`Iterable` desugar) ──────
619
620    /// Mint a fresh, collision-free [`NodeId`] for a synthesized AIR node.
621    ///
622    /// Ids are drawn monotonically from [`SYNTH_ID_BASE`], far above the
623    /// lowerer's dense zero-based range, so a synthesized node's id never
624    /// equals a real node's id (which codegen's receiver-identity check and the
625    /// per-module item dedup rely on).
626    fn next_synth_id(&self) -> NodeId {
627        let id = self.synth_id.get();
628        self.synth_id.set(id.wrapping_add(1));
629        id
630    }
631
632    /// Build a synthesized AIR node carrying a fresh id, the given `span`, and
633    /// the `scope_id` metadata downstream passes expect (copied from the `for`
634    /// node so the synthesized subtree lives in the loop's lexical scope).
635    fn synth_node(&self, span: Span, scope_id: i64, kind: NodeKind) -> AIRNode {
636        let mut node = AIRNode::new(self.next_synth_id(), span, kind);
637        node.metadata
638            .insert("scope_id".to_string(), Value::Int(scope_id));
639        node
640    }
641
642    /// Build a synthesized identifier-reference node for a local binding.
643    fn synth_ident(&self, name: &str, span: Span, scope_id: i64) -> AIRNode {
644        self.synth_node(
645            span,
646            scope_id,
647            NodeKind::Identifier {
648                name: bock_ast::Ident {
649                    name: name.to_string(),
650                    span,
651                },
652            },
653        )
654    }
655
656    /// Build a synthesized zero-argument method call on `receiver`, in the SAME
657    /// desugared shape the lowerer produces (`Call { callee: FieldAccess(recv,
658    /// method), args: [self = recv] }`). The receiver is cloned into both the
659    /// field-access object and the `self` arg with the *same* node id, matching
660    /// the lowerer so codegen's receiver-identity check recognises the call as a
661    /// method call rather than a field-closure invocation.
662    fn synth_method_call(
663        &self,
664        receiver: AIRNode,
665        method: &str,
666        span: Span,
667        scope_id: i64,
668    ) -> AIRNode {
669        let field_access = self.synth_node(
670            span,
671            scope_id,
672            NodeKind::FieldAccess {
673                object: Box::new(receiver.clone()),
674                field: bock_ast::Ident {
675                    name: method.to_string(),
676                    span,
677                },
678            },
679        );
680        let self_arg = bock_air::AirArg {
681            label: None,
682            value: receiver,
683        };
684        self.synth_node(
685            span,
686            scope_id,
687            NodeKind::Call {
688                callee: Box::new(field_access),
689                args: vec![self_arg],
690                type_args: vec![],
691            },
692        )
693    }
694
695    /// Build a synthesized `Some`/`None`-style constructor pattern.
696    fn synth_ctor_pat(
697        &self,
698        ctor: &str,
699        fields: Vec<AIRNode>,
700        span: Span,
701        scope_id: i64,
702    ) -> AIRNode {
703        self.synth_node(
704            span,
705            scope_id,
706            NodeKind::ConstructorPat {
707                path: TypePath {
708                    segments: vec![bock_ast::Ident {
709                        name: ctor.to_string(),
710                        span,
711                    }],
712                    span,
713                },
714                fields,
715            },
716        )
717    }
718
719    /// Rewrite a `for <pattern> in <iterable> { <body> }` whose `iterable`
720    /// implements `Iterable` into the proven manual-drive shape, in place.
721    ///
722    /// The `node` (a [`NodeKind::For`]) is rewritten to:
723    ///
724    /// ```text
725    /// {
726    ///   let mut __bock_iter_N = <iterable>.iter();
727    ///   loop {
728    ///     match __bock_iter_N.next() {
729    ///       Some(<pattern>) => <body>
730    ///       None            => break
731    ///     }
732    ///   }
733    /// }
734    /// ```
735    ///
736    /// The user's `<pattern>`, `<iterable>`, and `<body>` are *moved* (not
737    /// cloned) out of the original `For` node into the synthesized subtree.
738    /// After rewriting, the caller infers the new subtree through the normal
739    /// [`TypeChecker::infer_node`] path, so the `match`/`Some(pat)`/method-call
740    /// nodes pick up exactly the metadata codegen needs (the `Optional[T]`
741    /// payload typing, receiver-kind tags, copy-type marks). The synthesized
742    /// `loop` is native on every target, and the user's `break`/`continue` land
743    /// inside the `Some` arm body, targeting that loop.
744    ///
745    /// `iter_var` names the binding; passing a per-loop-unique name keeps nested
746    /// desugared `for` loops from shadowing one another.
747    fn desugar_for_iterable(
748        &self,
749        node: &mut AIRNode,
750        pattern: AIRNode,
751        iterable: AIRNode,
752        body: AIRNode,
753        iter_var: &str,
754    ) {
755        let span = node.span;
756        let scope_id = node
757            .metadata
758            .get("scope_id")
759            .and_then(|v| match v {
760                Value::Int(i) => Some(*i),
761                _ => None,
762            })
763            .unwrap_or(0);
764
765        // `let mut __bock_iter_N = <iterable>.iter()`
766        let iter_call = self.synth_method_call(iterable, "iter", span, scope_id);
767        let let_pat = self.synth_node(
768            span,
769            scope_id,
770            NodeKind::BindPat {
771                name: bock_ast::Ident {
772                    name: iter_var.to_string(),
773                    span,
774                },
775                is_mut: true,
776            },
777        );
778        let let_binding = self.synth_node(
779            span,
780            scope_id,
781            NodeKind::LetBinding {
782                is_mut: true,
783                pattern: Box::new(let_pat),
784                ty: None,
785                value: Box::new(iter_call),
786            },
787        );
788
789        // `match __bock_iter_N.next() { Some(<pattern>) => <body>; None => break }`
790        let next_recv = self.synth_ident(iter_var, span, scope_id);
791        let next_call = self.synth_method_call(next_recv, "next", span, scope_id);
792        let some_pat = self.synth_ctor_pat("Some", vec![pattern], span, scope_id);
793        let some_arm = self.synth_node(
794            span,
795            scope_id,
796            NodeKind::MatchArm {
797                pattern: Box::new(some_pat),
798                guard: None,
799                body: Box::new(body),
800            },
801        );
802        let none_pat = self.synth_ctor_pat("None", vec![], span, scope_id);
803        let break_node = self.synth_node(span, scope_id, NodeKind::Break { value: None });
804        let none_arm = self.synth_node(
805            span,
806            scope_id,
807            NodeKind::MatchArm {
808                pattern: Box::new(none_pat),
809                guard: None,
810                body: Box::new(break_node),
811            },
812        );
813        let match_node = self.synth_node(
814            span,
815            scope_id,
816            NodeKind::Match {
817                scrutinee: Box::new(next_call),
818                arms: vec![some_arm, none_arm],
819            },
820        );
821
822        // `loop { <match> }`
823        let loop_body = self.synth_node(
824            span,
825            scope_id,
826            NodeKind::Block {
827                stmts: vec![match_node],
828                tail: None,
829            },
830        );
831        let loop_node = self.synth_node(
832            span,
833            scope_id,
834            NodeKind::Loop {
835                body: Box::new(loop_body),
836            },
837        );
838
839        // `{ <let>; <loop> }` — replace the `for` node's kind in place.
840        node.kind = NodeKind::Block {
841            stmts: vec![let_binding, loop_node],
842            tail: None,
843        };
844    }
845
846    // ── Side-table helpers ───────────────────────────────────────────────────
847
848    /// Record the type of `node` in the side-table (after applying the current
849    /// substitution), stamp the node's `type_info` slot, and return the type.
850    fn record(&mut self, node: &mut AIRNode, ty: Type) -> Type {
851        let resolved = self.subst.apply(&ty);
852        self.types.insert(node.id, resolved.clone());
853        node.type_info = Some(TypeInfo {
854            resolved_type: None,
855        });
856        // Mark primitive types as copy so ownership analysis skips moves.
857        if matches!(resolved, Type::Primitive(_)) {
858            node.metadata.insert("copy_type".into(), Value::Bool(true));
859        }
860        resolved
861    }
862
863    /// Look up the resolved type for `node_id` from the side-table.
864    #[must_use]
865    pub fn type_of(&self, id: NodeId) -> Option<&Type> {
866        self.types.get(&id)
867    }
868
869    /// Stamp the receiver-kind annotation ([`RECV_KIND_META_KEY`]) onto a
870    /// method-call `node` from the resolved `receiver_ty`.
871    ///
872    /// This is the checker→codegen lynchpin (see [`recv_kind_tag`]): at the
873    /// method-resolution sites the checker already knows the receiver type, so
874    /// it records the receiver *category* on the call node for codegen to read
875    /// after the type side-table is dropped. No-ops when the receiver type maps
876    /// to no tag (inference var, function type, …), leaving the call to its
877    /// existing structural lowering. The receiver type is run through the
878    /// current substitution first so a late-unified type var resolves.
879    ///
880    /// Bounded type variables get a bespoke tag the plain [`recv_kind_tag`]
881    /// cannot produce (it has no access to the bounds table): when the resolved
882    /// receiver is a `Type::TypeVar` carrying a trait bound — the receiver of
883    /// `a.compare(b)` inside `max[T: Comparable](a, b)` — the tag is
884    /// `"TraitBound:<Trait>"` (the first bound). This tells codegen the method
885    /// dispatches through that trait rather than a concrete type. A new *value*
886    /// of the existing `recv_kind` metadata key (same mechanism as
887    /// `Primitive:…`/`User:…`/`Optional`), so it does not touch the AIR shape,
888    /// the export ABI, or any visitor.
889    fn stamp_recv_kind(&self, node: &mut AIRNode, receiver_ty: &Type) {
890        let resolved = self.subst.apply(receiver_ty);
891        let tag = match &resolved {
892            Type::TypeVar(id) => self
893                .type_var_bounds
894                .get(id)
895                .and_then(|bounds| bounds.first())
896                .map(|trait_name| format!("TraitBound:{trait_name}")),
897            _ => recv_kind_tag(&resolved),
898        };
899        if let Some(tag) = tag {
900            node.metadata
901                .insert(RECV_KIND_META_KEY.to_string(), Value::String(tag));
902        }
903    }
904
905    // ── Getters for export collection ───────────────────────────────────────
906
907    /// Record field types: record_name → [(field_name, field_type)].
908    #[must_use]
909    pub fn record_field_types(&self) -> &HashMap<String, Vec<(String, Type)>> {
910        &self.record_field_types
911    }
912
913    /// Generic type parameter names for records: record_name → Vec\<param_name\>.
914    #[must_use]
915    pub fn record_generic_params(&self) -> &HashMap<String, Vec<String>> {
916        &self.record_generic_params
917    }
918
919    /// Effect operation types: effect_name → [(op_name, fn_type)].
920    #[must_use]
921    pub fn effect_op_types(&self) -> &HashMap<String, Vec<(String, Type)>> {
922        &self.effect_op_types
923    }
924
925    /// Component effects for composite effects: effect_name → Vec\<component_name\>.
926    #[must_use]
927    pub fn effect_components(&self) -> &HashMap<String, Vec<String>> {
928        &self.effect_components
929    }
930
931    /// Inherent impl method signatures: type_name → (method_name → fn_type).
932    #[must_use]
933    pub fn method_types(&self) -> &HashMap<String, HashMap<String, Type>> {
934        &self.method_types
935    }
936
937    /// Trait method signatures: trait_name → (method_name → fn_type).
938    #[must_use]
939    pub fn trait_method_types(&self) -> &HashMap<String, HashMap<String, Type>> {
940        &self.trait_method_types
941    }
942
943    /// Type alias mappings: alias_name → underlying type.
944    #[must_use]
945    pub fn type_aliases(&self) -> &HashMap<String, Type> {
946        &self.type_aliases
947    }
948
949    /// Where-clause trait bounds on a generic function's type parameters,
950    /// keyed by the [`TypeVarId`] the parameter was assigned during signature
951    /// collection.
952    ///
953    /// Returns `(var_id, [trait_name, …])` pairs — one entry per generic
954    /// parameter that carries at least one bound. The `var_id` is the same id
955    /// that appears as `?<id>` in the function's exported [`Type`] string, so
956    /// the export ABI can encode the bound against the right type variable
957    /// (Q-xmod-bounds). Returns an empty vec for an unknown or non-generic
958    /// function, or one with no where-clause bounds.
959    #[must_use]
960    pub fn fn_where_bounds(&self, name: &str) -> Vec<(TypeVarId, Vec<String>)> {
961        let Some(sig) = self.fn_sigs.get(name) else {
962            return vec![];
963        };
964        // Map generic-param name → its TypeVarId (positional zip).
965        let name_to_id: HashMap<&str, TypeVarId> = sig
966            .generic_params
967            .iter()
968            .zip(sig.generic_var_ids.iter())
969            .map(|(n, id)| (n.as_str(), *id))
970            .collect();
971
972        let mut out: Vec<(TypeVarId, Vec<String>)> = Vec::new();
973        for clause in &sig.where_clause {
974            let Some(&var_id) = name_to_id.get(clause.param.name.as_str()) else {
975                continue; // bound on an unknown param — already diagnosed
976            };
977            let traits: Vec<String> = clause.bounds.iter().map(type_path_to_name).collect();
978            if traits.is_empty() {
979                continue;
980            }
981            // Merge bounds for the same param (multiple where-clauses or
982            // inline + where) so the export carries the full set.
983            if let Some(existing) = out.iter_mut().find(|(id, _)| *id == var_id) {
984                for t in traits {
985                    if !existing.1.contains(&t) {
986                        existing.1.push(t);
987                    }
988                }
989            } else {
990                out.push((var_id, traits));
991            }
992        }
993        out
994    }
995
996    // ── Setters for import seeding ──────────────────────────────────────────
997
998    /// Insert record field types for an imported record.
999    pub fn insert_record_field_types(&mut self, name: String, fields: Vec<(String, Type)>) {
1000        self.record_field_types.insert(name, fields);
1001    }
1002
1003    /// Insert generic parameter names for an imported record/enum.
1004    pub fn insert_record_generic_params(&mut self, name: String, params: Vec<String>) {
1005        self.record_generic_params.insert(name, params);
1006    }
1007
1008    /// Insert trait method signatures for an imported trait.
1009    pub fn insert_trait_method_types(&mut self, name: String, methods: HashMap<String, Type>) {
1010        self.trait_method_types.insert(name, methods);
1011    }
1012
1013    /// Insert effect operation types for an imported effect.
1014    pub fn insert_effect_op_types(&mut self, name: String, ops: Vec<(String, Type)>) {
1015        self.effect_op_types.insert(name, ops);
1016    }
1017
1018    /// Insert component effects for an imported composite effect.
1019    pub fn insert_effect_components(&mut self, name: String, components: Vec<String>) {
1020        self.effect_components.insert(name, components);
1021    }
1022
1023    /// Record a trait impl declared in an imported module (Q-xmod-impl).
1024    ///
1025    /// `trait_args` is empty for a plain trait impl (`impl Comparable for B`)
1026    /// and non-empty for a parameterized one (`impl From[A] for B`). The
1027    /// recorded impls are folded into the freshly-built `impl_table` in
1028    /// [`TypeChecker::check_module`], so cross-module `.into()` resolution and
1029    /// cross-module where-clause bound satisfaction see them.
1030    pub fn register_imported_trait_impl(
1031        &mut self,
1032        trait_name: String,
1033        trait_args: Vec<Type>,
1034        target: Type,
1035    ) {
1036        self.imported_trait_impls
1037            .push((trait_name, trait_args, target));
1038    }
1039
1040    /// Insert a type alias for an imported type alias.
1041    pub fn insert_type_alias(&mut self, name: String, underlying: Type) {
1042        self.type_aliases.insert(name, underlying);
1043    }
1044
1045    /// Insert method types for an imported type's inherent impl.
1046    pub fn insert_method_types(&mut self, type_name: String, methods: HashMap<String, Type>) {
1047        self.method_types.insert(type_name, methods);
1048    }
1049
1050    /// Seed an imported generic function signature so that each call site
1051    /// gets fresh [`TypeVarId`]s (just like local generic functions).
1052    ///
1053    /// When a generic function is exported, its type string contains the
1054    /// original [`TypeVarId`]s (e.g. `"Fn(?3) -> ?3"`). Without an `FnSig`
1055    /// entry the call-site instantiation logic in the `Call` handler is
1056    /// bypassed, causing the first call to bind those vars permanently.
1057    ///
1058    /// This method re-allocates fresh [`TypeVarId`]s, remaps the function
1059    /// type, stores the remapped type in `env`, and inserts a matching
1060    /// `FnSig` into `fn_sigs`.
1061    pub fn seed_imported_generic_fn(&mut self, name: &str, fn_ty: &FnType) -> Type {
1062        self.seed_imported_generic_fn_with_bounds(name, fn_ty, &[])
1063    }
1064
1065    /// Like [`Self::seed_imported_generic_fn`] but also reconstructs the
1066    /// imported function's where-clause trait bounds so they are enforced at
1067    /// call sites in the importing module (Q-xmod-bounds).
1068    ///
1069    /// `bounds` is `(original_type_var_id, [trait_name, …])`, where the var id
1070    /// is the one encoded in the export ABI (the `?<id>` that appears in the
1071    /// exported type string). Each id is matched to the synthetic generic
1072    /// parameter (`T<position>`) created for it here, and a [`TypeConstraint`]
1073    /// is built so the call-site trait-bound check can enforce it.
1074    pub fn seed_imported_generic_fn_with_bounds(
1075        &mut self,
1076        name: &str,
1077        fn_ty: &FnType,
1078        bounds: &[(TypeVarId, Vec<String>)],
1079    ) -> Type {
1080        // Collect unique TypeVarIds from the function type in order of first
1081        // appearance, so the mapping is deterministic.
1082        let mut original_ids = Vec::new();
1083        collect_type_var_ids_fn(fn_ty, &mut original_ids);
1084
1085        if original_ids.is_empty() {
1086            // Not actually generic — just define in env and return.
1087            let ty = Type::Function(fn_ty.clone());
1088            self.env.define(name, ty.clone());
1089            return ty;
1090        }
1091
1092        // Allocate fresh TypeVarIds and build the replacement map.
1093        let remap: HashMap<TypeVarId, Type> = original_ids
1094            .iter()
1095            .map(|&id| (id, self.fresh_var()))
1096            .collect();
1097
1098        let fresh_ids: Vec<TypeVarId> = original_ids
1099            .iter()
1100            .map(|id| match &remap[id] {
1101                Type::TypeVar(fresh) => *fresh,
1102                _ => unreachable!(),
1103            })
1104            .collect();
1105
1106        // Remap the function type.
1107        let remapped = Type::Function(FnType {
1108            params: fn_ty
1109                .params
1110                .iter()
1111                .map(|t| self.replace_type_vars(t, &remap))
1112                .collect(),
1113            ret: Box::new(self.replace_type_vars(&fn_ty.ret, &remap)),
1114            effects: fn_ty.effects.clone(),
1115        });
1116
1117        // Store remapped type in env.
1118        self.env.define(name, remapped.clone());
1119
1120        // Create synthetic generic param names. Synthetic param `T<i>`
1121        // corresponds to `original_ids[i]`.
1122        let generic_params: Vec<String> =
1123            (0..original_ids.len()).map(|i| format!("T{i}")).collect();
1124
1125        // Reconstruct the where-clause: map each encoded `(original_var_id,
1126        // traits)` to the synthetic param name that the same id became, and
1127        // build a `TypeConstraint` keyed on that name. `check_trait_bounds_at_call`
1128        // pairs `clause.param.name` with `generic_params`/`generic_var_ids` by
1129        // name, so the constraint reaches the right fresh call-site var.
1130        let where_clause: Vec<TypeConstraint> = bounds
1131            .iter()
1132            .filter_map(|(orig_id, traits)| {
1133                let pos = original_ids.iter().position(|id| id == orig_id)?;
1134                if traits.is_empty() {
1135                    return None;
1136                }
1137                Some(TypeConstraint {
1138                    id: 0,
1139                    span: Span::dummy(),
1140                    param: bock_ast::Ident {
1141                        name: generic_params[pos].clone(),
1142                        span: Span::dummy(),
1143                    },
1144                    bounds: traits
1145                        .iter()
1146                        .map(|t| TypePath {
1147                            segments: t
1148                                .split('.')
1149                                .map(|seg| bock_ast::Ident {
1150                                    name: seg.to_string(),
1151                                    span: Span::dummy(),
1152                                })
1153                                .collect(),
1154                            span: Span::dummy(),
1155                        })
1156                        .collect(),
1157                })
1158            })
1159            .collect();
1160
1161        // Extract param types and return type from remapped function.
1162        if let Type::Function(ref f) = remapped {
1163            self.fn_sigs.insert(
1164                name.to_string(),
1165                FnSig {
1166                    generic_params,
1167                    generic_var_ids: fresh_ids,
1168                    param_types: f.params.clone(),
1169                    return_type: (*f.ret).clone(),
1170                    where_clause,
1171                },
1172            );
1173        }
1174
1175        remapped
1176    }
1177
1178    // ── Unification helper ───────────────────────────────────────────────────
1179
1180    /// Try to unify `found` (the type the expression actually has) with
1181    /// `expected` (the type the surrounding context requires). On failure
1182    /// emit an `E4001` at `span` and return `Type::Error`.
1183    ///
1184    /// The argument orientation is part of the diagnostic contract: the
1185    /// message reads ``expected `T`, found `U``` with `T` taken from
1186    /// `expected` and `U` from `found`, and the conversion hint (when one
1187    /// exists) suggests the conversion that produces the **expected** type.
1188    /// Call sites must pass the established/required type as `expected`
1189    /// (for operand pairs, the left/first operand establishes the
1190    /// expectation). Types render in surface Bock syntax via [`Type`]'s
1191    /// `Display` — never `Debug`.
1192    fn unify_or_error(&mut self, found: &Type, expected: &Type, span: Span, context: &str) -> Type {
1193        let found = self.resolve_alias(&self.subst.apply(found));
1194        let expected = self.resolve_alias(&self.subst.apply(expected));
1195        // `unify` is symmetric for solving, but its error payloads describe
1196        // the first argument as `left`/`expected` — pass `expected` first so
1197        // arity errors (`expected a function taking N parameters, …`) read
1198        // with the right orientation.
1199        match unify(&expected, &found, &mut self.subst) {
1200            Ok(()) => self.subst.apply(&found),
1201            Err(e) => {
1202                let msg = match &e {
1203                    TypeError::Mismatch { .. } => {
1204                        format!(
1205                            "type mismatch in {context}: expected `{expected}`, found `{found}`"
1206                        )
1207                    }
1208                    other => format!("type mismatch in {context}: {other}"),
1209                };
1210                let diag = self.diags.error(E_TYPE_MISMATCH, msg, span);
1211                if let Some(hint) = conversion_hint(&found, &expected) {
1212                    diag.note(hint);
1213                }
1214                Type::Error
1215            }
1216        }
1217    }
1218
1219    // ── Module-level pass ────────────────────────────────────────────────────
1220
1221    /// Type-check an AIR module, annotating every node with its resolved type.
1222    ///
1223    /// Performs two sub-passes:
1224    /// 1. **Collect** — gather all top-level function signatures.
1225    /// 2. **Check** — infer/check each top-level item.
1226    pub fn check_module(&mut self, module: &mut AIRNode) {
1227        // Clone children out to avoid simultaneous borrow of `module`.
1228        let (items, imports) = match &module.kind {
1229            NodeKind::Module { items, imports, .. } => (items.clone(), imports.clone()),
1230            _ => return,
1231        };
1232
1233        // §12.2 / DQ8 (Q-import-reject): reject any `use` whose module path
1234        // carries neither a brace-list nor a wildcard — a bare
1235        // `use core.error`. Module-qualified access is deferred to v1.x; the
1236        // only two v1 import forms are the braced list and the wildcard.
1237        self.reject_bare_module_imports(&imports);
1238
1239        // Build the trait-impl table from the module's `impl` blocks and wire
1240        // it into the checker so where-clause bounds are enforced at call
1241        // sites. Without a wired table, `check_trait_bounds_at_call` is a
1242        // no-op and bounds go unchecked.
1243        //
1244        // Order matters (Q1b sealing): `build_from` runs sealing on *user*
1245        // impls first; `register_canonical_conformances` then registers the
1246        // compiler's primitive conformances via `register_trait_impl_inner`,
1247        // which bypasses the sealing check, so the compiler can never reject
1248        // its own registration.
1249        let mut impl_table = ImplTable::build_from(module);
1250        crate::traits::register_canonical_conformances(&mut impl_table);
1251        // Canonical primitive conversions (`From`/`TryFrom` + blanket `Into`),
1252        // registered after conformances so `(5).into()`, `Float.from(3)`, and
1253        // `Int.try_from(s)` resolve uniformly with user conversions.
1254        crate::traits::register_canonical_conversions(&mut impl_table);
1255        // Q-xmod-impl: fold in trait impls declared in imported modules so
1256        // cross-module `.into()` (and `From`/`Into` resolution) and
1257        // cross-module where-clause bounds see them. Runs after the local +
1258        // canonical registration so a local impl always wins; the fold then
1259        // re-synthesizes the blanket `Into` from any imported `From`.
1260        if !self.imported_trait_impls.is_empty() {
1261            impl_table.fold_imported_impls(&self.imported_trait_impls);
1262        }
1263        // Surface coherence (`E4010`) and sealing (`E4011`) diagnostics
1264        // produced during table construction.
1265        self.diags.absorb(&impl_table.diags);
1266        self.impl_table = Some(impl_table);
1267
1268        // Pass 1: collect signatures
1269        for item in &items {
1270            self.collect_sig(item);
1271        }
1272
1273        // Pass 1b: §10.3 Layer-2 (Module) handlers. A module-level
1274        // `handle <Effect> with <handler>` installs that effect's operation
1275        // types into the module env so a bare op call anywhere in the module
1276        // type-checks without an enclosing `handling` block. Mirrors the
1277        // resolver's `inject_module_handle_operations`. Runs after `collect_sig`
1278        // (so `effect_op_types` is populated) and before item checking.
1279        {
1280            let mut visited = std::collections::HashSet::new();
1281            for item in &items {
1282                if let NodeKind::ModuleHandle { effect, .. } = &item.kind {
1283                    let ename = type_path_to_name(effect);
1284                    self.inject_effect_ops_into_env(&ename, &mut visited);
1285                }
1286            }
1287        }
1288
1289        // Pass 2: check items in place.
1290        // We re-borrow the items vec mutably.
1291        if let NodeKind::Module { items, .. } = &mut module.kind {
1292            for item in items.iter_mut() {
1293                self.check_item(item);
1294            }
1295        }
1296
1297        self.record(module, Type::Primitive(PrimitiveType::Void));
1298    }
1299
1300    /// §12.2 / DQ8 (Q-import-reject): emit `E4014` for every import whose items
1301    /// are [`ImportItems::Module`] — a `use` of a module path with neither a
1302    /// brace-list nor a wildcard (e.g. `use core.error`). v1 accepts only the
1303    /// braced form (`use core.error.{Error}`) and the discouraged wildcard
1304    /// (`use core.error.*`); module-qualified access is deferred to v1.x.
1305    fn reject_bare_module_imports(&mut self, imports: &[AIRNode]) {
1306        for import in imports {
1307            let NodeKind::ImportDecl { path, items } = &import.kind else {
1308                continue;
1309            };
1310            if !matches!(items, bock_ast::ImportItems::Module) {
1311                continue;
1312            }
1313            let path_str = path
1314                .segments
1315                .iter()
1316                .map(|s| s.name.as_str())
1317                .collect::<Vec<_>>()
1318                .join(".");
1319            self.diags
1320                .error(
1321                    E_BARE_MODULE_IMPORT,
1322                    format!(
1323                        "`use {path_str}` is not a v1 import form: a `use` must \
1324                         name what it imports with a brace-list or a wildcard"
1325                    ),
1326                    import.span,
1327                )
1328                .note(format!(
1329                    "import the names you need with the braced form, e.g. \
1330                     `use {path_str}.{{ /* names */ }}`"
1331                ))
1332                .note(
1333                    "module-qualified access (referring to symbols as \
1334                     `module.Symbol`) is deferred to v1.x",
1335                );
1336        }
1337    }
1338
1339    /// Collect a top-level function signature into `self.fn_sigs` and `self.env`.
1340    fn collect_sig(&mut self, node: &AIRNode) {
1341        match &node.kind {
1342            NodeKind::FnDecl {
1343                name,
1344                generic_params,
1345                params,
1346                return_type,
1347                effect_clause,
1348                where_clause,
1349                ..
1350            } => {
1351                let gp_names: Vec<String> =
1352                    generic_params.iter().map(|g| g.name.name.clone()).collect();
1353
1354                // Build placeholder types for generic params
1355                let gp_map: HashMap<String, Type> = gp_names
1356                    .iter()
1357                    .map(|n| (n.clone(), self.fresh_var()))
1358                    .collect();
1359
1360                // Extract TypeVarIds so instantiate_and_check can map them
1361                // to fresh vars at each call site.
1362                let gp_var_ids: Vec<TypeVarId> = gp_names
1363                    .iter()
1364                    .map(|n| match &gp_map[n] {
1365                        Type::TypeVar(id) => *id,
1366                        _ => unreachable!(),
1367                    })
1368                    .collect();
1369
1370                // Convert AIR param nodes to Types
1371                let param_types: Vec<Type> = params
1372                    .iter()
1373                    .map(|p| self.air_type_node_to_type(p.kind.param_ty_node(), &gp_map))
1374                    .collect();
1375
1376                let ret_ty = return_type
1377                    .as_deref()
1378                    .map(|n| self.air_type_node_to_type(n, &gp_map))
1379                    .unwrap_or(Type::Primitive(PrimitiveType::Void));
1380
1381                // Convert effect clause to EffectRef list
1382                let effects: Vec<EffectRef> = effect_clause
1383                    .iter()
1384                    .map(|tp| {
1385                        let name = tp
1386                            .segments
1387                            .iter()
1388                            .map(|s| s.name.as_str())
1389                            .collect::<Vec<_>>()
1390                            .join(".");
1391                        EffectRef::new(name)
1392                    })
1393                    .collect();
1394
1395                // Also define in env as a function type
1396                let fn_ty = Type::Function(FnType {
1397                    params: param_types.clone(),
1398                    ret: Box::new(ret_ty.clone()),
1399                    effects: effects.clone(),
1400                });
1401                self.env.define(name.name.clone(), fn_ty);
1402
1403                // Fold bracket-form generic bounds (`[T: Trait]`) into the
1404                // stored where-clause so they are enforced at call sites by the
1405                // same `check_trait_bounds_at_call` path that `where (T: Trait)`
1406                // bounds use (§4 treats the two forms as equivalent). Each
1407                // `GenericParam` whose `bounds` are non-empty becomes a
1408                // synthesized `TypeConstraint` keyed on the param name; the
1409                // explicit `where` clauses follow, so a param with bounds in
1410                // both forms contributes both (the bound-check and ABI encoder
1411                // both de-duplicate per param). Without this fold, bracket
1412                // bounds reach `type_var_bounds` (for method resolution) but
1413                // never the call-site satisfaction check, so a non-conforming
1414                // type argument was silently accepted (Q-bracket-bounds-unenforced).
1415                let bracket_bounds = generic_params.iter().filter_map(|gp| {
1416                    if gp.bounds.is_empty() {
1417                        None
1418                    } else {
1419                        Some(TypeConstraint {
1420                            id: gp.id,
1421                            span: gp.span,
1422                            param: gp.name.clone(),
1423                            bounds: gp.bounds.clone(),
1424                        })
1425                    }
1426                });
1427                let merged_where_clause: Vec<TypeConstraint> =
1428                    bracket_bounds.chain(where_clause.iter().cloned()).collect();
1429
1430                self.fn_sigs.insert(
1431                    name.name.clone(),
1432                    FnSig {
1433                        generic_params: gp_names,
1434                        generic_var_ids: gp_var_ids,
1435                        param_types,
1436                        return_type: ret_ty,
1437                        where_clause: merged_where_clause,
1438                    },
1439                );
1440            }
1441            NodeKind::ConstDecl { name, ty, .. } => {
1442                let const_ty = self.air_type_node_to_type(ty, &HashMap::new());
1443                self.env.define(name.name.clone(), const_ty);
1444            }
1445            NodeKind::EnumDecl {
1446                name,
1447                variants,
1448                generic_params,
1449                ..
1450            } => {
1451                let enum_name = name.name.clone();
1452
1453                // Extract generic param names.
1454                let gp_names: Vec<String> =
1455                    generic_params.iter().map(|g| g.name.name.clone()).collect();
1456
1457                // For generic enums, build a gp_map so variant field types
1458                // resolve type parameters (e.g. L, R) as fresh type vars
1459                // instead of Named("L"), and build a Generic return type
1460                // for tuple-variant constructor fn_sigs.
1461                //
1462                // The gp_map type vars are "template" vars: they must only
1463                // appear inside fn_sigs entries (which create per-call-site
1464                // fresh vars).  Unit/struct variants use Named to avoid
1465                // binding the template vars through unification.
1466                let named_ty = Type::Named(crate::NamedType {
1467                    name: enum_name.clone(),
1468                });
1469                let (gp_map, gp_var_ids, generic_ret_ty) = if gp_names.is_empty() {
1470                    (HashMap::new(), vec![], named_ty.clone())
1471                } else {
1472                    let gp_map: HashMap<String, Type> = gp_names
1473                        .iter()
1474                        .map(|n| (n.clone(), self.fresh_var()))
1475                        .collect();
1476                    let gp_var_ids: Vec<TypeVarId> = gp_names
1477                        .iter()
1478                        .map(|n| match &gp_map[n] {
1479                            Type::TypeVar(id) => *id,
1480                            _ => unreachable!(),
1481                        })
1482                        .collect();
1483                    let type_args: Vec<Type> = gp_names.iter().map(|n| gp_map[n].clone()).collect();
1484                    let generic_ret_ty = Type::Generic(GenericType {
1485                        constructor: enum_name.clone(),
1486                        args: type_args,
1487                    });
1488                    (gp_map, gp_var_ids, generic_ret_ty)
1489                };
1490
1491                // Register the enum type name itself (always Named so
1492                // it doesn't leak template type vars).
1493                self.env.define(enum_name.clone(), named_ty.clone());
1494
1495                // Store generic params for struct-variant field lookup.
1496                if !gp_names.is_empty() {
1497                    self.record_generic_params
1498                        .insert(enum_name.clone(), gp_names.clone());
1499                }
1500
1501                // DQ29: record every variant's payload component types for the
1502                // structural-Equatable predicate (an enum conforms iff every
1503                // payload type of every variant conforms). Generic params are
1504                // stored SYMBOLICALLY as `Named(param)` — the convention
1505                // `record_field_types` already uses for generic records — so
1506                // the predicate can substitute the instantiation's type
1507                // arguments at the use site (the template type vars in
1508                // `gp_map` are reserved for constructor fn_sigs).
1509                let symbolic_gp_map: HashMap<String, Type> = gp_names
1510                    .iter()
1511                    .map(|n| (n.clone(), Type::Named(crate::NamedType { name: n.clone() })))
1512                    .collect();
1513                let mut payloads: Vec<EnumVariantPayloadTypes> = Vec::new();
1514                for variant in variants {
1515                    if let NodeKind::EnumVariant {
1516                        name: vname,
1517                        payload,
1518                    } = &variant.kind
1519                    {
1520                        let components: Vec<(String, Type)> = match payload {
1521                            EnumVariantPayload::Unit => vec![],
1522                            EnumVariantPayload::Tuple(param_nodes) => param_nodes
1523                                .iter()
1524                                .enumerate()
1525                                .map(|(i, p)| {
1526                                    (
1527                                        format!("_{i}"),
1528                                        self.air_type_node_to_type(p, &symbolic_gp_map),
1529                                    )
1530                                })
1531                                .collect(),
1532                            EnumVariantPayload::Struct(fields) => fields
1533                                .iter()
1534                                .map(|f| {
1535                                    (
1536                                        f.name.name.clone(),
1537                                        self.type_expr_to_type(&f.ty, &symbolic_gp_map),
1538                                    )
1539                                })
1540                                .collect(),
1541                        };
1542                        payloads.push((vname.name.clone(), components));
1543                    }
1544                }
1545                self.enum_variant_payloads
1546                    .insert(enum_name.clone(), payloads);
1547
1548                // Register each variant as a value/constructor in scope.
1549                for variant in variants {
1550                    if let NodeKind::EnumVariant {
1551                        name: vname,
1552                        payload,
1553                    } = &variant.kind
1554                    {
1555                        match payload {
1556                            EnumVariantPayload::Unit => {
1557                                // Unit variant — use Named (not Generic with
1558                                // template vars) so unification doesn't bind
1559                                // the shared template type vars.
1560                                self.env.define(vname.name.clone(), named_ty.clone());
1561                            }
1562                            EnumVariantPayload::Tuple(param_nodes) => {
1563                                // Tuple variant is a constructor function.
1564                                let param_tys: Vec<Type> = param_nodes
1565                                    .iter()
1566                                    .map(|p| self.air_type_node_to_type(p, &gp_map))
1567                                    .collect();
1568                                let fn_ty = Type::Function(FnType {
1569                                    params: param_tys.clone(),
1570                                    ret: Box::new(generic_ret_ty.clone()),
1571                                    effects: vec![],
1572                                });
1573                                self.env.define(vname.name.clone(), fn_ty);
1574
1575                                // For generic enums, register in fn_sigs so
1576                                // each call site gets fresh type var instantiation.
1577                                if !gp_names.is_empty() {
1578                                    self.fn_sigs.insert(
1579                                        vname.name.clone(),
1580                                        FnSig {
1581                                            generic_params: gp_names.clone(),
1582                                            generic_var_ids: gp_var_ids.clone(),
1583                                            param_types: param_tys,
1584                                            return_type: generic_ret_ty.clone(),
1585                                            where_clause: vec![],
1586                                        },
1587                                    );
1588                                }
1589                            }
1590                            EnumVariantPayload::Struct(fields) => {
1591                                // Record variant — use Named (not Generic)
1592                                // so template type vars are not leaked.
1593                                self.env.define(vname.name.clone(), named_ty.clone());
1594                                // Register field types so record construction
1595                                // can type-check individual fields.
1596                                let field_types: Vec<(String, Type)> = fields
1597                                    .iter()
1598                                    .map(|f| {
1599                                        let ty = self.type_expr_to_type(&f.ty, &gp_map);
1600                                        (f.name.name.clone(), ty)
1601                                    })
1602                                    .collect();
1603                                self.record_field_types
1604                                    .insert(vname.name.clone(), field_types);
1605                                // For generic enum struct variants, register
1606                                // their params for record construction lookup.
1607                                if !gp_names.is_empty() {
1608                                    self.record_generic_params
1609                                        .insert(vname.name.clone(), gp_names.clone());
1610                                }
1611                            }
1612                        }
1613                    }
1614                }
1615            }
1616            NodeKind::ImplBlock {
1617                target, methods, ..
1618            } => {
1619                let target_name = match &target.kind {
1620                    NodeKind::TypeNamed { path, .. } => type_path_to_name(path),
1621                    _ => return,
1622                };
1623                let target_ty = Type::Named(crate::NamedType {
1624                    name: target_name.clone(),
1625                });
1626                for method in methods {
1627                    if let NodeKind::FnDecl {
1628                        name,
1629                        params,
1630                        return_type,
1631                        generic_params: method_gps,
1632                        ..
1633                    } = &method.kind
1634                    {
1635                        let gp_map: HashMap<String, Type> = HashMap::new();
1636
1637                        // Record the method's OWN type-param names (e.g. the `U`
1638                        // in `fn map[U](...)`) so the call site can substitute
1639                        // them with fresh inference vars
1640                        // (Q-checker-method-generic-call-infer). The type's own
1641                        // params (`T`) are pinned by the receiver and are NOT
1642                        // listed here.
1643                        let method_gp_names: Vec<String> =
1644                            method_gps.iter().map(|g| g.name.name.clone()).collect();
1645                        if !method_gp_names.is_empty() {
1646                            self.method_generic_params
1647                                .entry(target_name.clone())
1648                                .or_default()
1649                                .insert(name.name.clone(), method_gp_names);
1650                        }
1651
1652                        let param_types: Vec<Type> = params
1653                            .iter()
1654                            .map(|p| {
1655                                // For `self` params (no type annotation), use the target type.
1656                                if let NodeKind::Param {
1657                                    pattern, ty: None, ..
1658                                } = &p.kind
1659                                {
1660                                    if let NodeKind::BindPat { name, .. } = &pattern.kind {
1661                                        if name.name == "self" {
1662                                            return target_ty.clone();
1663                                        }
1664                                    }
1665                                }
1666                                self.air_type_node_to_type(p, &gp_map)
1667                            })
1668                            .collect();
1669
1670                        let ret_ty = return_type
1671                            .as_deref()
1672                            .map(|n| self.air_type_node_to_type(n, &gp_map))
1673                            .unwrap_or(Type::Primitive(PrimitiveType::Void));
1674
1675                        let fn_ty = Type::Function(FnType {
1676                            params: param_types,
1677                            ret: Box::new(ret_ty),
1678                            effects: vec![],
1679                        });
1680
1681                        // Substitute `Self` -> the impl's target type across the
1682                        // method signature (params + return). An explicit `Self`
1683                        // written in an impl method's own signature — e.g.
1684                        // `fn double(self) -> Self` or `fn combine(self, other:
1685                        // Self)` — lowers to `Type::Named("Self")`, which the
1686                        // trait-method resolution path substitutes but the impl
1687                        // method's own registered `FnSig` did not, yielding E4001
1688                        // at call sites (Named("Self") vs the concrete target).
1689                        // This mirrors the trait-method `self_params=["Self"]`
1690                        // substitution. Associated-type `Self::Output` is out of
1691                        // scope (parsed as a distinct type path, not `Self`).
1692                        let self_params = ["Self".to_string()];
1693                        let self_args = [target_ty.clone()];
1694                        let fn_ty = substitute_type_params(&fn_ty, &self_params, &self_args);
1695
1696                        self.method_types
1697                            .entry(target_name.clone())
1698                            .or_default()
1699                            .insert(name.name.clone(), fn_ty);
1700                    }
1701                }
1702            }
1703            NodeKind::EffectDecl {
1704                name,
1705                operations,
1706                components,
1707                ..
1708            } => {
1709                // Collect operation signatures so `with` clauses can inject
1710                // them into function type environments.
1711                let mut ops = Vec::new();
1712                for op in operations {
1713                    if let NodeKind::FnDecl {
1714                        name: op_name,
1715                        params,
1716                        return_type,
1717                        ..
1718                    } = &op.kind
1719                    {
1720                        let param_types: Vec<Type> = params
1721                            .iter()
1722                            .map(|p| {
1723                                self.air_type_node_to_type(p.kind.param_ty_node(), &HashMap::new())
1724                            })
1725                            .collect();
1726                        let ret_ty = return_type
1727                            .as_deref()
1728                            .map(|n| self.air_type_node_to_type(n, &HashMap::new()))
1729                            .unwrap_or(Type::Primitive(PrimitiveType::Void));
1730                        let fn_ty = Type::Function(FnType {
1731                            params: param_types,
1732                            ret: Box::new(ret_ty),
1733                            effects: vec![],
1734                        });
1735                        ops.push((op_name.name.clone(), fn_ty));
1736                    }
1737                }
1738                self.effect_op_types.insert(name.name.clone(), ops);
1739
1740                let comp_names: Vec<String> = components.iter().map(type_path_to_name).collect();
1741                if !comp_names.is_empty() {
1742                    self.effect_components.insert(name.name.clone(), comp_names);
1743                }
1744            }
1745            NodeKind::RecordDecl {
1746                name,
1747                fields,
1748                generic_params,
1749                ..
1750            } => {
1751                let record_name = name.name.clone();
1752                let gp_names: Vec<String> =
1753                    generic_params.iter().map(|g| g.name.name.clone()).collect();
1754                let field_types: Vec<(String, Type)> = fields
1755                    .iter()
1756                    .map(|f| {
1757                        let ty = self.type_expr_to_type(&f.ty, &HashMap::new());
1758                        (f.name.name.clone(), ty)
1759                    })
1760                    .collect();
1761                self.record_field_types
1762                    .insert(record_name.clone(), field_types);
1763                if !gp_names.is_empty() {
1764                    self.record_generic_params
1765                        .insert(record_name.clone(), gp_names);
1766                }
1767                // Also register the record name as a Named type in env.
1768                self.env.define(
1769                    record_name.clone(),
1770                    Type::Named(crate::NamedType { name: record_name }),
1771                );
1772            }
1773            NodeKind::TypeAlias { name, ty, .. } => {
1774                let underlying = self.air_type_node_to_type(ty, &HashMap::new());
1775                self.type_aliases.insert(name.name.clone(), underlying);
1776            }
1777            NodeKind::ClassDecl {
1778                name,
1779                fields,
1780                methods,
1781                base,
1782                generic_params,
1783                ..
1784            } => {
1785                let class_name = name.name.clone();
1786
1787                // DQ29: classes are excluded from structural Equatable; record
1788                // the name so the predicate can tell a class apart from a
1789                // record (both populate `record_field_types`).
1790                self.class_names.insert(class_name.clone());
1791
1792                // Register generic params if present.
1793                let gp_names: Vec<String> =
1794                    generic_params.iter().map(|g| g.name.name.clone()).collect();
1795                if !gp_names.is_empty() {
1796                    self.record_generic_params
1797                        .insert(class_name.clone(), gp_names);
1798                }
1799
1800                // Register field types (same as RecordDecl).
1801                let field_types: Vec<(String, Type)> = fields
1802                    .iter()
1803                    .map(|f| {
1804                        let ty = self.type_expr_to_type(&f.ty, &HashMap::new());
1805                        (f.name.name.clone(), ty)
1806                    })
1807                    .collect();
1808                self.record_field_types
1809                    .insert(class_name.clone(), field_types);
1810
1811                // Register the class name as a Named type.
1812                let class_ty = Type::Named(crate::NamedType {
1813                    name: class_name.clone(),
1814                });
1815                self.env.define(class_name.clone(), class_ty.clone());
1816
1817                // Inherit methods from base class if present.
1818                if let Some(base_path) = base {
1819                    let base_name = type_path_to_name(base_path);
1820                    if let Some(base_methods) = self.method_types.get(&base_name).cloned() {
1821                        self.method_types
1822                            .entry(class_name.clone())
1823                            .or_default()
1824                            .extend(base_methods);
1825                    }
1826                }
1827
1828                // Register methods (same logic as ImplBlock).
1829                for method in methods {
1830                    if let NodeKind::FnDecl {
1831                        name: method_name,
1832                        params,
1833                        return_type,
1834                        generic_params: method_gps,
1835                        ..
1836                    } = &method.kind
1837                    {
1838                        let gp_map: HashMap<String, Type> = HashMap::new();
1839
1840                        // Record the method's OWN type-param names so the call
1841                        // site can substitute them with fresh inference vars
1842                        // (Q-checker-method-generic-call-infer); see the
1843                        // `ImplBlock` branch for the rationale.
1844                        let method_gp_names: Vec<String> =
1845                            method_gps.iter().map(|g| g.name.name.clone()).collect();
1846                        if !method_gp_names.is_empty() {
1847                            self.method_generic_params
1848                                .entry(class_name.clone())
1849                                .or_default()
1850                                .insert(method_name.name.clone(), method_gp_names);
1851                        }
1852
1853                        let param_types: Vec<Type> = params
1854                            .iter()
1855                            .map(|p| {
1856                                if let NodeKind::Param {
1857                                    pattern, ty: None, ..
1858                                } = &p.kind
1859                                {
1860                                    if let NodeKind::BindPat { name, .. } = &pattern.kind {
1861                                        if name.name == "self" {
1862                                            return class_ty.clone();
1863                                        }
1864                                    }
1865                                }
1866                                self.air_type_node_to_type(p, &gp_map)
1867                            })
1868                            .collect();
1869
1870                        let ret_ty = return_type
1871                            .as_deref()
1872                            .map(|n| self.air_type_node_to_type(n, &gp_map))
1873                            .unwrap_or(Type::Primitive(PrimitiveType::Void));
1874
1875                        let fn_ty = Type::Function(FnType {
1876                            params: param_types,
1877                            ret: Box::new(ret_ty),
1878                            effects: vec![],
1879                        });
1880
1881                        self.method_types
1882                            .entry(class_name.clone())
1883                            .or_default()
1884                            .insert(method_name.name.clone(), fn_ty);
1885                    }
1886                }
1887            }
1888            NodeKind::TraitDecl { name, methods, .. } => {
1889                let trait_name = name.name.clone();
1890                let self_ty = Type::Named(crate::NamedType {
1891                    name: "Self".to_string(),
1892                });
1893                let mut trait_methods = HashMap::new();
1894                for method in methods {
1895                    if let NodeKind::FnDecl {
1896                        name: method_name,
1897                        params,
1898                        return_type,
1899                        ..
1900                    } = &method.kind
1901                    {
1902                        let gp_map: HashMap<String, Type> = HashMap::new();
1903                        let param_types: Vec<Type> = params
1904                            .iter()
1905                            .map(|p| {
1906                                if let NodeKind::Param {
1907                                    pattern, ty: None, ..
1908                                } = &p.kind
1909                                {
1910                                    if let NodeKind::BindPat { name, .. } = &pattern.kind {
1911                                        if name.name == "self" {
1912                                            return self_ty.clone();
1913                                        }
1914                                    }
1915                                }
1916                                self.air_type_node_to_type(p, &gp_map)
1917                            })
1918                            .collect();
1919                        let ret_ty = return_type
1920                            .as_deref()
1921                            .map(|n| self.air_type_node_to_type(n, &gp_map))
1922                            .unwrap_or(Type::Primitive(PrimitiveType::Void));
1923                        let fn_ty = Type::Function(FnType {
1924                            params: param_types,
1925                            ret: Box::new(ret_ty),
1926                            effects: vec![],
1927                        });
1928                        trait_methods.insert(method_name.name.clone(), fn_ty);
1929                    }
1930                }
1931                if !trait_methods.is_empty() {
1932                    self.trait_method_types.insert(trait_name, trait_methods);
1933                }
1934            }
1935            _ => {}
1936        }
1937    }
1938
1939    /// Resolve a type through type aliases. If `ty` is a `Named` type whose
1940    /// name is a registered type alias, return the underlying type instead.
1941    fn resolve_alias(&self, ty: &Type) -> Type {
1942        match ty {
1943            Type::Named(nt) => {
1944                if let Some(underlying) = self.type_aliases.get(&nt.name) {
1945                    underlying.clone()
1946                } else {
1947                    ty.clone()
1948                }
1949            }
1950            _ => ty.clone(),
1951        }
1952    }
1953
1954    /// Type-check a top-level item node (mutates the node tree).
1955    fn check_item(&mut self, node: &mut AIRNode) {
1956        match &node.kind {
1957            NodeKind::FnDecl { .. } => {
1958                self.check_fn_decl(node);
1959            }
1960            NodeKind::ConstDecl { .. } => {
1961                self.check_const_decl(node);
1962            }
1963            NodeKind::ImplBlock { .. } => {
1964                self.check_impl_block(node);
1965            }
1966            NodeKind::ClassDecl { .. } => {
1967                self.check_class_decl(node);
1968                // DQ31: a class with an explicit `impl Equatable` earns the
1969                // `CUSTOM_EQ` stamp so the Rust backend emits a `PartialEq`
1970                // delegating to its `eq` — letting a `List`/`Map`/`Set` of the
1971                // class compare through the custom equality natively.
1972                self.stamp_derive_structural_eq(node);
1973            }
1974            // Record/enum declarations carry no body to check, but DQ29 stamps
1975            // the structurally-Equatable ones for the Rust backend's
1976            // `PartialEq` derive (see `DERIVE_EQ_META_KEY`).
1977            NodeKind::RecordDecl { .. } | NodeKind::EnumDecl { .. } => {
1978                self.stamp_derive_structural_eq(node);
1979                self.record(node, Type::Primitive(PrimitiveType::Void));
1980            }
1981            // Other top-level items: record as Void for now.
1982            _ => {
1983                self.record(node, Type::Primitive(PrimitiveType::Void));
1984            }
1985        }
1986    }
1987
1988    /// Stamp a `RecordDecl` / `EnumDecl` with [`DERIVE_EQ_META_KEY`] when the
1989    /// declared type conforms to `Equatable` structurally (DQ29) and declares
1990    /// no explicit `impl Equatable` (the impl suppresses the structural
1991    /// default — `==` routes through its `eq` instead, so the derive would
1992    /// pin the WRONG equality into containers).
1993    ///
1994    /// The probe runs on the bare `Named` type: a generic decl's symbolic
1995    /// `Named(param)` field placeholders are unknown to the predicate and thus
1996    /// conservatively conforming, which matches Rust's conditional derive
1997    /// semantics (`#[derive(PartialEq)]` on `Pair<A, B>` bounds each use site
1998    /// on `A: PartialEq, B: PartialEq` — rule 4's per-instantiation decision).
1999    fn stamp_derive_structural_eq(&mut self, node: &mut AIRNode) {
2000        // A class is excluded from the structural default (DQ29 rule 7), so it
2001        // only ever earns the DQ31 `CUSTOM_EQ` stamp (via an explicit impl) —
2002        // never the structural derive.
2003        let is_class = matches!(&node.kind, NodeKind::ClassDecl { .. });
2004        let name = match &node.kind {
2005            NodeKind::RecordDecl { name, .. }
2006            | NodeKind::EnumDecl { name, .. }
2007            | NodeKind::ClassDecl { name, .. } => name.name.clone(),
2008            _ => return,
2009        };
2010        let named = Type::Named(crate::NamedType { name });
2011        if let Some(table) = self.impl_table.as_ref() {
2012            if resolve_impl(&TraitRef::new("Equatable"), &named, table).is_some() {
2013                // DQ31: an explicit `impl Equatable` suppresses the structural
2014                // derive (its `eq` IS the equality), but the Rust backend must
2015                // still emit a `PartialEq` DELEGATING to that `eq` so a
2016                // container of the type (`Vec<T>` / `HashMap` / `HashSet`)
2017                // compares through the custom equality natively — the type's
2018                // one equality, the same inside a container as outside.
2019                node.metadata
2020                    .insert(CUSTOM_EQ_META_KEY.to_string(), Value::Bool(true));
2021                return;
2022            }
2023        }
2024        if is_class {
2025            // A class without an explicit impl has no equality at all; never
2026            // derive a structural `PartialEq` for it.
2027            return;
2028        }
2029        let mut in_progress = HashSet::new();
2030        let mut path = Vec::new();
2031        if self
2032            .structural_equatable_witness(&named, &mut in_progress, &mut path)
2033            .is_none()
2034        {
2035            node.metadata
2036                .insert(DERIVE_EQ_META_KEY.to_string(), Value::Bool(true));
2037        }
2038    }
2039
2040    /// Type-check every method **body** in an `impl` block.
2041    ///
2042    /// Mirrors [`Self::check_fn_decl`] per method, but establishes the impl
2043    /// context first: the impl's generic params become fresh type vars (with
2044    /// their bounds recorded), `Self` is mapped to the concrete target type,
2045    /// and `self` is bound in scope to that target. For a generic impl
2046    /// (`impl[T] Foo[T] { … }`) the target is a `Generic` whose args are those
2047    /// fresh vars, so field/method access through `record_generic_params`
2048    /// substitution resolves the same way it does at external call sites.
2049    ///
2050    /// Method signatures are already registered in `method_types` by
2051    /// [`Self::collect_sig`]; this pass only walks the bodies that pass missed,
2052    /// so type errors inside methods are reported and the checker's codegen
2053    /// metadata stamps (`recv_kind`, `list_concat`) reach method bodies.
2054    fn check_impl_block(&mut self, node: &mut AIRNode) {
2055        let (generic_params, target) = match &node.kind {
2056            NodeKind::ImplBlock {
2057                generic_params,
2058                target,
2059                ..
2060            } => (generic_params.clone(), target.clone()),
2061            _ => return,
2062        };
2063
2064        let target_name = match &target.kind {
2065            NodeKind::TypeNamed { path, .. } => Some(type_path_to_name(path)),
2066            _ => None,
2067        };
2068        let Some(target_name) = target_name else {
2069            self.record(node, Type::Primitive(PrimitiveType::Void));
2070            return;
2071        };
2072
2073        let (impl_gp_map, target_ty) = self.build_impl_context(&generic_params, &target_name);
2074
2075        if let NodeKind::ImplBlock { methods, .. } = &mut node.kind {
2076            let mut methods = std::mem::take(methods);
2077            for method in methods.iter_mut() {
2078                self.check_method_body(method, &target_ty, &impl_gp_map);
2079            }
2080            if let NodeKind::ImplBlock { methods: slot, .. } = &mut node.kind {
2081                *slot = methods;
2082            }
2083        }
2084
2085        self.record(node, Type::Primitive(PrimitiveType::Void));
2086    }
2087
2088    /// Type-check every method **body** in a `class` declaration. See
2089    /// [`Self::check_impl_block`]; classes are the inherent-impl analogue with
2090    /// declared fields and optional base inheritance (already folded into
2091    /// `method_types`/`record_field_types` by [`Self::collect_sig`]).
2092    fn check_class_decl(&mut self, node: &mut AIRNode) {
2093        let (generic_params, class_name) = match &node.kind {
2094            NodeKind::ClassDecl {
2095                generic_params,
2096                name,
2097                ..
2098            } => (generic_params.clone(), name.name.clone()),
2099            _ => return,
2100        };
2101
2102        let (impl_gp_map, target_ty) = self.build_impl_context(&generic_params, &class_name);
2103
2104        if let NodeKind::ClassDecl { methods, .. } = &mut node.kind {
2105            let mut methods = std::mem::take(methods);
2106            for method in methods.iter_mut() {
2107                self.check_method_body(method, &target_ty, &impl_gp_map);
2108            }
2109            if let NodeKind::ClassDecl { methods: slot, .. } = &mut node.kind {
2110                *slot = methods;
2111            }
2112        }
2113
2114        self.record(node, Type::Primitive(PrimitiveType::Void));
2115    }
2116
2117    /// Build the per-method type context shared by impl and class bodies:
2118    /// a `gp_map` mapping each of the impl/class's generic params to a fresh
2119    /// type var (with inline trait bounds recorded) plus `Self` -> the concrete
2120    /// target, and the target type itself (`Generic` when the impl is generic,
2121    /// `Named` otherwise) to bind `self`.
2122    fn build_impl_context(
2123        &mut self,
2124        generic_params: &[GenericParam],
2125        target_name: &str,
2126    ) -> (HashMap<String, Type>, Type) {
2127        let mut gp_map: HashMap<String, Type> = generic_params
2128            .iter()
2129            .map(|g| (g.name.name.clone(), self.fresh_var()))
2130            .collect();
2131
2132        // Record inline trait bounds (e.g. `impl[T: Show] Foo[T]`) on the
2133        // fresh type vars so method bodies can resolve trait methods on `T`.
2134        for gp in generic_params {
2135            if let Some(Type::TypeVar(id)) = gp_map.get(&gp.name.name) {
2136                let bound_names: Vec<String> = gp.bounds.iter().map(type_path_to_name).collect();
2137                if !bound_names.is_empty() {
2138                    self.type_var_bounds
2139                        .entry(*id)
2140                        .or_default()
2141                        .extend(bound_names);
2142                }
2143            }
2144        }
2145
2146        let target_ty = if generic_params.is_empty() {
2147            Type::Named(crate::NamedType {
2148                name: target_name.to_string(),
2149            })
2150        } else {
2151            // Generic target: `Foo[T, U]` with the impl's params as args, so
2152            // field/method access resolves through `record_generic_params`.
2153            let args: Vec<Type> = generic_params
2154                .iter()
2155                .map(|g| gp_map[&g.name.name].clone())
2156                .collect();
2157            Type::Generic(GenericType {
2158                constructor: target_name.to_string(),
2159                args,
2160            })
2161        };
2162
2163        // `Self` written anywhere in a method body or signature resolves to the
2164        // concrete target (mirrors the signature substitution in `collect_sig`).
2165        gp_map.insert("Self".to_string(), target_ty.clone());
2166
2167        (gp_map, target_ty)
2168    }
2169
2170    /// Type-check a single impl/class method body in place.
2171    ///
2172    /// `target_ty` is the concrete (or generic-instantiated) type the method is
2173    /// attached to; `self` is bound to it. `impl_gp_map` carries the impl/class
2174    /// generic params + `Self`; the method's own generic params are layered on
2175    /// top. This is the per-function template of [`Self::check_fn_decl`],
2176    /// extended with the impl context.
2177    fn check_method_body(
2178        &mut self,
2179        node: &mut AIRNode,
2180        target_ty: &Type,
2181        impl_gp_map: &HashMap<String, Type>,
2182    ) {
2183        let (generic_params, params, return_type, effect_clause, where_clause) =
2184            match node.kind.clone() {
2185                NodeKind::FnDecl {
2186                    generic_params,
2187                    params,
2188                    return_type,
2189                    effect_clause,
2190                    where_clause,
2191                    ..
2192                } => (
2193                    generic_params,
2194                    params,
2195                    return_type,
2196                    effect_clause,
2197                    where_clause,
2198                ),
2199                // Methods are always FnDecl; ignore anything else defensively.
2200                _ => return,
2201            };
2202
2203        self.env.push_scope();
2204
2205        // Start from the impl context (impl generic params + `Self`) and layer
2206        // the method's own generic params on top.
2207        let mut gp_map = impl_gp_map.clone();
2208        for gp in &generic_params {
2209            gp_map.insert(gp.name.name.clone(), self.fresh_var());
2210        }
2211
2212        // Record trait bounds on the method's own type variables.
2213        for gp in &generic_params {
2214            if let Some(Type::TypeVar(id)) = gp_map.get(&gp.name.name) {
2215                let bound_names: Vec<String> = gp.bounds.iter().map(type_path_to_name).collect();
2216                if !bound_names.is_empty() {
2217                    self.type_var_bounds
2218                        .entry(*id)
2219                        .or_default()
2220                        .extend(bound_names);
2221                }
2222            }
2223        }
2224        for clause in &where_clause {
2225            if let Some(Type::TypeVar(id)) = gp_map.get(&clause.param.name) {
2226                let bound_names: Vec<String> =
2227                    clause.bounds.iter().map(type_path_to_name).collect();
2228                if !bound_names.is_empty() {
2229                    self.type_var_bounds
2230                        .entry(*id)
2231                        .or_default()
2232                        .extend(bound_names);
2233                }
2234            }
2235        }
2236
2237        // Bind params. A `self` receiver (no annotation) binds to the target
2238        // type; everything else resolves through `gp_map` (so `Self` and the
2239        // impl/method generics map to the concrete instantiation).
2240        for p in &params {
2241            if let NodeKind::Param { pattern, ty, .. } = &p.kind {
2242                if let NodeKind::BindPat { name, .. } = &pattern.kind {
2243                    if name.name == "self" && ty.is_none() {
2244                        self.env.define("self".to_string(), target_ty.clone());
2245                        continue;
2246                    }
2247                    let pty = self.air_type_node_to_type(p.kind.param_ty_node(), &gp_map);
2248                    self.env.define(name.name.clone(), pty);
2249                } else if let Some(pat_name) = p.kind.param_pat_name() {
2250                    let pty = self.air_type_node_to_type(p.kind.param_ty_node(), &gp_map);
2251                    self.env.define(pat_name, pty);
2252                }
2253            }
2254        }
2255
2256        let ret_ty = return_type
2257            .as_deref()
2258            .map(|n| self.air_type_node_to_type(n, &gp_map))
2259            .unwrap_or(Type::Primitive(PrimitiveType::Void));
2260
2261        // Inject effect operation types from the method's `with` clause.
2262        {
2263            let mut visited = std::collections::HashSet::new();
2264            for effect_tp in &effect_clause {
2265                let ename = type_path_to_name(effect_tp);
2266                self.inject_effect_ops_into_env(&ename, &mut visited);
2267            }
2268        }
2269
2270        self.check_where_clause(&where_clause, &gp_map, node.span);
2271
2272        self.return_ty_stack.push(ret_ty.clone());
2273        if let NodeKind::FnDecl { body, .. } = &mut node.kind {
2274            self.check_node(body, &ret_ty);
2275        }
2276        self.return_ty_stack.pop();
2277
2278        self.env.pop_scope();
2279
2280        // Methods record Void as their item-level type (their signature already
2281        // lives in `method_types`); the body walk's purpose is diagnostics +
2282        // codegen metadata stamping, not a fresh signature.
2283        self.record(node, Type::Primitive(PrimitiveType::Void));
2284    }
2285
2286    /// Type-check a function declaration node in place.
2287    fn check_fn_decl(&mut self, node: &mut AIRNode) {
2288        // Extract what we need by cloning to avoid borrow conflicts.
2289        let (_name, generic_params, params, return_type, effect_clause, where_clause, _body) =
2290            match node.kind.clone() {
2291                NodeKind::FnDecl {
2292                    name,
2293                    generic_params,
2294                    params,
2295                    return_type,
2296                    effect_clause,
2297                    where_clause,
2298                    body,
2299                    ..
2300                } => (
2301                    name,
2302                    generic_params,
2303                    params,
2304                    return_type,
2305                    effect_clause,
2306                    where_clause,
2307                    body,
2308                ),
2309                _ => return,
2310            };
2311
2312        self.env.push_scope();
2313
2314        // Introduce generic params as fresh type vars
2315        let gp_map: HashMap<String, Type> = generic_params
2316            .iter()
2317            .map(|g| (g.name.name.clone(), self.fresh_var()))
2318            .collect();
2319
2320        // Record trait bounds on type variables from inline bounds
2321        // (e.g. `T: Describable`) and where-clause constraints.
2322        for gp in &generic_params {
2323            if let Some(Type::TypeVar(id)) = gp_map.get(&gp.name.name) {
2324                let bound_names: Vec<String> = gp.bounds.iter().map(type_path_to_name).collect();
2325                if !bound_names.is_empty() {
2326                    self.type_var_bounds
2327                        .entry(*id)
2328                        .or_default()
2329                        .extend(bound_names);
2330                }
2331            }
2332        }
2333        for clause in &where_clause {
2334            if let Some(Type::TypeVar(id)) = gp_map.get(&clause.param.name) {
2335                let bound_names: Vec<String> =
2336                    clause.bounds.iter().map(type_path_to_name).collect();
2337                if !bound_names.is_empty() {
2338                    self.type_var_bounds
2339                        .entry(*id)
2340                        .or_default()
2341                        .extend(bound_names);
2342                }
2343            }
2344        }
2345
2346        // Record this function's generic-param bounds BY NAME for the duration
2347        // of its body, so `check_trait_bounds_at_call` can recognise a call that
2348        // forwards one of these params (an abstract `Named(param)`) to a callee
2349        // requiring the same bound. Both bound forms contribute. Save/restore so
2350        // nested method bodies don't leak each other's params.
2351        let saved_fn_param_bounds = std::mem::take(&mut self.current_fn_param_bounds);
2352        for gp in &generic_params {
2353            let bound_names: Vec<String> = gp.bounds.iter().map(type_path_to_name).collect();
2354            if !bound_names.is_empty() {
2355                self.current_fn_param_bounds
2356                    .entry(gp.name.name.clone())
2357                    .or_default()
2358                    .extend(bound_names);
2359            }
2360        }
2361        for clause in &where_clause {
2362            let bound_names: Vec<String> = clause.bounds.iter().map(type_path_to_name).collect();
2363            if !bound_names.is_empty() {
2364                self.current_fn_param_bounds
2365                    .entry(clause.param.name.clone())
2366                    .or_default()
2367                    .extend(bound_names);
2368            }
2369        }
2370
2371        // Bind params
2372        let param_types: Vec<Type> = params
2373            .iter()
2374            .map(|p| {
2375                let ty = self.air_type_node_to_type(p.kind.param_ty_node(), &gp_map);
2376                let pat_name = p.kind.param_pat_name();
2377                if let Some(n) = pat_name {
2378                    self.env.define(n, ty.clone());
2379                }
2380                ty
2381            })
2382            .collect();
2383
2384        let ret_ty = return_type
2385            .as_deref()
2386            .map(|n| self.air_type_node_to_type(n, &gp_map))
2387            .unwrap_or(Type::Primitive(PrimitiveType::Void));
2388
2389        // Inject effect operation types from the `with` clause so that
2390        // calls like `log("msg")` type-check inside effectful functions.
2391        {
2392            let mut visited = std::collections::HashSet::new();
2393            for effect_tp in &effect_clause {
2394                let ename = type_path_to_name(effect_tp);
2395                self.inject_effect_ops_into_env(&ename, &mut visited);
2396            }
2397        }
2398
2399        // Check where clause bounds (simple existence check — full trait
2400        // resolution is out of scope).
2401        self.check_where_clause(&where_clause, &gp_map, node.span);
2402
2403        // Push return type for `return` expressions
2404        self.return_ty_stack.push(ret_ty.clone());
2405
2406        // Check body — need mutable access via the original node.
2407        if let NodeKind::FnDecl { body, .. } = &mut node.kind {
2408            self.check_node(body, &ret_ty);
2409        }
2410
2411        self.return_ty_stack.pop();
2412        self.env.pop_scope();
2413        self.current_fn_param_bounds = saved_fn_param_bounds;
2414
2415        let effects: Vec<EffectRef> = effect_clause
2416            .iter()
2417            .map(|tp| {
2418                let name = tp
2419                    .segments
2420                    .iter()
2421                    .map(|s| s.name.as_str())
2422                    .collect::<Vec<_>>()
2423                    .join(".");
2424                EffectRef::new(name)
2425            })
2426            .collect();
2427
2428        let fn_ty = Type::Function(FnType {
2429            params: param_types,
2430            ret: Box::new(ret_ty),
2431            effects,
2432        });
2433        self.record(node, fn_ty);
2434    }
2435
2436    /// Type-check a constant declaration node in place.
2437    fn check_const_decl(&mut self, node: &mut AIRNode) {
2438        let (name, ty_node, _value_node) = match node.kind.clone() {
2439            NodeKind::ConstDecl {
2440                name, ty, value, ..
2441            } => (name, ty, value),
2442            _ => return,
2443        };
2444        let expected_ty = self.air_type_node_to_type(&ty_node, &HashMap::new());
2445        if let NodeKind::ConstDecl { value, .. } = &mut node.kind {
2446            self.check_node(value, &expected_ty);
2447        }
2448        self.env.define(name.name, expected_ty.clone());
2449        self.record(node, expected_ty);
2450    }
2451
2452    // ── Where-clause verification ────────────────────────────────────────────
2453
2454    /// Emit a diagnostic if any where-clause bound refers to a type parameter
2455    /// that is not in scope. Full trait-satisfaction checking is deferred.
2456    fn check_where_clause(
2457        &mut self,
2458        clauses: &[TypeConstraint],
2459        gp_map: &HashMap<String, Type>,
2460        span: Span,
2461    ) {
2462        for clause in clauses {
2463            if !gp_map.contains_key(&clause.param.name) {
2464                self.diags.error(
2465                    E_WHERE_CLAUSE,
2466                    format!(
2467                        "where-clause references unknown type parameter `{}`",
2468                        clause.param.name
2469                    ),
2470                    span,
2471                );
2472            }
2473        }
2474    }
2475
2476    // ── Effect operation injection ─────────────────────────────────────────
2477
2478    /// Recursively inject effect operation types into the current type
2479    /// environment. Handles composite effects by resolving components.
2480    fn inject_effect_ops_into_env(
2481        &mut self,
2482        effect_name: &str,
2483        visited: &mut std::collections::HashSet<String>,
2484    ) {
2485        if !visited.insert(effect_name.to_string()) {
2486            return;
2487        }
2488        if let Some(ops) = self.effect_op_types.get(effect_name).cloned() {
2489            for (op_name, fn_ty) in ops {
2490                self.env.define(op_name, fn_ty);
2491            }
2492        }
2493        if let Some(components) = self.effect_components.get(effect_name).cloned() {
2494            for comp in &components {
2495                self.inject_effect_ops_into_env(comp, visited);
2496            }
2497        }
2498    }
2499
2500    // ── Trait-bound enforcement at call sites ─────────────────────────────
2501
2502    /// Check that all where-clause bounds are satisfied after generic
2503    /// type-variable binding at a call site.
2504    ///
2505    /// `fn_name` is used in diagnostics.  `sig` provides the where-clause
2506    /// constraints and the mapping from generic-param names to the original
2507    /// [`TypeVarId`]s.  `fresh_map` maps those original ids to the fresh
2508    /// call-site type variables whose concrete types can be read from
2509    /// `self.subst`.
2510    fn check_trait_bounds_at_call(
2511        &mut self,
2512        fn_name: &str,
2513        sig: &FnSig,
2514        fresh_map: &HashMap<TypeVarId, Type>,
2515        span: Span,
2516    ) {
2517        let impl_table = match &self.impl_table {
2518            Some(t) => t,
2519            None => return, // no impl table → skip bound checking
2520        };
2521
2522        // Build name→fresh_type_var map for looking up the concrete type
2523        // each generic parameter was resolved to.
2524        let name_to_fresh: HashMap<&str, &Type> = sig
2525            .generic_params
2526            .iter()
2527            .zip(sig.generic_var_ids.iter())
2528            .filter_map(|(name, orig_id)| {
2529                fresh_map
2530                    .get(orig_id)
2531                    .map(|fresh_ty| (name.as_str(), fresh_ty))
2532            })
2533            .collect();
2534
2535        for clause in &sig.where_clause {
2536            let param_name = &clause.param.name;
2537            let concrete_ty = match name_to_fresh.get(param_name.as_str()) {
2538                Some(fresh) => self.subst.apply(fresh),
2539                None => continue, // unknown param — already diagnosed by check_where_clause
2540            };
2541
2542            for bound_path in &clause.bounds {
2543                let trait_name = bound_path
2544                    .segments
2545                    .iter()
2546                    .map(|s| s.name.as_str())
2547                    .collect::<Vec<_>>()
2548                    .join(".");
2549                let trait_ref = TraitRef::new(&trait_name);
2550                // Exact (non-parameterized) lookup first; then fall back to
2551                // *arg-imprecise* satisfaction for a parameterized bound such
2552                // as `T: Into[U]`. The bound's type argument is dropped at
2553                // parse time (the `where` clause stores only the trait path),
2554                // so a parameterized bound is satisfied when the concrete type
2555                // implements the trait for *some* argument. This is the
2556                // documented v1 limitation (see the session PR notes).
2557                let concrete_key = crate::traits::type_key(&concrete_ty);
2558                let satisfied = resolve_impl(&trait_ref, &concrete_ty, impl_table).is_some()
2559                    || impl_table.has_any_param_trait_impl(&trait_name, &concrete_key)
2560                    || self.abstract_param_satisfies_bound(&concrete_ty, &trait_name);
2561                if !satisfied {
2562                    // DQ29 (§18.5): an `Equatable` bound is ALSO satisfied by
2563                    // structural conformance — a record/enum whose fields /
2564                    // payloads are all Equatable passes without an explicit
2565                    // impl, exactly as the `==` operator gate accepts it. A
2566                    // structurally non-Equatable type is rejected with the
2567                    // same witness-carrying diagnostic as the gate
2568                    // (E4015 instead of the generic bound error).
2569                    if trait_name == "Equatable" {
2570                        let mut in_progress = HashSet::new();
2571                        let mut path = Vec::new();
2572                        match self.structural_equatable_witness(
2573                            &concrete_ty,
2574                            &mut in_progress,
2575                            &mut path,
2576                        ) {
2577                            None => continue,
2578                            Some(witness) => {
2579                                let (detail, suggestion) =
2580                                    equatable_failure_wording(&concrete_key, &witness);
2581                                self.diags
2582                                    .error(
2583                                        E_NOT_EQUATABLE,
2584                                        format!(
2585                                            "type `{concrete_ty}` does not satisfy bound \
2586                                             `Equatable` required by function `{fn_name}` \
2587                                             — {detail}"
2588                                        ),
2589                                        span,
2590                                    )
2591                                    .note(suggestion);
2592                                continue;
2593                            }
2594                        }
2595                    }
2596                    self.diags
2597                        .error(
2598                            E_WHERE_CLAUSE,
2599                            format!(
2600                                "type `{concrete_ty}` does not satisfy bound `{trait_name}` \
2601                                 required by function `{fn_name}`",
2602                            ),
2603                            span,
2604                        )
2605                        .note(format!(
2606                            "implement the trait for the type, e.g. `impl {trait_name} for \
2607                             {concrete_ty}`, or call `{fn_name}` with a conforming type"
2608                        ));
2609                }
2610            }
2611        }
2612    }
2613
2614    /// Whether `concrete_ty` satisfies `trait_name` *vacuously* because it is
2615    /// still an abstract type parameter that already carries the bound in the
2616    /// enclosing scope — not a concrete type the impl table could resolve.
2617    ///
2618    /// Two abstract forms arise at a generic call site:
2619    ///
2620    /// * `Type::Named(param)` — a type parameter of the function whose body we
2621    ///   are checking, forwarded into a callee with the same bound (e.g.
2622    ///   `from_list[T: Comparable]` calling `add[T: Comparable](…, x: T)`). The
2623    ///   bound holds because [`Self::current_fn_param_bounds`] records that the
2624    ///   enclosing function already requires `param: trait_name`.
2625    /// * `Type::TypeVar(_)` — an inference variable that unification has not yet
2626    ///   solved to a concrete type. It cannot be soundly *rejected* (the real
2627    ///   type may well conform), so it is treated as satisfied; the concrete
2628    ///   instantiation is bound-checked at the outer call site where the
2629    ///   variable resolves.
2630    ///
2631    /// This keeps the bracket-form `[T: Trait]` and `where (T: Trait)` bounds
2632    /// enforceable for *concrete* arguments while not falsely rejecting a
2633    /// generic function that legitimately forwards its own bounded parameter.
2634    fn abstract_param_satisfies_bound(&self, concrete_ty: &Type, trait_name: &str) -> bool {
2635        match concrete_ty {
2636            Type::Named(nt) => self
2637                .current_fn_param_bounds
2638                .get(&nt.name)
2639                .is_some_and(|bounds| bounds.iter().any(|b| b == trait_name)),
2640            Type::TypeVar(_) => true,
2641            _ => false,
2642        }
2643    }
2644
2645    // ── Bidirectional core ───────────────────────────────────────────────────
2646
2647    /// **Synthesis** (bottom-up): infer a type for `node` and record it.
2648    ///
2649    /// This is the internal mutable-node version; the public `infer_expr`
2650    /// provides read-only access via the side-table.
2651    fn infer_node(&mut self, node: &mut AIRNode) -> Type {
2652        let span = node.span;
2653        let ty = match &node.kind {
2654            // ── Literals ────────────────────────────────────────────────────
2655            NodeKind::Literal { lit } => self.infer_literal(lit),
2656
2657            // ── Identifier reference ─────────────────────────────────────────
2658            NodeKind::Identifier { name } => {
2659                let name = name.name.clone();
2660                match self.env.lookup(&name) {
2661                    Some(ty) => {
2662                        let ty = ty.clone();
2663                        self.subst.apply(&ty)
2664                    }
2665                    None => {
2666                        // The lowerer's method-call desugar duplicates the
2667                        // receiver node (see `reported_undefined`), so the
2668                        // same undefined identifier can be inferred twice at
2669                        // one span. Emit once per `(name, span)`.
2670                        if self.reported_undefined.insert((name.clone(), span)) {
2671                            self.diags.error(
2672                                E_UNDEFINED_VAR,
2673                                format!("undefined variable `{name}`"),
2674                                span,
2675                            );
2676                        }
2677                        Type::Error
2678                    }
2679                }
2680            }
2681
2682            // ── Binary operations ─────────────────────────────────────────────
2683            NodeKind::BinaryOp { op, .. } => {
2684                let op = *op;
2685                // Infer operands (need mutable access)
2686                let (lt, rt) = if let NodeKind::BinaryOp { left, right, .. } = &mut node.kind {
2687                    let lt = self.infer_node(left);
2688                    let rt = self.infer_node(right);
2689                    (lt, rt)
2690                } else {
2691                    unreachable!()
2692                };
2693                let result = self.infer_binop(op, &lt, &rt, span);
2694                // `+` on `List[T]` operands is concatenation, not numeric addition.
2695                // Stamp the node so codegen lowers it to each target's concat idiom
2696                // rather than a native `+` (which fails on TS/Rust/Go arrays/slices
2697                // and silently string-concats on JS). The result *or* either operand
2698                // resolving to a concrete `List` is sufficient — a record-field
2699                // receiver (`self.items + [x]`) may leave the unified result type a
2700                // still-open var while the left operand is already a concrete
2701                // `List`, so checking the operands too closes that gap.
2702                if matches!(op, BinOp::Add) {
2703                    let is_list = |t: &Type| matches!(self.subst.apply(t), Type::Generic(g) if g.constructor == "List");
2704                    if is_list(&result) || is_list(&lt) || is_list(&rt) {
2705                        node.metadata
2706                            .insert(LIST_CONCAT_META_KEY.to_string(), Value::Bool(true));
2707                    }
2708                    // String `+` is concatenation. Stamp it so the Rust backend
2709                    // lowers it to `format!` (Rust has no `String + String`). The
2710                    // result *or* either operand resolving to `String` is
2711                    // sufficient (an operand may still be an open var while the
2712                    // other side is already concrete `String`).
2713                    let is_string = |t: &Type| {
2714                        matches!(self.subst.apply(t), Type::Primitive(PrimitiveType::String))
2715                    };
2716                    if is_string(&result) || is_string(&lt) || is_string(&rt) {
2717                        node.metadata
2718                            .insert(STRING_CONCAT_META_KEY.to_string(), Value::Bool(true));
2719                    }
2720                }
2721                // `/` and `%` on two *integer* operands are integer division /
2722                // remainder with the cross-target truncate-toward-zero,
2723                // dividend-sign, abort-on-zero semantics fixed by DQ23 (§3.6).
2724                // Stamp the node so codegen lowers it to that contract rather than
2725                // the target's native operator (JS `/` is float division; Python
2726                // `//` floors and `%` follows floor division). Both operands must
2727                // resolve to an integer primitive — a mixed `Int`/`Float` operand
2728                // pair is a §4.2 type error reported by `infer_binop`, not stamped.
2729                if matches!(op, BinOp::Div | BinOp::Rem) {
2730                    let is_int = |t: &Type| {
2731                        matches!(
2732                            self.subst.apply(t),
2733                            Type::Primitive(
2734                                PrimitiveType::Int
2735                                    | PrimitiveType::Int8
2736                                    | PrimitiveType::Int16
2737                                    | PrimitiveType::Int32
2738                                    | PrimitiveType::Int64
2739                                    | PrimitiveType::Int128
2740                                    | PrimitiveType::UInt8
2741                                    | PrimitiveType::UInt16
2742                                    | PrimitiveType::UInt32
2743                                    | PrimitiveType::UInt64
2744                            )
2745                        )
2746                    };
2747                    if is_int(&lt) && is_int(&rt) {
2748                        node.metadata
2749                            .insert(INT_ARITH_META_KEY.to_string(), Value::Bool(true));
2750                    }
2751                }
2752                // `<`/`>`/`<=`/`>=` on two **user** (`Named`) operands that
2753                // implement `Comparable` must be lowered through the type's
2754                // `compare(self, other)` rather than the target's native ordering
2755                // operator (which is broken on every backend for user values, see
2756                // `USER_COMPARE_META_KEY`). `infer_binop` already accepted the
2757                // comparison (`require_comparable_operand`); stamp the node so
2758                // codegen routes it through `compare`. Probe the left operand,
2759                // falling back to the right only when the left stayed an inference
2760                // variable — mirroring the gate's post-unify probe.
2761                if matches!(op, BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge) {
2762                    let probe = match self.subst.apply(&lt) {
2763                        Type::TypeVar(_) => &rt,
2764                        _ => &lt,
2765                    };
2766                    if self.is_user_comparable(probe) {
2767                        node.metadata
2768                            .insert(USER_COMPARE_META_KEY.to_string(), Value::Bool(true));
2769                    }
2770                }
2771                // `==`/`!=` on operands whose native target equality is wrong
2772                // (records/enums/collections/tuples, explicit `impl Equatable`,
2773                // bounded generics) are stamped with the equality lane codegen
2774                // must use (DQ29 — see `USER_EQ_META_KEY`). Same post-unify
2775                // probe as the ordering stamp above.
2776                if matches!(op, BinOp::Eq | BinOp::Ne) {
2777                    let probe = match self.subst.apply(&lt) {
2778                        Type::TypeVar(_) => &rt,
2779                        _ => &lt,
2780                    };
2781                    if let Some(kind) = self.user_eq_kind(probe) {
2782                        node.metadata.insert(
2783                            USER_EQ_META_KEY.to_string(),
2784                            Value::String(kind.to_string()),
2785                        );
2786                    }
2787                }
2788                result
2789            }
2790
2791            // ── Unary operations ──────────────────────────────────────────────
2792            NodeKind::UnaryOp { op, .. } => {
2793                let op = *op;
2794                let operand_ty = if let NodeKind::UnaryOp { operand, .. } = &mut node.kind {
2795                    self.infer_node(operand)
2796                } else {
2797                    unreachable!()
2798                };
2799                self.infer_unop(op, &operand_ty, span)
2800            }
2801
2802            // ── Field access ──────────────────────────────────────────────────
2803            NodeKind::FieldAccess { field, .. } => {
2804                let field_name = field.name.clone();
2805                // §10.4 reserved surface: `Effect.handler(...)`. An effect
2806                // name is a *type*, not a value, so `Log` in value position
2807                // would otherwise fall through to a rule-less "undefined
2808                // variable" error. When the object is an unbound identifier
2809                // that names a known effect and the accessed member is
2810                // `handler`, report the actual rule (the lambda-handler
2811                // form is reserved until v1.x) instead — and suppress the
2812                // generic E4002 for the effect name (the lowerer's
2813                // method-call desugar also duplicates it as `args[0]`; see
2814                // `reported_undefined`).
2815                if field_name == "handler" {
2816                    if let NodeKind::FieldAccess { object, .. } = &node.kind {
2817                        if let NodeKind::Identifier { name } = &object.kind {
2818                            let effect_name = name.name.clone();
2819                            if self.env.lookup(&effect_name).is_none()
2820                                && (self.effect_op_types.contains_key(&effect_name)
2821                                    || self.effect_components.contains_key(&effect_name))
2822                            {
2823                                self.reported_undefined
2824                                    .insert((effect_name.clone(), object.span));
2825                                self.diags
2826                                    .error(
2827                                        E_RESERVED_LAMBDA_HANDLER,
2828                                        format!(
2829                                            "the lambda-handler form `{effect_name}.handler(...)` is reserved until v1.x"
2830                                        ),
2831                                        span,
2832                                    )
2833                                    .note(format!(
2834                                        "v1 supports one handler form: declare a record, `impl {effect_name} for <Record>`, then install it with `handle {effect_name} with <record>` (module level) or `handling ({effect_name} with <record>) {{ ... }}` (block level)"
2835                                    ));
2836                                return self.record(node, Type::Error);
2837                            }
2838                        }
2839                    }
2840                }
2841                let obj_ty = if let NodeKind::FieldAccess { object, .. } = &mut node.kind {
2842                    self.infer_node(object)
2843                } else {
2844                    unreachable!()
2845                };
2846                let obj_ty = self.subst.apply(&obj_ty);
2847                match &obj_ty {
2848                    Type::Error => Type::Error,
2849                    Type::Named(nt) => {
2850                        // Prefer a same-named *field* over a method in bare
2851                        // value position. A getter method whose name matches a
2852                        // field (`impl Error for SimpleError { fn message(self)
2853                        // -> String { self.message } }`) is idiomatic; reading
2854                        // `self.message` must yield the field's type, not the
2855                        // method's function type. Method *calls* still resolve
2856                        // the method type — the `Call` handler resolves a
2857                        // FieldAccess callee against `method_types` directly
2858                        // (see `resolve_user_method_fn_type`).
2859                        if let Some(fields) = self.record_field_types.get(&nt.name) {
2860                            if let Some((_, field_ty)) =
2861                                fields.iter().find(|(n, _)| n == &field_name)
2862                            {
2863                                return self.record(node, field_ty.clone());
2864                            }
2865                        }
2866                        // Look up method on the named type from inherent impls.
2867                        // Freshen the method's OWN type params per call site so
2868                        // they infer from the arguments
2869                        // (Q-checker-method-generic-call-infer).
2870                        if let Some(fn_ty) = self
2871                            .method_types
2872                            .get(&nt.name)
2873                            .and_then(|methods| methods.get(&field_name))
2874                            .cloned()
2875                        {
2876                            let resolved =
2877                                self.freshen_method_type_params(&nt.name, &field_name, fn_ty);
2878                            return self.record(node, resolved);
2879                        }
2880                        self.fresh_var()
2881                    }
2882                    Type::Generic(g) => {
2883                        // User-defined generic type: look up fields/methods by
2884                        // constructor name, substituting type params. Prefer a
2885                        // same-named *field* over a method in bare value
2886                        // position (see the `Named` case above for rationale).
2887                        if let Some(fields) = self.record_field_types.get(&g.constructor) {
2888                            if let Some((_, field_ty)) =
2889                                fields.iter().find(|(n, _)| n == &field_name)
2890                            {
2891                                let resolved = if let Some(params) =
2892                                    self.record_generic_params.get(&g.constructor)
2893                                {
2894                                    substitute_type_params(field_ty, params, &g.args)
2895                                } else {
2896                                    field_ty.clone()
2897                                };
2898                                return self.record(node, resolved);
2899                            }
2900                        }
2901                        if let Some(fn_ty) = self
2902                            .method_types
2903                            .get(&g.constructor)
2904                            .and_then(|methods| methods.get(&field_name))
2905                            .cloned()
2906                        {
2907                            // Pin the type's own params (`T`) to the receiver's
2908                            // concrete args, then freshen the method's OWN params
2909                            // (`U`) per call site so they infer from the
2910                            // arguments (Q-checker-method-generic-call-infer).
2911                            let resolved = if let Some(params) =
2912                                self.record_generic_params.get(&g.constructor)
2913                            {
2914                                substitute_type_params(&fn_ty, params, &g.args)
2915                            } else {
2916                                fn_ty
2917                            };
2918                            let resolved = self.freshen_method_type_params(
2919                                &g.constructor,
2920                                &field_name,
2921                                resolved,
2922                            );
2923                            return self.record(node, resolved);
2924                        }
2925                        // Fall through to built-in methods.
2926                        if let Some(fn_ty) =
2927                            self.resolve_builtin_method_fn_type(&obj_ty, &field_name)
2928                        {
2929                            fn_ty
2930                        } else {
2931                            self.fresh_var()
2932                        }
2933                    }
2934                    Type::TypeVar(id) => {
2935                        // Look up trait bounds for this type variable and
2936                        // resolve methods from the bounded traits.
2937                        if let Some(bounds) = self.type_var_bounds.get(id).cloned() {
2938                            let self_params = vec!["Self".to_string()];
2939                            let self_args = vec![obj_ty.clone()];
2940                            for trait_name in &bounds {
2941                                if let Some(methods) =
2942                                    self.trait_method_types.get(trait_name).cloned()
2943                                {
2944                                    if let Some(fn_ty) = methods.get(&field_name) {
2945                                        let resolved =
2946                                            substitute_type_params(fn_ty, &self_params, &self_args);
2947                                        return self.record(node, resolved);
2948                                    }
2949                                }
2950                            }
2951                        }
2952                        // Fall through to built-in methods.
2953                        if let Some(fn_ty) =
2954                            self.resolve_builtin_method_fn_type(&obj_ty, &field_name)
2955                        {
2956                            fn_ty
2957                        } else {
2958                            self.fresh_var()
2959                        }
2960                    }
2961                    Type::Primitive(_) => {
2962                        // Q-bridge (#104): consult canonical trait conformances
2963                        // first so e.g. `(1).compare(2)` types as
2964                        // `Fn(Int, Int) -> Ordering` rather than the intrinsic
2965                        // `compare -> Int` fallback. Falls through to the
2966                        // intrinsic method signatures for non-trait methods
2967                        // (`abs`, `to_string`, …) or when no conformance is in
2968                        // scope.
2969                        if let Some(fn_ty) =
2970                            self.resolve_primitive_canonical_method_fn_type(&obj_ty, &field_name)
2971                        {
2972                            fn_ty
2973                        } else if let Some(fn_ty) =
2974                            self.resolve_builtin_method_fn_type(&obj_ty, &field_name)
2975                        {
2976                            fn_ty
2977                        } else {
2978                            self.fresh_var()
2979                        }
2980                    }
2981                    _ => {
2982                        // Check built-in method signatures for Generic / Primitive types.
2983                        if let Some(fn_ty) =
2984                            self.resolve_builtin_method_fn_type(&obj_ty, &field_name)
2985                        {
2986                            fn_ty
2987                        } else {
2988                            // Return a fresh type var; downstream calls may unify it.
2989                            self.fresh_var()
2990                        }
2991                    }
2992                }
2993            }
2994
2995            // ── Index access ──────────────────────────────────────────────────
2996            NodeKind::Index { .. } => {
2997                let (obj_ty, idx_ty) = if let NodeKind::Index { object, index } = &mut node.kind {
2998                    let o = self.infer_node(object);
2999                    let i = self.infer_node(index);
3000                    (o, i)
3001                } else {
3002                    unreachable!()
3003                };
3004                // Check index is an integer
3005                self.unify_or_error(&idx_ty, &Type::Primitive(PrimitiveType::Int), span, "index");
3006                // Element type is a fresh var
3007                match &obj_ty {
3008                    Type::Error => Type::Error,
3009                    Type::Generic(g) if g.constructor == "List" && g.args.len() == 1 => {
3010                        g.args[0].clone()
3011                    }
3012                    _ => self.fresh_var(),
3013                }
3014            }
3015
3016            // ── Function call ─────────────────────────────────────────────────
3017            NodeKind::Call { .. } => {
3018                // Q-prim-assoc: a primitive associated-conversion call
3019                // (`Float.from(3)`, `Int.try_from(s)`) resolves against the
3020                // canonical primitive conversions, not the ordinary callee path
3021                // (the primitive type name is not a value binding). Handle it
3022                // first; `None` means "not such a call", so fall through.
3023                if let Some(result_ty) = self.try_resolve_primitive_conversion_call(node) {
3024                    return self.record(node, result_ty);
3025                }
3026
3027                // Clone callee/args to avoid borrow issues; rewrite below.
3028                let (callee_clone, args_clone, _type_args_clone) = if let NodeKind::Call {
3029                    callee,
3030                    args,
3031                    type_args,
3032                } = &node.kind
3033                {
3034                    (*callee.clone(), args.clone(), type_args.clone())
3035                } else {
3036                    unreachable!()
3037                };
3038
3039                // Extract callee name for generic function lookup.
3040                let callee_name = if let NodeKind::Identifier { name } = &callee_clone.kind {
3041                    Some(name.name.clone())
3042                } else {
3043                    None
3044                };
3045
3046                // Infer callee type via mutable sub-node
3047                let mut callee_ty = if let NodeKind::Call { callee, .. } = &mut node.kind {
3048                    self.infer_node(callee)
3049                } else {
3050                    unreachable!()
3051                };
3052
3053                // Receiver-type annotation (checker→codegen): a desugared method
3054                // call is `Call { callee: FieldAccess(recv, method), args:
3055                // [recv, …] }`. Inferring the callee above recorded the
3056                // receiver's type in the side-table, so stamp the call node with
3057                // the receiver category for codegen (see `RECV_KIND_META_KEY`).
3058                if let NodeKind::FieldAccess { object, field, .. } = &callee_clone.kind {
3059                    if let Some(recv_ty) = self.types.get(&object.id).cloned() {
3060                        self.stamp_recv_kind(node, &recv_ty);
3061                        // The FieldAccess handler prefers a same-named *field*
3062                        // over a method in value position, so a method call
3063                        // whose name collides with a field would otherwise see
3064                        // the (non-callable) field type here. In call-callee
3065                        // position the method takes precedence: re-resolve the
3066                        // method's function type from `method_types` and use it.
3067                        let recv_ty = self.subst.apply(&recv_ty);
3068                        // DQ22: `contains` is not a `Map` method. Map membership is
3069                        // `contains_key` (key) / `contains_value` (value); bare
3070                        // `contains` is `Set`-only (a Set has only elements, so it
3071                        // is unambiguous there). Reject `m.contains(...)` with a
3072                        // precise "did you mean `contains_key`?" suggestion rather
3073                        // than letting the unknown method resolve to a fresh type
3074                        // variable. NOT aliased to `contains_key`. Handled ahead of
3075                        // the general unknown-method check so the Map-specific
3076                        // wording (and the `contains_value` hint) wins.
3077                        let map_contains = field.name == "contains"
3078                            && matches!(&recv_ty, Type::Generic(g)
3079                                if g.constructor == "Map" && g.args.len() == 2);
3080                        if map_contains {
3081                            self.diags
3082                                .error(
3083                                    E_NO_SUCH_METHOD,
3084                                    "`contains` is not a method on `Map`; \
3085                                     did you mean `contains_key`?",
3086                                    field.span,
3087                                )
3088                                .note(
3089                                    "use `contains_key(k)` to test for a key \
3090                                     or `contains_value(v)` for a value; bare \
3091                                     `contains` is a `Set` method",
3092                                );
3093                        } else {
3094                            // Q-checker-unknown-method-concrete: a method that does
3095                            // not resolve on a concrete, closed-method-set receiver
3096                            // is an error (with a nearest-name suggestion) — not a
3097                            // silent fresh type variable. A no-op for §4.9
3098                            // `Flexible`/sketch receivers, inference vars, and user
3099                            // types whose definition is not in scope.
3100                            self.check_unknown_method_on_concrete(
3101                                &recv_ty,
3102                                &field.name,
3103                                field.span,
3104                            );
3105                        }
3106                        if !matches!(callee_ty, Type::Function(_)) {
3107                            if let Some(fn_ty) =
3108                                self.resolve_user_method_fn_type(&recv_ty, &field.name)
3109                            {
3110                                callee_ty = fn_ty;
3111                            }
3112                        }
3113                    }
3114                }
3115
3116                // For generic functions, create a fresh instantiation with
3117                // new type vars so each call site gets independent inference.
3118                // Also capture the sig + fresh_map for trait-bound checking.
3119                let mut call_site_info: Option<(String, FnSig, HashMap<TypeVarId, Type>)> = None;
3120                let effective_ty = match (&callee_name, &callee_ty) {
3121                    (Some(name), Type::Function(f)) => {
3122                        if let Some(sig) = self.fn_sigs.get(name).cloned() {
3123                            if !sig.generic_params.is_empty() {
3124                                let fresh_map: HashMap<TypeVarId, Type> = sig
3125                                    .generic_var_ids
3126                                    .iter()
3127                                    .map(|&id| (id, self.fresh_var()))
3128                                    .collect();
3129                                let ety = Type::Function(FnType {
3130                                    params: f
3131                                        .params
3132                                        .iter()
3133                                        .map(|t| self.replace_type_vars(t, &fresh_map))
3134                                        .collect(),
3135                                    ret: Box::new(self.replace_type_vars(&f.ret, &fresh_map)),
3136                                    effects: f.effects.clone(),
3137                                });
3138                                call_site_info = Some((name.clone(), sig, fresh_map));
3139                                ety
3140                            } else {
3141                                callee_ty.clone()
3142                            }
3143                        } else {
3144                            callee_ty.clone()
3145                        }
3146                    }
3147                    _ => callee_ty.clone(),
3148                };
3149
3150                let ret_ty = self.check_call(callee_clone.span, &effective_ty, &args_clone, span);
3151
3152                // Now type-check each arg node in place
3153                if let NodeKind::Call { args, .. } = &mut node.kind {
3154                    match &effective_ty {
3155                        Type::Function(f) => {
3156                            for (arg, param_ty) in args.iter_mut().zip(f.params.iter()) {
3157                                let pt = self.subst.apply(param_ty);
3158                                self.check_node(&mut arg.value, &pt);
3159                            }
3160                        }
3161                        _ => {
3162                            for arg in args.iter_mut() {
3163                                self.infer_node(&mut arg.value);
3164                            }
3165                        }
3166                    }
3167                }
3168
3169                // After args are checked (and type vars unified), verify
3170                // where-clause trait bounds.
3171                if let Some((fn_name, sig, fresh_map)) = &call_site_info {
3172                    self.check_trait_bounds_at_call(fn_name, sig, fresh_map, span);
3173                }
3174
3175                ret_ty
3176            }
3177
3178            // ── Method call ───────────────────────────────────────────────────
3179            NodeKind::MethodCall { method, .. } => {
3180                let method_name = method.name.clone();
3181                let method_span = method.span;
3182                let receiver_ty =
3183                    if let NodeKind::MethodCall { receiver, args, .. } = &mut node.kind {
3184                        let rt = self.infer_node(receiver);
3185                        for arg in args.iter_mut() {
3186                            self.infer_node(&mut arg.value);
3187                        }
3188                        rt
3189                    } else {
3190                        unreachable!()
3191                    };
3192                // Receiver-type annotation (checker→codegen): the AIR lowerer
3193                // desugars most method calls into the `Call(FieldAccess(…))`
3194                // form, but stamp a surviving `MethodCall` too so the annotation
3195                // is comprehensive regardless of lowering shape.
3196                self.stamp_recv_kind(node, &receiver_ty);
3197                // Q-checker-unknown-method-concrete: flag an unknown method on a
3198                // concrete receiver here too (mirrors the desugared `Call` path),
3199                // so a surviving `MethodCall` shape is covered. A no-op for §4.9
3200                // `Flexible`/sketch and other open receivers.
3201                self.check_unknown_method_on_concrete(&receiver_ty, &method_name, method_span);
3202                self.resolve_method_return_type(&receiver_ty, &method_name)
3203            }
3204
3205            // ── Lambda ────────────────────────────────────────────────────────
3206            NodeKind::Lambda { .. } => {
3207                // With no expected type, give each param a fresh var and infer body.
3208                let (param_tys, body_ty) = self.infer_lambda(node);
3209                Type::Function(FnType {
3210                    params: param_tys,
3211                    ret: Box::new(body_ty),
3212                    effects: vec![],
3213                })
3214            }
3215
3216            // ── Pipe ──────────────────────────────────────────────────────────
3217            NodeKind::Pipe { .. } => {
3218                // `left |> f` desugars to `f(left)`.
3219                let (lty, rty) = if let NodeKind::Pipe { left, right } = &mut node.kind {
3220                    let l = self.infer_node(left);
3221                    let r = self.infer_node(right);
3222                    (l, r)
3223                } else {
3224                    unreachable!()
3225                };
3226                // rty should be a function; its return type is the pipe result.
3227                match &rty {
3228                    Type::Function(f) if f.params.len() == 1 => {
3229                        let param_ty = self.subst.apply(&f.params[0]);
3230                        self.unify_or_error(&lty, &param_ty, span, "pipe");
3231                        self.subst.apply(&f.ret)
3232                    }
3233                    Type::Error => Type::Error,
3234                    _ => self.fresh_var(),
3235                }
3236            }
3237
3238            // ── If expression ─────────────────────────────────────────────────
3239            NodeKind::If { .. } => self.infer_if(node),
3240
3241            // ── Match expression ──────────────────────────────────────────────
3242            NodeKind::Match { .. } => self.infer_match(node),
3243
3244            // ── Block ─────────────────────────────────────────────────────────
3245            NodeKind::Block { .. } => self.infer_block(node),
3246
3247            // ── Let binding ───────────────────────────────────────────────────
3248            NodeKind::LetBinding { .. } => {
3249                self.check_let_binding(node);
3250                Type::Primitive(PrimitiveType::Void)
3251            }
3252
3253            // ── Return ────────────────────────────────────────────────────────
3254            NodeKind::Return { .. } => {
3255                let expected = self.return_ty_stack.last().cloned();
3256                if let NodeKind::Return { value } = &mut node.kind {
3257                    match (value, &expected) {
3258                        (Some(v), Some(e)) => {
3259                            let et = e.clone();
3260                            self.check_node(v, &et);
3261                        }
3262                        (Some(v), None) => {
3263                            self.infer_node(v);
3264                        }
3265                        _ => {}
3266                    }
3267                }
3268                Type::Primitive(PrimitiveType::Never)
3269            }
3270
3271            // ── List literal ──────────────────────────────────────────────────
3272            NodeKind::ListLiteral { .. } => {
3273                let elem_ty = self.fresh_var();
3274                if let NodeKind::ListLiteral { elems } = &mut node.kind {
3275                    for elem in elems.iter_mut() {
3276                        let et = elem_ty.clone();
3277                        self.check_node(elem, &et);
3278                    }
3279                }
3280                Type::Generic(GenericType {
3281                    constructor: "List".into(),
3282                    args: vec![self.subst.apply(&elem_ty)],
3283                })
3284            }
3285
3286            // ── Tuple literal ─────────────────────────────────────────────────
3287            NodeKind::TupleLiteral { .. } => {
3288                let elem_tys: Vec<Type> = if let NodeKind::TupleLiteral { elems } = &mut node.kind {
3289                    elems.iter_mut().map(|e| self.infer_node(e)).collect()
3290                } else {
3291                    vec![]
3292                };
3293                Type::Tuple(elem_tys)
3294            }
3295
3296            // ── Map literal ───────────────────────────────────────────────────
3297            NodeKind::MapLiteral { .. } => {
3298                let k_ty = self.fresh_var();
3299                let v_ty = self.fresh_var();
3300                if let NodeKind::MapLiteral { entries } = &mut node.kind {
3301                    for entry in entries.iter_mut() {
3302                        let kt = k_ty.clone();
3303                        let vt = v_ty.clone();
3304                        self.check_node(&mut entry.key, &kt);
3305                        self.check_node(&mut entry.value, &vt);
3306                    }
3307                }
3308                Type::Generic(GenericType {
3309                    constructor: "Map".into(),
3310                    args: vec![self.subst.apply(&k_ty), self.subst.apply(&v_ty)],
3311                })
3312            }
3313
3314            // ── Set literal ───────────────────────────────────────────────────
3315            NodeKind::SetLiteral { .. } => {
3316                let elem_ty = self.fresh_var();
3317                if let NodeKind::SetLiteral { elems } = &mut node.kind {
3318                    for elem in elems.iter_mut() {
3319                        let et = elem_ty.clone();
3320                        self.check_node(elem, &et);
3321                    }
3322                }
3323                Type::Generic(GenericType {
3324                    constructor: "Set".into(),
3325                    args: vec![self.subst.apply(&elem_ty)],
3326                })
3327            }
3328
3329            // ── String interpolation ──────────────────────────────────────────
3330            NodeKind::Interpolation { .. } => {
3331                if let NodeKind::Interpolation { parts } = &mut node.kind {
3332                    for part in parts.iter_mut() {
3333                        if let bock_air::AirInterpolationPart::Expr(e) = part {
3334                            let part_ty = self.infer_node(e);
3335                            // A `Bool`-typed `${expr}` part must stringify to the
3336                            // canonical lowercase `"true"`/`"false"` (§3.5). Python
3337                            // f-strings would otherwise print `True`/`False`; stamp
3338                            // the part node so the Python backend lowercases it. The
3339                            // part's resolved type is not otherwise reachable from
3340                            // codegen (it lives only in the dropped side-table).
3341                            if matches!(
3342                                self.subst.apply(&part_ty),
3343                                Type::Primitive(PrimitiveType::Bool)
3344                            ) {
3345                                e.metadata
3346                                    .insert(BOOL_STRINGIFY_META_KEY.to_string(), Value::Bool(true));
3347                            }
3348                        }
3349                    }
3350                }
3351                Type::Primitive(PrimitiveType::String)
3352            }
3353
3354            // ── Optional / Result construction ────────────────────────────────
3355            NodeKind::ResultConstruct { variant, .. } => {
3356                // Copy variant (it's Copy) so we drop the immutable borrow before
3357                // we need &mut node.kind below.
3358                let variant = *variant;
3359                let has_value =
3360                    matches!(&node.kind, NodeKind::ResultConstruct { value: Some(_), .. });
3361                let inner_ty = if has_value {
3362                    if let NodeKind::ResultConstruct { value: Some(v), .. } = &mut node.kind {
3363                        self.infer_node(v)
3364                    } else {
3365                        unreachable!()
3366                    }
3367                } else {
3368                    Type::Primitive(PrimitiveType::Void)
3369                };
3370                let err_ty = self.fresh_var();
3371                let ok_ty = self.fresh_var();
3372                match variant {
3373                    bock_air::ResultVariant::Ok => {
3374                        self.unify_or_error(&inner_ty, &ok_ty, span, "Ok construct");
3375                        Type::Result(Box::new(ok_ty), Box::new(err_ty))
3376                    }
3377                    bock_air::ResultVariant::Err => {
3378                        self.unify_or_error(&inner_ty, &err_ty, span, "Err construct");
3379                        Type::Result(Box::new(ok_ty), Box::new(err_ty))
3380                    }
3381                }
3382            }
3383
3384            // ── Propagate (?) ─────────────────────────────────────────────────
3385            NodeKind::Propagate { .. } => {
3386                let inner_ty = if let NodeKind::Propagate { expr } = &mut node.kind {
3387                    self.infer_node(expr)
3388                } else {
3389                    unreachable!()
3390                };
3391                // `expr?` unwraps a Result[T, E] or Optional[T]; type is T.
3392                match &inner_ty {
3393                    Type::Result(ok, _) => *ok.clone(),
3394                    Type::Optional(inner) => *inner.clone(),
3395                    Type::Error => Type::Error,
3396                    _ => self.fresh_var(),
3397                }
3398            }
3399
3400            // ── Await ─────────────────────────────────────────────────────────
3401            NodeKind::Await { .. } => {
3402                if let NodeKind::Await { expr } = &mut node.kind {
3403                    self.infer_node(expr);
3404                }
3405                self.fresh_var()
3406            }
3407
3408            // ── Borrow / Move ─────────────────────────────────────────────────
3409            NodeKind::Borrow { .. } | NodeKind::MutableBorrow { .. } => {
3410                // Ownership tracking is done in a later pass; propagate inner type.
3411                match &mut node.kind {
3412                    NodeKind::Borrow { expr } | NodeKind::MutableBorrow { expr } => {
3413                        self.infer_node(expr)
3414                    }
3415                    _ => unreachable!(),
3416                }
3417            }
3418
3419            NodeKind::Move { .. } => {
3420                if let NodeKind::Move { expr } = &mut node.kind {
3421                    self.infer_node(expr)
3422                } else {
3423                    unreachable!()
3424                }
3425            }
3426
3427            // ── Assign ────────────────────────────────────────────────────────
3428            NodeKind::Assign { .. } => {
3429                let (tty, vty) = if let NodeKind::Assign { target, value, .. } = &mut node.kind {
3430                    let t = self.infer_node(target);
3431                    let v = self.infer_node(value);
3432                    (t, v)
3433                } else {
3434                    unreachable!()
3435                };
3436                // Orientation: the assignment target establishes the
3437                // expected type; the assigned value is the found type.
3438                self.unify_or_error(&vty, &tty, span, "assignment");
3439                Type::Primitive(PrimitiveType::Void)
3440            }
3441
3442            // ── Range ─────────────────────────────────────────────────────────
3443            NodeKind::Range { .. } => {
3444                let (lty, hty) = if let NodeKind::Range { lo, hi, .. } = &mut node.kind {
3445                    let l = self.infer_node(lo);
3446                    let h = self.infer_node(hi);
3447                    (l, h)
3448                } else {
3449                    unreachable!()
3450                };
3451                // Orientation: the low bound establishes the expected type;
3452                // the high bound is the found type.
3453                self.unify_or_error(&hty, &lty, span, "range bounds");
3454                Type::Generic(GenericType {
3455                    constructor: "Range".into(),
3456                    args: vec![lty],
3457                })
3458            }
3459
3460            // ── Loops ─────────────────────────────────────────────────────────
3461            NodeKind::For { .. } => {
3462                let node_span = node.span;
3463                // First, infer the iterable so we can classify it. The built-in
3464                // collections (`List`/`Range`, and `Map`/`Set` element typing
3465                // below) keep their native fast path; a *user* type that
3466                // implements `Iterable` is rewritten (§18.5 desugar) into the
3467                // proven `loop { match it.next() { Some(pat) => body; None =>
3468                // break } }` shape so it lowers through the already-native
3469                // loop/match codegen with no per-target `for`-over-user-type
3470                // support.
3471                let iter_ty = if let NodeKind::For { iterable, .. } = &mut node.kind {
3472                    self.infer_node(iterable)
3473                } else {
3474                    unreachable!()
3475                };
3476                let resolved_iter_ty = self.subst.apply(&iter_ty);
3477
3478                let is_builtin_iterable = matches!(
3479                    &resolved_iter_ty,
3480                    Type::Generic(g)
3481                        if matches!(g.constructor.as_str(), "List" | "Range" | "Map" | "Set")
3482                );
3483
3484                // Desugar only a *user* type (not a built-in collection) that
3485                // has a registered `Iterable` impl for some type argument.
3486                if !is_builtin_iterable {
3487                    let implements_iterable = self
3488                        .impl_table
3489                        .as_ref()
3490                        .map(|table| {
3491                            let key = crate::traits::type_key(&resolved_iter_ty);
3492                            resolve_impl(&TraitRef::new("Iterable"), &resolved_iter_ty, table)
3493                                .is_some()
3494                                || table.has_any_param_trait_impl("Iterable", &key)
3495                        })
3496                        .unwrap_or(false);
3497
3498                    if implements_iterable {
3499                        // Move the user's pattern / iterable / body out of the
3500                        // `For` node into the synthesized subtree.
3501                        let (pattern, iterable, body) = if let NodeKind::For {
3502                            pattern,
3503                            iterable,
3504                            body,
3505                        } = &mut node.kind
3506                        {
3507                            (
3508                                std::mem::replace(
3509                                    pattern,
3510                                    Box::new(AIRNode::new(0, node_span, NodeKind::Placeholder)),
3511                                ),
3512                                std::mem::replace(
3513                                    iterable,
3514                                    Box::new(AIRNode::new(0, node_span, NodeKind::Placeholder)),
3515                                ),
3516                                std::mem::replace(
3517                                    body,
3518                                    Box::new(AIRNode::new(0, node_span, NodeKind::Placeholder)),
3519                                ),
3520                            )
3521                        } else {
3522                            unreachable!()
3523                        };
3524
3525                        // Gensym a unique binding name so nested desugared `for`
3526                        // loops do not shadow one another.
3527                        let n = self.synth_iter_var.get();
3528                        self.synth_iter_var.set(n.wrapping_add(1));
3529                        let iter_var = format!("__bock_iter_{n}");
3530
3531                        self.desugar_for_iterable(node, *pattern, *iterable, *body, &iter_var);
3532                        // Re-infer the rewritten subtree (now a `Block`) through
3533                        // the normal path, so the synthesized `match`/`Some(pat)`
3534                        // / method-call nodes pick up the element typing and the
3535                        // codegen metadata (Optional payload, receiver kind).
3536                        return self.infer_block(node);
3537                    }
3538                }
3539
3540                // Built-in / fallback path: element-type the loop variable from
3541                // the iterable's generic argument (List/Range/Map/Set), else a
3542                // fresh var, exactly as before.
3543                self.env.push_scope();
3544                if let NodeKind::For {
3545                    pattern,
3546                    iterable: _,
3547                    body,
3548                } = &mut node.kind
3549                {
3550                    let elem_ty = match &resolved_iter_ty {
3551                        Type::Generic(g) if g.constructor == "List" && g.args.len() == 1 => {
3552                            g.args[0].clone()
3553                        }
3554                        Type::Generic(g) if g.constructor == "Range" && g.args.len() == 1 => {
3555                            g.args[0].clone()
3556                        }
3557                        _ => self.fresh_var(),
3558                    };
3559                    self.bind_pattern_type(pattern, &elem_ty);
3560                    self.infer_node(body);
3561                }
3562                self.env.pop_scope();
3563                Type::Primitive(PrimitiveType::Void)
3564            }
3565
3566            NodeKind::While { .. } => {
3567                if let NodeKind::While { condition, body } = &mut node.kind {
3568                    let bool_ty = Type::Primitive(PrimitiveType::Bool);
3569                    self.check_node(condition, &bool_ty);
3570                    self.infer_node(body);
3571                }
3572                Type::Primitive(PrimitiveType::Void)
3573            }
3574
3575            NodeKind::Loop { .. } => {
3576                if let NodeKind::Loop { body } = &mut node.kind {
3577                    self.infer_node(body);
3578                }
3579                // A `loop` with a break value would need more analysis; use fresh var.
3580                self.fresh_var()
3581            }
3582
3583            NodeKind::Break { .. } => {
3584                if let NodeKind::Break { value: Some(v) } = &mut node.kind {
3585                    self.infer_node(v);
3586                }
3587                Type::Primitive(PrimitiveType::Never)
3588            }
3589
3590            NodeKind::Continue => Type::Primitive(PrimitiveType::Never),
3591
3592            NodeKind::Guard { .. } => {
3593                if let NodeKind::Guard {
3594                    let_pattern,
3595                    condition,
3596                    else_block,
3597                } = &mut node.kind
3598                {
3599                    if let_pattern.is_some() {
3600                        // guard (let pat = expr) — infer the condition type
3601                        // and bind pattern variables into the current scope
3602                        // (they must be visible after the guard statement).
3603                        let cond_ty = self.infer_node(condition);
3604                        if let Some(pat) = let_pattern {
3605                            self.bind_pattern_type(pat, &cond_ty);
3606                        }
3607                    } else {
3608                        let bool_ty = Type::Primitive(PrimitiveType::Bool);
3609                        self.check_node(condition, &bool_ty);
3610                    }
3611                    self.infer_node(else_block);
3612                }
3613                Type::Primitive(PrimitiveType::Void)
3614            }
3615
3616            // ── Compose ───────────────────────────────────────────────────────
3617            NodeKind::Compose { .. } => {
3618                if let NodeKind::Compose { left, right } = &mut node.kind {
3619                    self.infer_node(left);
3620                    self.infer_node(right);
3621                }
3622                self.fresh_var() // f >> g: detailed typing deferred
3623            }
3624
3625            // ── Placeholder ───────────────────────────────────────────────────
3626            NodeKind::Placeholder => self.fresh_var(),
3627
3628            // ── Unreachable ───────────────────────────────────────────────────
3629            NodeKind::Unreachable => Type::Primitive(PrimitiveType::Never),
3630
3631            // ── Handling block ────────────────────────────────────────────────
3632            NodeKind::HandlingBlock { .. } => {
3633                if let NodeKind::HandlingBlock { handlers, body } = &mut node.kind {
3634                    for hp in handlers.iter_mut() {
3635                        self.infer_node(&mut hp.handler);
3636                    }
3637                    // §10.4 bare-op form: inject the handled effects' operation
3638                    // types into a fresh env scope so a bare op call inside the
3639                    // block (`log(...)`) type-checks. Mirrors the resolver's
3640                    // op injection in `resolve_handling`. The scope is popped
3641                    // after the body so the ops do not leak past the block.
3642                    let effect_names: Vec<String> = handlers
3643                        .iter()
3644                        .map(|hp| type_path_to_name(&hp.effect))
3645                        .collect();
3646                    self.env.push_scope();
3647                    let mut visited = std::collections::HashSet::new();
3648                    for ename in &effect_names {
3649                        self.inject_effect_ops_into_env(ename, &mut visited);
3650                    }
3651                    let ty = self.infer_node(body);
3652                    self.env.pop_scope();
3653                    ty
3654                } else {
3655                    unreachable!()
3656                }
3657            }
3658
3659            // ── RecordConstruct ───────────────────────────────────────────────
3660            NodeKind::RecordConstruct { path, .. } => {
3661                let name = path
3662                    .segments
3663                    .last()
3664                    .map(|s| s.name.clone())
3665                    .unwrap_or_default();
3666
3667                // If this is a generic record, create fresh type vars for
3668                // each type parameter so we can infer them from field values.
3669                let generic_params = self.record_generic_params.get(&name).cloned();
3670                let fresh_type_args: Option<Vec<Type>> = generic_params
3671                    .as_ref()
3672                    .map(|params| params.iter().map(|_| self.fresh_var()).collect());
3673
3674                if let NodeKind::RecordConstruct { fields, spread, .. } = &mut node.kind {
3675                    // Type-check each field value against the declared field type.
3676                    let declared_fields = self.record_field_types.get(&name).cloned();
3677                    for f in fields.iter_mut() {
3678                        if let Some(v) = &mut f.value {
3679                            if let Some(ref decl) = declared_fields {
3680                                if let Some((_, expected_ty)) =
3681                                    decl.iter().find(|(n, _)| n == &f.name.name)
3682                                {
3683                                    // For generic records, substitute param names
3684                                    // (e.g. Named("A")) with fresh type vars.
3685                                    let et = if let (Some(ref params), Some(ref args)) =
3686                                        (&generic_params, &fresh_type_args)
3687                                    {
3688                                        substitute_type_params(expected_ty, params, args)
3689                                    } else {
3690                                        expected_ty.clone()
3691                                    };
3692                                    self.check_node(v, &et);
3693                                } else {
3694                                    self.infer_node(v);
3695                                }
3696                            } else {
3697                                self.infer_node(v);
3698                            }
3699                        }
3700                    }
3701                    if let Some(s) = spread {
3702                        self.infer_node(s);
3703                    }
3704                }
3705
3706                // For generic records, return Generic with the inferred type args.
3707                if let Some(type_args) = fresh_type_args {
3708                    Type::Generic(GenericType {
3709                        constructor: name,
3710                        args: type_args,
3711                    })
3712                } else {
3713                    // Non-generic: look up in env. For enum record variants,
3714                    // this resolves to the parent enum type.
3715                    self.env
3716                        .lookup(&name)
3717                        .cloned()
3718                        .unwrap_or(Type::Named(crate::NamedType { name }))
3719                }
3720            }
3721
3722            // ── Error node ────────────────────────────────────────────────────
3723            NodeKind::Error => Type::Error,
3724
3725            // ── Everything else: return a fresh var ───────────────────────────
3726            _ => self.fresh_var(),
3727        };
3728
3729        self.record(node, ty)
3730    }
3731
3732    /// **Checking** (top-down): verify `node` has type `expected`, emitting a
3733    /// diagnostic if not. Falls back to `infer_node` for most expression forms.
3734    fn check_node(&mut self, node: &mut AIRNode, expected: &Type) {
3735        let span = node.span;
3736        match &node.kind {
3737            // ── Check mode for list literals ─────────────────────────────────
3738            NodeKind::ListLiteral { .. } => {
3739                if let Type::Generic(g) = expected {
3740                    if g.constructor == "List" && g.args.len() == 1 {
3741                        let elem_ty = g.args[0].clone();
3742                        if let NodeKind::ListLiteral { elems } = &mut node.kind {
3743                            for elem in elems.iter_mut() {
3744                                let et = elem_ty.clone();
3745                                self.check_node(elem, &et);
3746                            }
3747                        }
3748                        self.record(node, expected.clone());
3749                        return;
3750                    }
3751                }
3752                // Fallthrough to infer mode
3753                let inferred = self.infer_node(node);
3754                self.unify_or_error(&inferred, expected, span, "list literal");
3755            }
3756
3757            // ── Check mode for lambdas (push param types from context) ────────
3758            NodeKind::Lambda { .. } => {
3759                if let Type::Function(f_expected) = expected {
3760                    let param_types = f_expected.params.clone();
3761                    let ret_ty = *f_expected.ret.clone();
3762
3763                    self.env.push_scope();
3764                    if let NodeKind::Lambda { params, body } = &mut node.kind {
3765                        for (param, pty) in params.iter_mut().zip(param_types.iter()) {
3766                            if let Some(name) = param.kind.param_pat_name() {
3767                                self.env.define(name, pty.clone());
3768                            }
3769                            self.record(param, pty.clone());
3770                        }
3771                        self.check_node(body, &ret_ty);
3772                    }
3773                    self.env.pop_scope();
3774                    self.record(node, expected.clone());
3775                } else {
3776                    let inferred = self.infer_node(node);
3777                    self.unify_or_error(&inferred, expected, span, "lambda");
3778                }
3779            }
3780
3781            // ── Check mode for match expression ───────────────────────────────
3782            NodeKind::Match { .. } => {
3783                // Type-check scrutinee by inference, then check each arm body
3784                // against the expected type.
3785                let scrutinee_ty = if let NodeKind::Match { scrutinee, .. } = &mut node.kind {
3786                    self.infer_node(scrutinee)
3787                } else {
3788                    unreachable!()
3789                };
3790
3791                if let NodeKind::Match { arms, .. } = &mut node.kind {
3792                    for arm in arms.iter_mut() {
3793                        self.env.push_scope();
3794                        if let NodeKind::MatchArm {
3795                            pattern,
3796                            guard,
3797                            body,
3798                        } = &mut arm.kind
3799                        {
3800                            self.bind_pattern_type(pattern, &scrutinee_ty.clone());
3801                            if let Some(g) = guard {
3802                                let bt = Type::Primitive(PrimitiveType::Bool);
3803                                self.check_node(g, &bt);
3804                            }
3805                            let et = expected.clone();
3806                            self.check_node(body, &et);
3807                        }
3808                        self.env.pop_scope();
3809                        self.record(arm, expected.clone());
3810                    }
3811                }
3812                self.record(node, expected.clone());
3813            }
3814
3815            // ── Check mode for if expression ──────────────────────────────────
3816            NodeKind::If { .. } => {
3817                if let NodeKind::If {
3818                    condition,
3819                    then_block,
3820                    else_block,
3821                    ..
3822                } = &mut node.kind
3823                {
3824                    let bt = Type::Primitive(PrimitiveType::Bool);
3825                    self.check_node(condition, &bt);
3826                    let et = expected.clone();
3827                    self.check_node(then_block, &et);
3828                    if let Some(eb) = else_block {
3829                        let et2 = expected.clone();
3830                        self.check_node(eb, &et2);
3831                    }
3832                }
3833                self.record(node, expected.clone());
3834            }
3835
3836            // ── Check mode for block ──────────────────────────────────────────
3837            NodeKind::Block { .. } => {
3838                if let NodeKind::Block { stmts, tail } = &mut node.kind {
3839                    self.env.push_scope();
3840                    for stmt in stmts.iter_mut() {
3841                        self.infer_node(stmt);
3842                    }
3843                    if let Some(tail_expr) = tail {
3844                        let et = expected.clone();
3845                        self.check_node(tail_expr, &et);
3846                    } else {
3847                        // No tail: block type is Void; unify with expected.
3848                        let void_ty = Type::Primitive(PrimitiveType::Void);
3849                        self.unify_or_error(&void_ty, expected, node.span, "block");
3850                    }
3851                    self.env.pop_scope();
3852                }
3853                self.record(node, expected.clone());
3854            }
3855
3856            // ── Check mode for `.into()` (return-type-driven conversion) ──────
3857            // A `receiver.into()` call lowers to
3858            // `Call { callee: FieldAccess(receiver, "into"), args: [self] }`.
3859            // In check mode the target type `U` comes from the expected type:
3860            // we look up the blanket/explicit `Into[U] for A` impl (where `A`
3861            // is the receiver type). On success the call's type is exactly `U`;
3862            // on failure we emit `E4012`. This is the inline resolution hook —
3863            // no obligation queue. If the expected type is not yet concrete
3864            // (no reachable annotation) we fall through to ordinary inference,
3865            // which keeps `.into()` usable only where a target type is known
3866            // (the documented v1 annotation-required limitation).
3867            NodeKind::Call { callee, args, .. }
3868                if args.len() == 1
3869                    && matches!(
3870                        &callee.kind,
3871                        NodeKind::FieldAccess { field, .. } if field.name == "into"
3872                    ) =>
3873            {
3874                let target = self.subst.apply(expected);
3875                // Infer the receiver (the desugared `self` argument).
3876                let receiver_ty = if let NodeKind::Call { args, .. } = &mut node.kind {
3877                    self.infer_node(&mut args[0].value)
3878                } else {
3879                    unreachable!()
3880                };
3881                let receiver_ty = self.subst.apply(&receiver_ty);
3882
3883                // Only attempt conversion resolution when both the target and
3884                // the receiver are concrete enough to key the impl table. A
3885                // type-variable target means no reachable annotation — fall
3886                // through to generic inference.
3887                let resolvable = !matches!(target, Type::TypeVar(_) | Type::Error)
3888                    && !matches!(receiver_ty, Type::TypeVar(_) | Type::Error);
3889                if resolvable {
3890                    if let Some(table) = self.impl_table.as_ref() {
3891                        let trait_ref = TraitRef::parameterized("Into", vec![target.clone()]);
3892                        if resolve_impl(&trait_ref, &receiver_ty, table).is_some() {
3893                            self.record(node, target.clone());
3894                            return;
3895                        }
3896                        // No matching conversion: emit a precise diagnostic.
3897                        self.diags.error(
3898                            E_NO_CONVERSION,
3899                            format!(
3900                                "cannot convert `{}` into `{}` via `.into()`: no `From`/`Into`                                  impl relates these types",
3901                                crate::traits::type_key(&receiver_ty),
3902                                crate::traits::type_key(&target),
3903                            ),
3904                            span,
3905                        );
3906                        self.record(node, target.clone());
3907                        return;
3908                    }
3909                }
3910                // Fall through: no impl table or target not reachable.
3911                let inferred = self.infer_node(node);
3912                let expected = self.subst.apply(expected);
3913                self.unify_or_error(&inferred, &expected, span, "expression");
3914            }
3915
3916            // ── Everything else: infer then check ─────────────────────────────
3917            _ => {
3918                let inferred = self.infer_node(node);
3919                let expected = self.subst.apply(expected);
3920                self.unify_or_error(&inferred, &expected, span, "expression");
3921            }
3922        }
3923    }
3924
3925    // ── If expression ────────────────────────────────────────────────────────
3926
3927    fn infer_if(&mut self, node: &mut AIRNode) -> Type {
3928        let span = node.span;
3929        if let NodeKind::If {
3930            condition,
3931            then_block,
3932            else_block,
3933            ..
3934        } = &mut node.kind
3935        {
3936            let bool_ty = Type::Primitive(PrimitiveType::Bool);
3937            self.check_node(condition, &bool_ty);
3938            let then_ty = self.infer_node(then_block);
3939            if let Some(eb) = else_block {
3940                let else_ty = self.infer_node(eb);
3941                let never = Type::Primitive(PrimitiveType::Never);
3942                // If one branch diverges (Never), the result is the other branch's type.
3943                let (a, b) = if then_ty == never {
3944                    (&else_ty, &then_ty)
3945                } else {
3946                    (&then_ty, &else_ty)
3947                };
3948                // Orientation: the first (non-diverging) branch establishes
3949                // the expected type; the other branch is the found type.
3950                self.unify_or_error(b, a, span, "if-else branches")
3951            } else {
3952                // No else: result is Optional[then_ty] or Void
3953                Type::Primitive(PrimitiveType::Void)
3954            }
3955        } else {
3956            unreachable!()
3957        }
3958    }
3959
3960    // ── Match expression ─────────────────────────────────────────────────────
3961
3962    fn infer_match(&mut self, node: &mut AIRNode) -> Type {
3963        let span = node.span;
3964        let never = Type::Primitive(PrimitiveType::Never);
3965        // Infer scrutinee type
3966        let scrutinee_ty = if let NodeKind::Match { scrutinee, .. } = &mut node.kind {
3967            self.infer_node(scrutinee)
3968        } else {
3969            unreachable!()
3970        };
3971
3972        // Infer each arm's body type, collecting them.
3973        let mut arm_types: Vec<Type> = Vec::new();
3974        if let NodeKind::Match { arms, .. } = &mut node.kind {
3975            for arm in arms.iter_mut() {
3976                self.env.push_scope();
3977                let arm_ty = if let NodeKind::MatchArm {
3978                    pattern,
3979                    guard,
3980                    body,
3981                } = &mut arm.kind
3982                {
3983                    self.bind_pattern_type(pattern, &scrutinee_ty.clone());
3984                    if let Some(g) = guard {
3985                        let bt = Type::Primitive(PrimitiveType::Bool);
3986                        self.check_node(g, &bt);
3987                    }
3988                    self.infer_node(body)
3989                } else {
3990                    self.fresh_var()
3991                };
3992                self.env.pop_scope();
3993                self.record(arm, arm_ty.clone());
3994                arm_types.push(arm_ty);
3995            }
3996        }
3997
3998        // Filter out Never arms; unify the rest.
3999        let non_never: Vec<&Type> = arm_types.iter().filter(|t| **t != never).collect();
4000        if non_never.is_empty() {
4001            // All arms diverge — match type is Never.
4002            never
4003        } else {
4004            let result_ty = self.fresh_var();
4005            for t in &non_never {
4006                let rt = result_ty.clone();
4007                self.unify_or_error(t, &rt, span, "match arm");
4008            }
4009            self.subst.apply(&result_ty)
4010        }
4011    }
4012
4013    // ── Block expression ─────────────────────────────────────────────────────
4014
4015    fn infer_block(&mut self, node: &mut AIRNode) -> Type {
4016        self.env.push_scope();
4017        let ty = if let NodeKind::Block { stmts, tail } = &mut node.kind {
4018            for stmt in stmts.iter_mut() {
4019                self.infer_node(stmt);
4020            }
4021            if let Some(tail_expr) = tail {
4022                self.infer_node(tail_expr)
4023            } else {
4024                Type::Primitive(PrimitiveType::Void)
4025            }
4026        } else {
4027            unreachable!()
4028        };
4029        self.env.pop_scope();
4030        ty
4031    }
4032
4033    // ── Let binding ──────────────────────────────────────────────────────────
4034
4035    fn check_let_binding(&mut self, node: &mut AIRNode) {
4036        let (ty_node, _value_clone) = match &node.kind {
4037            NodeKind::LetBinding { ty, value, .. } => (ty.clone(), *value.clone()),
4038            _ => return,
4039        };
4040
4041        if let Some(ty_ann) = &ty_node {
4042            let expected = self.air_type_node_to_type(ty_ann, &HashMap::new());
4043            if let NodeKind::LetBinding { value, pattern, .. } = &mut node.kind {
4044                self.check_node(value, &expected);
4045                self.bind_pattern_type(pattern, &expected);
4046            }
4047        } else {
4048            // No annotation: infer from value
4049            let inferred = if let NodeKind::LetBinding { value, .. } = &mut node.kind {
4050                self.infer_node(value)
4051            } else {
4052                unreachable!()
4053            };
4054            let resolved = self.subst.apply(&inferred);
4055            if let NodeKind::LetBinding { pattern, .. } = &mut node.kind {
4056                self.bind_pattern_type(pattern, &resolved);
4057            }
4058        }
4059    }
4060
4061    // ── Lambda inference ─────────────────────────────────────────────────────
4062
4063    /// Infer types for a lambda with no check context (fresh vars for params).
4064    fn infer_lambda(&mut self, node: &mut AIRNode) -> (Vec<Type>, Type) {
4065        self.env.push_scope();
4066        let (param_tys, body_ty) = if let NodeKind::Lambda { params, body } = &mut node.kind {
4067            let param_tys: Vec<Type> = params
4068                .iter_mut()
4069                .map(|p| {
4070                    let ty = self.fresh_var();
4071                    if let Some(name) = p.kind.param_pat_name() {
4072                        self.env.define(name, ty.clone());
4073                    }
4074                    ty
4075                })
4076                .collect();
4077            let body_ty = self.infer_node(body);
4078            (param_tys, body_ty)
4079        } else {
4080            unreachable!()
4081        };
4082        self.env.pop_scope();
4083        (param_tys, body_ty)
4084    }
4085
4086    // ── Function call type checking ──────────────────────────────────────────
4087
4088    /// Given the type of the callee and the argument list, return the return type.
4089    /// Handles generic instantiation.
4090    fn check_call(
4091        &mut self,
4092        callee_span: Span,
4093        callee_ty: &Type,
4094        args: &[bock_air::AirArg],
4095        call_span: Span,
4096    ) -> Type {
4097        match callee_ty {
4098            Type::Error => Type::Error,
4099            Type::Function(f) => {
4100                // Non-generic call: check arity then return ret type.
4101                if f.params.len() != args.len() {
4102                    self.diags.error(
4103                        E_ARITY_MISMATCH,
4104                        format!(
4105                            "function expects {} argument(s), got {}",
4106                            f.params.len(),
4107                            args.len()
4108                        ),
4109                        call_span,
4110                    );
4111                    return Type::Error;
4112                }
4113                self.subst.apply(&f.ret)
4114            }
4115            _ => {
4116                // Could still be a named function looked up in env.
4117                // If callee_ty is Named, try to find in fn_sigs.
4118                if let Type::Named(nt) = callee_ty {
4119                    if let Some(sig) = self.fn_sigs.get(&nt.name).cloned() {
4120                        return self.instantiate_and_check(&nt.name, &sig, args, call_span);
4121                    }
4122                }
4123                // If the callee's type is still an inference variable (e.g.
4124                // a method call on a parameter with an unknown built-in
4125                // type like `Channel[T]`), don't commit to "not callable" —
4126                // return a fresh var so downstream code can continue.
4127                if matches!(callee_ty, Type::TypeVar(_)) {
4128                    return self.fresh_var();
4129                }
4130                self.diags.error(
4131                    E_NOT_CALLABLE,
4132                    format!("expected a function type, got {callee_ty:?}"),
4133                    callee_span,
4134                );
4135                Type::Error
4136            }
4137        }
4138    }
4139
4140    /// Instantiate a generic function signature with fresh type vars and
4141    /// return the (substituted) return type.
4142    ///
4143    /// Maps the original [`TypeVarId`]s from the signature to new fresh
4144    /// variables using [`replace_type_vars`](Self::replace_type_vars), so
4145    /// each call site gets independent type inference.
4146    fn instantiate_and_check(
4147        &mut self,
4148        fn_name: &str,
4149        sig: &FnSig,
4150        args: &[bock_air::AirArg],
4151        span: Span,
4152    ) -> Type {
4153        if sig.param_types.len() != args.len() {
4154            self.diags.error(
4155                E_ARITY_MISMATCH,
4156                format!(
4157                    "function expects {} argument(s), got {}",
4158                    sig.param_types.len(),
4159                    args.len()
4160                ),
4161                span,
4162            );
4163            return Type::Error;
4164        }
4165
4166        // Create fresh vars for each generic parameter, keyed by the
4167        // original TypeVarId from collect_sig.
4168        let fresh_map: HashMap<TypeVarId, Type> = sig
4169            .generic_var_ids
4170            .iter()
4171            .map(|&id| (id, self.fresh_var()))
4172            .collect();
4173
4174        // Substitute generic params in param types (used for arg unification
4175        // by the caller) and return type.
4176        let _param_tys: Vec<Type> = sig
4177            .param_types
4178            .iter()
4179            .map(|t| self.replace_type_vars(t, &fresh_map))
4180            .collect();
4181
4182        // Check where-clause trait bounds.
4183        self.check_trait_bounds_at_call(fn_name, sig, &fresh_map, span);
4184
4185        // Substitute in return type.
4186        self.replace_type_vars(&sig.return_type, &fresh_map)
4187    }
4188
4189    // ── Method return-type resolution ─────────────────────────────────────
4190
4191    /// Resolve a primitive method-call return type via a *canonical* trait
4192    /// conformance, if one applies.
4193    ///
4194    /// Q-bridge (#104): primitives gain trait methods (`compare`, `eq`, …)
4195    /// through compiler-registered canonical conformances in `impl_table`.
4196    /// This helper fires only when **all** of the following hold:
4197    ///
4198    /// 1. the receiver is a primitive (checked by the caller),
4199    /// 2. some in-scope trait (in `trait_method_types`) declares `method`,
4200    /// 3. a canonical conformance for that trait is registered for the
4201    ///    receiver in `impl_table`.
4202    ///
4203    /// When matched, returns the trait method's declared return type with the
4204    /// `Self` type mapped to the concrete receiver. Returns `None` (fall
4205    /// through to the intrinsic arms) when no such conformance is in scope —
4206    /// preserving behavior for code that never imports the core trait.
4207    fn resolve_primitive_canonical_method_return(
4208        &self,
4209        receiver_ty: &Type,
4210        method: &str,
4211    ) -> Option<Type> {
4212        let impl_table = self.impl_table.as_ref()?;
4213
4214        // Find an in-scope trait that declares `method` AND has a canonical
4215        // conformance registered for this receiver. Iterating `trait_method_types`
4216        // keeps the lookup gated on the trait actually being imported (cond. 2/3).
4217        for (trait_name, methods) in &self.trait_method_types {
4218            let Some(Type::Function(fn_ty)) = methods.get(method) else {
4219                continue;
4220            };
4221            let trait_ref = TraitRef::new(trait_name);
4222            if resolve_impl(&trait_ref, receiver_ty, impl_table).is_none() {
4223                continue;
4224            }
4225            // Map the trait method's declared return type, substituting the
4226            // `Self` placeholder with the concrete receiver type. The return
4227            // type is otherwise already concrete (e.g. `Ordering`, `Bool`).
4228            let self_params = ["Self".to_string()];
4229            let self_args = [receiver_ty.clone()];
4230            return Some(substitute_type_params(&fn_ty.ret, &self_params, &self_args));
4231        }
4232        None
4233    }
4234
4235    /// Resolve the full *function* type of a primitive method call via a
4236    /// canonical trait conformance, if one applies.
4237    ///
4238    /// Q-bridge (#104): the AIR lowers `(1).compare(2)` to
4239    /// `Call(FieldAccess(1, "compare"), [1, 2])`, so the `FieldAccess` handler
4240    /// resolves the method's whole function type (receiver as the first
4241    /// parameter). This mirrors [`Self::resolve_primitive_canonical_method_return`]
4242    /// but returns the full `Fn(Self, …) -> Ret` type with every `Self`
4243    /// occurrence (params *and* return) mapped to the concrete receiver — so
4244    /// `(1).compare(2)` types as `Fn(Int, Int) -> Ordering` and the call
4245    /// yields `Ordering`, matchable against its variants.
4246    ///
4247    /// Gating matches the return-type helper: fires only when the receiver is
4248    /// primitive, an in-scope trait declares `method`, and a canonical
4249    /// conformance for that trait is registered for the receiver. Falls
4250    /// through (returns `None`) otherwise, preserving the intrinsic fast path.
4251    fn resolve_primitive_canonical_method_fn_type(
4252        &self,
4253        receiver_ty: &Type,
4254        method: &str,
4255    ) -> Option<Type> {
4256        let impl_table = self.impl_table.as_ref()?;
4257        for (trait_name, methods) in &self.trait_method_types {
4258            let Some(fn_ty @ Type::Function(_)) = methods.get(method) else {
4259                continue;
4260            };
4261            let trait_ref = TraitRef::new(trait_name);
4262            if resolve_impl(&trait_ref, receiver_ty, impl_table).is_none() {
4263                continue;
4264            }
4265            let self_params = ["Self".to_string()];
4266            let self_args = [receiver_ty.clone()];
4267            return Some(substitute_type_params(fn_ty, &self_params, &self_args));
4268        }
4269        None
4270    }
4271
4272    /// Resolve the full *function* type of a user-defined method (registered in
4273    /// `method_types`) on a receiver type, with the type's generic params
4274    /// substituted to the receiver's concrete arguments.
4275    ///
4276    /// Used by the `Call` handler so a method *call* whose name collides with a
4277    /// same-named record field still resolves the method (the `FieldAccess`
4278    /// handler prefers the field in bare value position; this restores the
4279    /// method type when the FieldAccess is a call callee).
4280    ///
4281    /// The method's OWN type parameters (e.g. the `U` in
4282    /// `Box[T].map[U](f: Fn(T) -> U) -> Box[U]`) are replaced with *fresh*
4283    /// inference variables per call site, so they are inferred from the call
4284    /// arguments — the method-level analogue of free-function call inference
4285    /// (Q-checker-method-generic-call-infer). The receiver pins the type's own
4286    /// params (`T`); only the method's own params (`U`) are freshened here.
4287    fn resolve_user_method_fn_type(&self, receiver_ty: &Type, method: &str) -> Option<Type> {
4288        let receiver_ty = self.subst.apply(receiver_ty);
4289        let (type_name, fn_ty) = match &receiver_ty {
4290            Type::Named(nt) => {
4291                let fn_ty = self
4292                    .method_types
4293                    .get(&nt.name)
4294                    .and_then(|m| m.get(method))
4295                    .cloned()?;
4296                (nt.name.clone(), fn_ty)
4297            }
4298            Type::Generic(g) => {
4299                let fn_ty = self
4300                    .method_types
4301                    .get(&g.constructor)
4302                    .and_then(|m| m.get(method))
4303                    .cloned()?;
4304                // Pin the type's own params (`T`) to the receiver's concrete args.
4305                let fn_ty = if let Some(params) = self.record_generic_params.get(&g.constructor) {
4306                    substitute_type_params(&fn_ty, params, &g.args)
4307                } else {
4308                    fn_ty
4309                };
4310                (g.constructor.clone(), fn_ty)
4311            }
4312            _ => return None,
4313        };
4314        Some(self.freshen_method_type_params(&type_name, method, fn_ty))
4315    }
4316
4317    /// Q-prim-assoc: resolve the **primitive** associated-conversion call form
4318    /// `Prim.from(x)` / `Prim.try_from(x)` (e.g. `Float.from(3)`,
4319    /// `Int.try_from(s)`), returning the call's result type when it resolves
4320    /// against a canonical primitive conversion.
4321    ///
4322    /// The lowerer represents `Type.method(args)` as a `Call` whose callee is
4323    /// `FieldAccess(Identifier(Type), method)` stamped with
4324    /// [`bock_air::lower::ASSOC_CALL_META_KEY`] (no `self` prepended). For a
4325    /// *user* type the `Identifier(Type)` infers to a `Named` type and the
4326    /// `FieldAccess`/`method_types` path resolves the impl's `from`/`try_from`.
4327    /// A *primitive* type name (`Int`/`Float`/`String`/`Char`/…) is not bound
4328    /// in the value env, so that path would emit `E4002 undefined variable`.
4329    /// This hook intercepts those calls and resolves them against the canonical
4330    /// primitive conversions registered by
4331    /// [`crate::traits::register_canonical_conversions`]:
4332    ///
4333    /// - `Prim.from(x)` resolves `From[typeof(x)] for Prim` and yields `Prim`.
4334    /// - `Prim.try_from(x)` resolves `TryFrom[typeof(x)] for Prim` and yields
4335    ///   `Result[Prim, ConvertError]`.
4336    ///
4337    /// Returns `Some(result_ty)` on a successful resolution (after inferring the
4338    /// argument so its node is typed for codegen). Returns `None` when the call
4339    /// is not a primitive associated `from`/`try_from` (let the ordinary Call
4340    /// path handle it). When the callee *is* a primitive `from`/`try_from` but
4341    /// no canonical conversion relates the argument type to the target, emits
4342    /// `E4012` and returns `Some(Type::Error)` so the call is not double-reported
4343    /// by the generic path.
4344    fn try_resolve_primitive_conversion_call(&mut self, node: &mut AIRNode) -> Option<Type> {
4345        if !is_associated_call_node(node) {
4346            return None;
4347        }
4348        // Destructure the callee shape: FieldAccess(Identifier(P), method).
4349        let (target_prim, method, method_span) = {
4350            let NodeKind::Call { callee, .. } = &node.kind else {
4351                return None;
4352            };
4353            let NodeKind::FieldAccess { object, field } = &callee.kind else {
4354                return None;
4355            };
4356            let NodeKind::Identifier { name } = &object.kind else {
4357                return None;
4358            };
4359            let prim = name_to_primitive(&name.name)?;
4360            let method = field.name.clone();
4361            if method != "from" && method != "try_from" {
4362                return None;
4363            }
4364            (prim, method, field.span)
4365        };
4366        let target_ty = Type::Primitive(target_prim);
4367
4368        // Infer the sole conversion argument (its node must be typed for codegen).
4369        let arg_ty = {
4370            let NodeKind::Call { args, .. } = &mut node.kind else {
4371                return None;
4372            };
4373            if args.len() != 1 {
4374                // A primitive `from`/`try_from` takes exactly one source value.
4375                return None;
4376            }
4377            self.infer_node(&mut args[0].value)
4378        };
4379        let arg_ty = self.subst.apply(&arg_ty);
4380
4381        // An unresolved / error argument can't key the impl table; defer to the
4382        // generic path rather than risk a spurious E4012.
4383        if matches!(arg_ty, Type::TypeVar(_) | Type::Error) {
4384            return None;
4385        }
4386
4387        let trait_name = if method == "from" { "From" } else { "TryFrom" };
4388        let resolves = self
4389            .impl_table
4390            .as_ref()
4391            .map(|table| {
4392                let trait_ref = TraitRef::parameterized(trait_name, vec![arg_ty.clone()]);
4393                resolve_impl(&trait_ref, &target_ty, table).is_some()
4394            })
4395            .unwrap_or(false);
4396
4397        if resolves {
4398            let result_ty = if method == "from" {
4399                target_ty
4400            } else {
4401                // `TryFrom::try_from` returns `Result[Self, ConvertError]`.
4402                Type::Result(
4403                    Box::new(target_ty),
4404                    Box::new(Type::Named(crate::NamedType {
4405                        name: "ConvertError".to_string(),
4406                    })),
4407                )
4408            };
4409            return Some(result_ty);
4410        }
4411
4412        // The callee is a primitive `from`/`try_from`, but no canonical
4413        // conversion relates the argument type to the target primitive. Reject
4414        // cleanly with `E4012` (mirrors the `.into()` no-conversion diagnostic).
4415        self.diags.error(
4416            E_NO_CONVERSION,
4417            format!(
4418                "cannot convert `{}` to `{}` via `{}.{}()`: no canonical `{}` \
4419                 conversion relates these types",
4420                crate::traits::type_key(&arg_ty),
4421                crate::traits::type_key(&target_ty),
4422                crate::traits::type_key(&target_ty),
4423                method,
4424                trait_name,
4425            ),
4426            method_span,
4427        );
4428        Some(Type::Error)
4429    }
4430
4431    /// Replace a method's OWN generic type parameters with fresh inference
4432    /// variables (Q-checker-method-generic-call-infer).
4433    ///
4434    /// A method like `fn map[U](...)` registers its own param names (`["U"]`) in
4435    /// `method_generic_params`. Those names survive in the stored method type as
4436    /// `Named("U")` placeholders. At each call site they must become *fresh*
4437    /// inference variables so the method's own params unify against the call
4438    /// arguments independently per call — exactly as `instantiate_and_check`
4439    /// freshens a free function's type params. The receiver has already pinned
4440    /// the type's own params before this runs, so only the method's own params
4441    /// remain to be freshened.
4442    fn freshen_method_type_params(&self, type_name: &str, method: &str, fn_ty: Type) -> Type {
4443        let Some(names) = self
4444            .method_generic_params
4445            .get(type_name)
4446            .and_then(|m| m.get(method))
4447        else {
4448            return fn_ty;
4449        };
4450        if names.is_empty() {
4451            return fn_ty;
4452        }
4453        let fresh: Vec<Type> = names.iter().map(|_| self.fresh_var()).collect();
4454        substitute_type_params(&fn_ty, names, &fresh)
4455    }
4456
4457    /// Conversion methods that are resolved by dedicated machinery (the
4458    /// `.into()` inline hook and the `From`/`TryFrom` impl table), *not* the
4459    /// per-receiver built-in method matches. The unknown-method check must
4460    /// never flag these — they legitimately resolve on receivers whose closed
4461    /// method set does not list them.
4462    const CONVERSION_METHODS: &'static [&'static str] = &["into", "from", "try_from"];
4463
4464    /// Q-checker-unknown-method-concrete: returns `true` when `method` resolves
4465    /// on `receiver_ty` through *any* path the checker knows — built-in
4466    /// intrinsics, canonical primitive trait conformances, user inherent/trait
4467    /// impls (`method_types`), record/class field-closures (a `field()` call),
4468    /// or the conversion hooks. Used to decide whether an unknown-method
4469    /// diagnostic is warranted on a concrete receiver.
4470    fn method_is_resolvable(&self, receiver_ty: &Type, method: &str) -> bool {
4471        let receiver_ty = self.subst.apply(receiver_ty);
4472
4473        // Conversion methods resolve through dedicated machinery.
4474        if Self::CONVERSION_METHODS.contains(&method) {
4475            return true;
4476        }
4477
4478        // Built-in intrinsic method (List/Map/Set/String/Int/…/Optional/Result).
4479        if self
4480            .resolve_builtin_method_fn_type(&receiver_ty, method)
4481            .is_some()
4482        {
4483            return true;
4484        }
4485
4486        // Primitive canonical-trait conformance (`compare`/`eq`/`to_string`/…),
4487        // gated on the trait actually being in scope.
4488        if matches!(receiver_ty, Type::Primitive(_))
4489            && self
4490                .resolve_primitive_canonical_method_fn_type(&receiver_ty, method)
4491                .is_some()
4492        {
4493            return true;
4494        }
4495
4496        // User type: inherent/trait-impl method, or a same-named field-closure.
4497        let user_name = match &receiver_ty {
4498            Type::Named(nt) => Some(&nt.name),
4499            Type::Generic(g) => Some(&g.constructor),
4500            _ => None,
4501        };
4502        if let Some(name) = user_name {
4503            if self
4504                .method_types
4505                .get(name)
4506                .is_some_and(|m| m.contains_key(method))
4507            {
4508                return true;
4509            }
4510            if self
4511                .record_field_types
4512                .get(name)
4513                .is_some_and(|fs| fs.iter().any(|(n, _)| n == method))
4514            {
4515                return true;
4516            }
4517        }
4518
4519        // Trait *default* methods: a concrete type that implements a trait
4520        // inherits every default method the trait declares but the impl did not
4521        // override. Such methods live in `trait_method_types` (the trait's
4522        // signatures), not in the type's `method_types`, so check every trait
4523        // the receiver implements — and that trait's supertraits — for a
4524        // declaration of `method`. This keeps inherited defaults (e.g.
4525        // `Eq::not_equals` calling the required `equals`) resolvable.
4526        if self.type_implements_trait_method(&receiver_ty, method) {
4527            return true;
4528        }
4529
4530        false
4531    }
4532
4533    /// Q-checker-unknown-method-concrete: returns `true` when `receiver_ty`
4534    /// implements some trait (directly, or via a supertrait of one it
4535    /// implements) that declares `method` in [`Self::trait_method_types`]. This
4536    /// covers inherited trait *default* methods, which are not registered in the
4537    /// type's own `method_types`.
4538    fn type_implements_trait_method(&self, receiver_ty: &Type, method: &str) -> bool {
4539        let Some(table) = self.impl_table.as_ref() else {
4540            return false;
4541        };
4542        let key = crate::traits::type_key(receiver_ty);
4543        for entry in table.entries() {
4544            if entry.type_key != key {
4545                continue;
4546            }
4547            let Some(trait_ref) = &entry.trait_ref else {
4548                continue;
4549            };
4550            // The directly-implemented trait, plus its supertraits.
4551            if self
4552                .trait_method_types
4553                .get(&trait_ref.name)
4554                .is_some_and(|m| m.contains_key(method))
4555            {
4556                return true;
4557            }
4558            for supertrait in table.all_supertraits(&trait_ref.name) {
4559                if self
4560                    .trait_method_types
4561                    .get(&supertrait)
4562                    .is_some_and(|m| m.contains_key(method))
4563                {
4564                    return true;
4565                }
4566            }
4567        }
4568        false
4569    }
4570
4571    /// Q-checker-unknown-method-concrete: the candidate method names for a
4572    /// **concrete, closed-method-set** receiver — used both to gate the
4573    /// unknown-method diagnostic (a `None` result means the receiver is not a
4574    /// closed concrete type, so no diagnostic) and to compute a nearest-name
4575    /// suggestion.
4576    ///
4577    /// Returns `None` for receivers whose method set is *open* or not fully
4578    /// known at this point:
4579    /// - `Type::TypeVar` — an unresolved inference variable; methods may resolve
4580    ///   once it is unified (and bounded-trait methods apply).
4581    /// - `Type::Flexible` — §4.9 sketch-mode narrowing resolves methods
4582    ///   aggressively by design; the diagnostic must never leak here.
4583    /// - `Type::Error` — poison; already diagnosed.
4584    /// - `Type::Function` / `Type::Tuple` / `Type::Refined` — no method surface.
4585    /// - a `Named`/`Generic` user type whose definition is not in scope (no
4586    ///   `record_field_types`/`method_types` entry) — its method set is unknown,
4587    ///   so suppress rather than risk a false positive.
4588    fn concrete_closed_method_names(&self, receiver_ty: &Type) -> Option<Vec<String>> {
4589        let receiver_ty = self.subst.apply(receiver_ty);
4590        match &receiver_ty {
4591            // Closed built-in receivers.
4592            Type::Primitive(p) if !matches!(p, PrimitiveType::Void | PrimitiveType::Never) => {
4593                let mut names = self.builtin_method_names(&receiver_ty);
4594                // Canonical primitive trait methods that are in scope.
4595                for methods in self.trait_method_types.values() {
4596                    for m in methods.keys() {
4597                        if self
4598                            .resolve_primitive_canonical_method_fn_type(&receiver_ty, m)
4599                            .is_some()
4600                        {
4601                            names.push(m.clone());
4602                        }
4603                    }
4604                }
4605                Some(names)
4606            }
4607            Type::Optional(_) | Type::Result(_, _) => Some(self.builtin_method_names(&receiver_ty)),
4608            Type::Generic(g) if matches!(g.constructor.as_str(), "List" | "Map" | "Set") => {
4609                let mut names = self.builtin_method_names(&receiver_ty);
4610                // A user `impl` on a built-in generic (rare) contributes too.
4611                if let Some(m) = self.method_types.get(&g.constructor) {
4612                    names.extend(m.keys().cloned());
4613                }
4614                Some(names)
4615            }
4616            // Known user types (definition in scope): the closed set is the
4617            // registered methods plus the record/class fields.
4618            Type::Named(nt) => {
4619                if !self.record_field_types.contains_key(&nt.name)
4620                    && !self.method_types.contains_key(&nt.name)
4621                {
4622                    return None;
4623                }
4624                let mut names: Vec<String> = self
4625                    .method_types
4626                    .get(&nt.name)
4627                    .map(|m| m.keys().cloned().collect())
4628                    .unwrap_or_default();
4629                if let Some(fs) = self.record_field_types.get(&nt.name) {
4630                    names.extend(fs.iter().map(|(n, _)| n.clone()));
4631                }
4632                Some(names)
4633            }
4634            Type::Generic(g) => {
4635                if !self.record_field_types.contains_key(&g.constructor)
4636                    && !self.method_types.contains_key(&g.constructor)
4637                {
4638                    return None;
4639                }
4640                let mut names: Vec<String> = self
4641                    .method_types
4642                    .get(&g.constructor)
4643                    .map(|m| m.keys().cloned().collect())
4644                    .unwrap_or_default();
4645                if let Some(fs) = self.record_field_types.get(&g.constructor) {
4646                    names.extend(fs.iter().map(|(n, _)| n.clone()));
4647                }
4648                Some(names)
4649            }
4650            // Open / non-concrete receivers — never flag.
4651            _ => None,
4652        }
4653    }
4654
4655    /// The built-in (intrinsic) method names for a closed built-in receiver,
4656    /// drawn from the union of the two intrinsic resolution tables (some methods
4657    /// live only in the return-type table — e.g. `display` — and some only in
4658    /// the fn-type table — e.g. `map`/`fold`/`zip`).
4659    fn builtin_method_names(&self, receiver_ty: &Type) -> Vec<String> {
4660        const ALL_BUILTIN_METHODS: &[&str] = &[
4661            // collections / iteration
4662            "len",
4663            "length",
4664            "count",
4665            "byte_len",
4666            "is_empty",
4667            "contains",
4668            "contains_key",
4669            "first",
4670            "last",
4671            "find",
4672            "get",
4673            "index_of",
4674            "push",
4675            "append",
4676            "pop",
4677            "insert",
4678            "remove",
4679            "remove_at",
4680            "concat",
4681            "clear",
4682            "reverse",
4683            "sort",
4684            "dedup",
4685            "flatten",
4686            "take",
4687            "skip",
4688            "slice",
4689            "filter",
4690            "map",
4691            "map_values",
4692            "flat_map",
4693            "fold",
4694            "reduce",
4695            "for_each",
4696            "any",
4697            "all",
4698            "enumerate",
4699            "zip",
4700            "join",
4701            "to_set",
4702            "to_list",
4703            "keys",
4704            "values",
4705            "entries",
4706            "set",
4707            "delete",
4708            "merge",
4709            "add",
4710            "union",
4711            "intersection",
4712            "difference",
4713            "symmetric_difference",
4714            "is_subset",
4715            "is_superset",
4716            "is_disjoint",
4717            // string
4718            "starts_with",
4719            "ends_with",
4720            "regex_match",
4721            "to_upper",
4722            "to_lower",
4723            "trim",
4724            "trim_start",
4725            "trim_end",
4726            "substring",
4727            "replace",
4728            "repeat",
4729            "pad_start",
4730            "pad_end",
4731            "format",
4732            "regex_replace",
4733            "regex_find",
4734            "split",
4735            "chars",
4736            "bytes",
4737            "char_at",
4738            // scalar
4739            "abs",
4740            "min",
4741            "max",
4742            "clamp",
4743            "shift_left",
4744            "shift_right",
4745            "to_float",
4746            "to_int",
4747            "floor",
4748            "ceil",
4749            "round",
4750            "sqrt",
4751            "is_nan",
4752            "is_infinite",
4753            "negate",
4754            "is_alpha",
4755            "is_digit",
4756            "is_whitespace",
4757            "compare",
4758            "hash_code",
4759            "equals",
4760            "to_string",
4761            "display",
4762            // optional / result
4763            "is_some",
4764            "is_none",
4765            "unwrap",
4766            "unwrap_or",
4767            "is_ok",
4768            "is_err",
4769            "map_err",
4770        ];
4771        ALL_BUILTIN_METHODS
4772            .iter()
4773            .filter(|m| self.method_is_resolvable(receiver_ty, m))
4774            .map(|m| (*m).to_string())
4775            .collect()
4776    }
4777
4778    /// Q-checker-unknown-method-concrete: emit `E4013` when `method` does not
4779    /// resolve on a **concrete, closed-method-set** receiver, with a nearest-name
4780    /// suggestion when one exists. A no-op for open / non-concrete receivers
4781    /// (inference vars, §4.9 `Flexible` sketch types, the `Error` sentinel, and
4782    /// user types whose definition is not in scope).
4783    ///
4784    /// `span` should be the method-name span so the diagnostic underlines the
4785    /// offending method.
4786    fn check_unknown_method_on_concrete(&mut self, receiver_ty: &Type, method: &str, span: Span) {
4787        // Conversion methods resolve elsewhere; never flag.
4788        if Self::CONVERSION_METHODS.contains(&method) {
4789            return;
4790        }
4791        // Resolves through some path → fine.
4792        if self.method_is_resolvable(receiver_ty, method) {
4793            return;
4794        }
4795        // Only flag concrete, closed-method-set receivers.
4796        let Some(candidates) = self.concrete_closed_method_names(receiver_ty) else {
4797            return;
4798        };
4799
4800        let receiver_ty = self.subst.apply(receiver_ty);
4801        let recv_desc = describe_receiver_type(&receiver_ty);
4802        let diag = self.diags.error(
4803            E_NO_SUCH_METHOD,
4804            format!("no method `{method}` on `{recv_desc}`"),
4805            span,
4806        );
4807        if let Some(suggestion) = nearest_method_name(method, &candidates) {
4808            diag.note(format!("did you mean `{suggestion}`?"));
4809        }
4810    }
4811
4812    /// Resolve the return type of a method call on a known receiver type.
4813    ///
4814    /// Returns a concrete type when the receiver type and method name
4815    /// identify a well-known built-in method; falls back to a fresh type
4816    /// variable otherwise.
4817    fn resolve_method_return_type(&self, receiver_ty: &Type, method: &str) -> Type {
4818        let receiver_ty = self.subst.apply(receiver_ty);
4819
4820        // Q-bridge (#104): for a primitive receiver, consult the canonical
4821        // trait conformances registered in `impl_table` *before* the intrinsic
4822        // `match`. If a registered conformance's trait declares `method`,
4823        // return that trait method's declared return type (with `Self` mapped
4824        // to the concrete receiver). This makes e.g. `(1).compare(2)` resolve
4825        // to `Ordering` (not the intrinsic `Int` fallback) and `a.eq(b)` to
4826        // `Bool`, uniformly with user types. Non-trait intrinsics (`abs`,
4827        // `to_string`, …) and code that never imports the core trait fall
4828        // through to the intrinsic arms below.
4829        if matches!(receiver_ty, Type::Primitive(_)) {
4830            if let Some(ty) = self.resolve_primitive_canonical_method_return(&receiver_ty, method) {
4831                return ty;
4832            }
4833        }
4834
4835        match &receiver_ty {
4836            Type::Error => Type::Error,
4837            // List[T] methods
4838            Type::Generic(g) if g.constructor == "List" && g.args.len() == 1 => {
4839                let elem_ty = &g.args[0];
4840                match method {
4841                    "len" | "length" | "count" => Type::Primitive(PrimitiveType::Int),
4842                    "first" | "last" | "find" | "get" => Type::Optional(Box::new(elem_ty.clone())),
4843                    "index_of" => Type::Optional(Box::new(Type::Primitive(PrimitiveType::Int))),
4844                    "contains" | "is_empty" | "any" | "all" => Type::Primitive(PrimitiveType::Bool),
4845                    // DQ18 + DQ30: the in-place mutators require a `mut` receiver
4846                    // (enforced in `ownership.rs`, E5004). `push`/`append` (DQ18)
4847                    // and `insert`/`reverse`/`set` (DQ30) return `Void`;
4848                    // `pop` returns `Optional[T]` (`None` on empty — emptiness is
4849                    // a normal state); `remove_at` returns the removed `T`
4850                    // (out-of-bounds aborts at runtime, §10.5 Panic). Functional
4851                    // list-building stays on `+`/`concat`. `remove` is NOT a
4852                    // `List` method (the by-index form is `remove_at`; by-value
4853                    // `remove(value)` is reserved) — it falls through to the
4854                    // unknown-method diagnostic.
4855                    "push" | "append" | "insert" | "reverse" | "set" => {
4856                        Type::Primitive(PrimitiveType::Void)
4857                    }
4858                    "pop" => Type::Optional(Box::new(elem_ty.clone())),
4859                    "remove_at" => elem_ty.clone(),
4860                    "concat" | "sort" | "filter" | "dedup" | "take" | "skip" | "flat_map"
4861                    | "slice" | "flatten" => receiver_ty.clone(),
4862                    "clear" | "for_each" => Type::Primitive(PrimitiveType::Void),
4863                    "join" | "display" => Type::Primitive(PrimitiveType::String),
4864                    "enumerate" => Type::Generic(GenericType {
4865                        constructor: "List".into(),
4866                        args: vec![Type::Tuple(vec![
4867                            Type::Primitive(PrimitiveType::Int),
4868                            elem_ty.clone(),
4869                        ])],
4870                    }),
4871                    "to_set" => Type::Generic(GenericType {
4872                        constructor: "Set".into(),
4873                        args: vec![elem_ty.clone()],
4874                    }),
4875                    _ => self.fresh_var(),
4876                }
4877            }
4878            // Map[K, V] methods
4879            Type::Generic(g) if g.constructor == "Map" && g.args.len() == 2 => {
4880                let key_ty = &g.args[0];
4881                let val_ty = &g.args[1];
4882                match method {
4883                    "len" | "length" | "count" => Type::Primitive(PrimitiveType::Int),
4884                    "contains_key" | "is_empty" => Type::Primitive(PrimitiveType::Bool),
4885                    "get" => Type::Optional(Box::new(val_ty.clone())),
4886                    "set" | "delete" | "merge" | "filter" => receiver_ty.clone(),
4887                    "for_each" => Type::Primitive(PrimitiveType::Void),
4888                    "keys" => Type::Generic(GenericType {
4889                        constructor: "List".into(),
4890                        args: vec![key_ty.clone()],
4891                    }),
4892                    "values" => Type::Generic(GenericType {
4893                        constructor: "List".into(),
4894                        args: vec![val_ty.clone()],
4895                    }),
4896                    "entries" | "to_list" => Type::Generic(GenericType {
4897                        constructor: "List".into(),
4898                        args: vec![Type::Tuple(vec![key_ty.clone(), val_ty.clone()])],
4899                    }),
4900                    _ => self.fresh_var(),
4901                }
4902            }
4903            // String methods
4904            Type::Primitive(PrimitiveType::String) => match method {
4905                "len" | "length" | "count" | "byte_len" => Type::Primitive(PrimitiveType::Int),
4906                "contains" | "starts_with" | "ends_with" | "is_empty" | "regex_match" => {
4907                    Type::Primitive(PrimitiveType::Bool)
4908                }
4909                "to_upper" | "to_lower" | "trim" | "trim_start" | "trim_end" | "reverse"
4910                | "slice" | "substring" | "replace" | "to_string" | "display" | "repeat"
4911                | "pad_start" | "pad_end" | "format" | "regex_replace" | "join" => {
4912                    Type::Primitive(PrimitiveType::String)
4913                }
4914                "split" | "regex_find" => Type::Generic(GenericType {
4915                    constructor: "List".into(),
4916                    args: vec![Type::Primitive(PrimitiveType::String)],
4917                }),
4918                "chars" => Type::Generic(GenericType {
4919                    constructor: "List".into(),
4920                    args: vec![Type::Primitive(PrimitiveType::Char)],
4921                }),
4922                "bytes" => Type::Generic(GenericType {
4923                    constructor: "List".into(),
4924                    args: vec![Type::Primitive(PrimitiveType::Int)],
4925                }),
4926                "index_of" => Type::Optional(Box::new(Type::Primitive(PrimitiveType::Int))),
4927                "char_at" => Type::Optional(Box::new(Type::Primitive(PrimitiveType::Char))),
4928                _ => self.fresh_var(),
4929            },
4930            // Int methods
4931            Type::Primitive(PrimitiveType::Int) => match method {
4932                "abs" | "min" | "max" | "clamp" | "shift_left" | "shift_right" | "compare"
4933                | "hash_code" => Type::Primitive(PrimitiveType::Int),
4934                "to_float" => Type::Primitive(PrimitiveType::Float),
4935                "to_string" | "display" => Type::Primitive(PrimitiveType::String),
4936                "equals" => Type::Primitive(PrimitiveType::Bool),
4937                _ => self.fresh_var(),
4938            },
4939            // Float methods
4940            Type::Primitive(PrimitiveType::Float) => match method {
4941                "abs" | "floor" | "ceil" | "round" | "sqrt" | "min" | "max" | "clamp" => {
4942                    Type::Primitive(PrimitiveType::Float)
4943                }
4944                "to_int" => Type::Primitive(PrimitiveType::Int),
4945                "to_string" | "display" => Type::Primitive(PrimitiveType::String),
4946                "is_nan" | "is_infinite" | "equals" => Type::Primitive(PrimitiveType::Bool),
4947                "compare" | "hash_code" => Type::Primitive(PrimitiveType::Int),
4948                _ => self.fresh_var(),
4949            },
4950            // Bool methods
4951            Type::Primitive(PrimitiveType::Bool) => match method {
4952                "negate" => Type::Primitive(PrimitiveType::Bool),
4953                "to_int" => Type::Primitive(PrimitiveType::Int),
4954                "to_string" | "display" => Type::Primitive(PrimitiveType::String),
4955                "compare" | "hash_code" => Type::Primitive(PrimitiveType::Int),
4956                "equals" => Type::Primitive(PrimitiveType::Bool),
4957                _ => self.fresh_var(),
4958            },
4959            // Char methods
4960            Type::Primitive(PrimitiveType::Char) => match method {
4961                "to_upper" | "to_lower" => Type::Primitive(PrimitiveType::Char),
4962                "is_alpha" | "is_digit" | "is_whitespace" | "equals" => {
4963                    Type::Primitive(PrimitiveType::Bool)
4964                }
4965                "to_int" | "compare" | "hash_code" => Type::Primitive(PrimitiveType::Int),
4966                "to_string" | "display" => Type::Primitive(PrimitiveType::String),
4967                _ => self.fresh_var(),
4968            },
4969            // Set[E] methods
4970            Type::Generic(g) if g.constructor == "Set" && g.args.len() == 1 => {
4971                let elem_ty = &g.args[0];
4972                match method {
4973                    "len" | "length" | "count" => Type::Primitive(PrimitiveType::Int),
4974                    "contains" | "is_empty" | "is_subset" | "is_superset" | "is_disjoint" => {
4975                        Type::Primitive(PrimitiveType::Bool)
4976                    }
4977                    "add"
4978                    | "remove"
4979                    | "union"
4980                    | "intersection"
4981                    | "difference"
4982                    | "symmetric_difference"
4983                    | "filter"
4984                    | "map" => receiver_ty.clone(),
4985                    "for_each" => Type::Primitive(PrimitiveType::Void),
4986                    "to_list" => Type::Generic(GenericType {
4987                        constructor: "List".into(),
4988                        args: vec![elem_ty.clone()],
4989                    }),
4990                    _ => self.fresh_var(),
4991                }
4992            }
4993            // Optional[T] methods
4994            Type::Optional(inner_ty) => match method {
4995                "is_some" | "is_none" => Type::Primitive(PrimitiveType::Bool),
4996                "unwrap" | "unwrap_or" => *inner_ty.clone(),
4997                _ => self.fresh_var(),
4998            },
4999            // Result[T, E] methods
5000            Type::Result(ok_ty, _err_ty) => match method {
5001                "is_ok" | "is_err" => Type::Primitive(PrimitiveType::Bool),
5002                "unwrap" | "unwrap_or" => *ok_ty.clone(),
5003                _ => self.fresh_var(),
5004            },
5005            // User-defined types: look up inherent impl methods.
5006            Type::Named(nt) => {
5007                if let Some(methods) = self.method_types.get(&nt.name) {
5008                    if let Some(Type::Function(f)) = methods.get(method) {
5009                        return self.subst.apply(&f.ret);
5010                    }
5011                }
5012                self.fresh_var()
5013            }
5014            // User-defined generic types: look up inherent impl methods and
5015            // substitute type parameters in the return type.
5016            Type::Generic(g) => {
5017                if let Some(methods) = self.method_types.get(&g.constructor) {
5018                    if let Some(Type::Function(f)) = methods.get(method) {
5019                        let ret_ty = self.subst.apply(&f.ret);
5020                        if let Some(params) = self.record_generic_params.get(&g.constructor) {
5021                            return substitute_type_params(&ret_ty, params, &g.args);
5022                        }
5023                        return ret_ty;
5024                    }
5025                }
5026                self.fresh_var()
5027            }
5028            _ => self.fresh_var(),
5029        }
5030    }
5031
5032    /// Return the full `Function` type for a built-in method accessed as a
5033    /// field (e.g. `items.len` yields `Fn(List[Int]) -> Int`).
5034    ///
5035    /// The AIR lowering desugars `obj.method(args)` into
5036    /// `Call(FieldAccess(obj, method), [obj, ...args])`, so the receiver
5037    /// is always the first parameter in the returned function type.
5038    ///
5039    /// Returns `None` when the method is unknown so the caller can fall
5040    /// back to a fresh type var.
5041    fn resolve_builtin_method_fn_type(&self, receiver_ty: &Type, method: &str) -> Option<Type> {
5042        let receiver_ty = self.subst.apply(receiver_ty);
5043        let mk = |recv: &Type, params: Vec<Type>, ret: Type| -> Option<Type> {
5044            let mut all_params = vec![recv.clone()];
5045            all_params.extend(params);
5046            Some(Type::Function(FnType {
5047                params: all_params,
5048                ret: Box::new(ret),
5049                effects: vec![],
5050            }))
5051        };
5052        match &receiver_ty {
5053            Type::Generic(g) if g.constructor == "List" && g.args.len() == 1 => {
5054                let elem = &g.args[0];
5055                let r = &receiver_ty;
5056                match method {
5057                    "len" | "length" | "count" => {
5058                        mk(r, vec![], Type::Primitive(PrimitiveType::Int))
5059                    }
5060                    "is_empty" => mk(r, vec![], Type::Primitive(PrimitiveType::Bool)),
5061                    "contains" => mk(r, vec![elem.clone()], Type::Primitive(PrimitiveType::Bool)),
5062                    "first" | "last" => mk(r, vec![], Type::Optional(Box::new(elem.clone()))),
5063                    "find" => {
5064                        let cb = Type::Function(FnType {
5065                            params: vec![elem.clone()],
5066                            ret: Box::new(Type::Primitive(PrimitiveType::Bool)),
5067                            effects: vec![],
5068                        });
5069                        mk(r, vec![cb], Type::Optional(Box::new(elem.clone())))
5070                    }
5071                    "get" => mk(
5072                        r,
5073                        vec![Type::Primitive(PrimitiveType::Int)],
5074                        Type::Optional(Box::new(elem.clone())),
5075                    ),
5076                    "index_of" => mk(
5077                        r,
5078                        vec![elem.clone()],
5079                        Type::Optional(Box::new(Type::Primitive(PrimitiveType::Int))),
5080                    ),
5081                    // DQ18: `push`/`append` mutate in place and return `Void`.
5082                    "push" | "append" => {
5083                        mk(r, vec![elem.clone()], Type::Primitive(PrimitiveType::Void))
5084                    }
5085                    // DQ30: the remaining in-place mutators. `pop` returns
5086                    // `Optional[T]` (`None` on empty); `remove_at` returns the
5087                    // removed `T` (OOB aborts, §10.5); `insert` (valid range
5088                    // `0..=len`) / `reverse` / indexed `set` return `Void`.
5089                    // `remove` is intentionally absent: the by-index form is
5090                    // `remove_at`, and by-value `remove(value)` is reserved.
5091                    "pop" => mk(r, vec![], Type::Optional(Box::new(elem.clone()))),
5092                    "remove_at" => mk(r, vec![Type::Primitive(PrimitiveType::Int)], elem.clone()),
5093                    "insert" => mk(
5094                        r,
5095                        vec![Type::Primitive(PrimitiveType::Int), elem.clone()],
5096                        Type::Primitive(PrimitiveType::Void),
5097                    ),
5098                    "reverse" => mk(r, vec![], Type::Primitive(PrimitiveType::Void)),
5099                    "set" => mk(
5100                        r,
5101                        vec![Type::Primitive(PrimitiveType::Int), elem.clone()],
5102                        Type::Primitive(PrimitiveType::Void),
5103                    ),
5104                    "concat" => mk(r, vec![receiver_ty.clone()], receiver_ty.clone()),
5105                    "clear" => mk(r, vec![], Type::Primitive(PrimitiveType::Void)),
5106                    "sort" | "dedup" | "flatten" => mk(r, vec![], receiver_ty.clone()),
5107                    "take" | "skip" => mk(
5108                        r,
5109                        vec![Type::Primitive(PrimitiveType::Int)],
5110                        receiver_ty.clone(),
5111                    ),
5112                    "slice" => mk(
5113                        r,
5114                        vec![
5115                            Type::Primitive(PrimitiveType::Int),
5116                            Type::Primitive(PrimitiveType::Int),
5117                        ],
5118                        receiver_ty.clone(),
5119                    ),
5120                    "filter" => {
5121                        let cb = Type::Function(FnType {
5122                            params: vec![elem.clone()],
5123                            ret: Box::new(Type::Primitive(PrimitiveType::Bool)),
5124                            effects: vec![],
5125                        });
5126                        mk(r, vec![cb], receiver_ty.clone())
5127                    }
5128                    "map" => {
5129                        let u = self.fresh_var();
5130                        let cb = Type::Function(FnType {
5131                            params: vec![elem.clone()],
5132                            ret: Box::new(u.clone()),
5133                            effects: vec![],
5134                        });
5135                        let ret = Type::Generic(GenericType {
5136                            constructor: "List".into(),
5137                            args: vec![u],
5138                        });
5139                        mk(r, vec![cb], ret)
5140                    }
5141                    "flat_map" => {
5142                        let u = self.fresh_var();
5143                        let inner_list = Type::Generic(GenericType {
5144                            constructor: "List".into(),
5145                            args: vec![u.clone()],
5146                        });
5147                        let cb = Type::Function(FnType {
5148                            params: vec![elem.clone()],
5149                            ret: Box::new(inner_list),
5150                            effects: vec![],
5151                        });
5152                        let ret = Type::Generic(GenericType {
5153                            constructor: "List".into(),
5154                            args: vec![u],
5155                        });
5156                        mk(r, vec![cb], ret)
5157                    }
5158                    "fold" => {
5159                        let acc = self.fresh_var();
5160                        let cb = Type::Function(FnType {
5161                            params: vec![acc.clone(), elem.clone()],
5162                            ret: Box::new(acc.clone()),
5163                            effects: vec![],
5164                        });
5165                        mk(r, vec![acc.clone(), cb], acc)
5166                    }
5167                    "reduce" => {
5168                        let cb = Type::Function(FnType {
5169                            params: vec![elem.clone(), elem.clone()],
5170                            ret: Box::new(elem.clone()),
5171                            effects: vec![],
5172                        });
5173                        mk(r, vec![cb], elem.clone())
5174                    }
5175                    "for_each" => {
5176                        let cb = Type::Function(FnType {
5177                            params: vec![elem.clone()],
5178                            ret: Box::new(Type::Primitive(PrimitiveType::Void)),
5179                            effects: vec![],
5180                        });
5181                        mk(r, vec![cb], Type::Primitive(PrimitiveType::Void))
5182                    }
5183                    "any" | "all" => {
5184                        let cb = Type::Function(FnType {
5185                            params: vec![elem.clone()],
5186                            ret: Box::new(Type::Primitive(PrimitiveType::Bool)),
5187                            effects: vec![],
5188                        });
5189                        mk(r, vec![cb], Type::Primitive(PrimitiveType::Bool))
5190                    }
5191                    "enumerate" => {
5192                        let pair =
5193                            Type::Tuple(vec![Type::Primitive(PrimitiveType::Int), elem.clone()]);
5194                        mk(
5195                            r,
5196                            vec![],
5197                            Type::Generic(GenericType {
5198                                constructor: "List".into(),
5199                                args: vec![pair],
5200                            }),
5201                        )
5202                    }
5203                    "zip" => {
5204                        let f = self.fresh_var();
5205                        let other_list = Type::Generic(GenericType {
5206                            constructor: "List".into(),
5207                            args: vec![f.clone()],
5208                        });
5209                        let pair = Type::Tuple(vec![elem.clone(), f]);
5210                        mk(
5211                            r,
5212                            vec![other_list],
5213                            Type::Generic(GenericType {
5214                                constructor: "List".into(),
5215                                args: vec![pair],
5216                            }),
5217                        )
5218                    }
5219                    "join" => mk(
5220                        r,
5221                        vec![Type::Primitive(PrimitiveType::String)],
5222                        Type::Primitive(PrimitiveType::String),
5223                    ),
5224                    "to_set" => mk(
5225                        r,
5226                        vec![],
5227                        Type::Generic(GenericType {
5228                            constructor: "Set".into(),
5229                            args: vec![elem.clone()],
5230                        }),
5231                    ),
5232                    _ => None,
5233                }
5234            }
5235            Type::Generic(g) if g.constructor == "Map" && g.args.len() == 2 => {
5236                let key = &g.args[0];
5237                let val = &g.args[1];
5238                let r = &receiver_ty;
5239                match method {
5240                    "len" | "length" | "count" => {
5241                        mk(r, vec![], Type::Primitive(PrimitiveType::Int))
5242                    }
5243                    "is_empty" => mk(r, vec![], Type::Primitive(PrimitiveType::Bool)),
5244                    "contains_key" => {
5245                        mk(r, vec![key.clone()], Type::Primitive(PrimitiveType::Bool))
5246                    }
5247                    "get" => mk(r, vec![key.clone()], Type::Optional(Box::new(val.clone()))),
5248                    "set" => mk(r, vec![key.clone(), val.clone()], receiver_ty.clone()),
5249                    "delete" => mk(r, vec![key.clone()], receiver_ty.clone()),
5250                    "merge" => mk(r, vec![receiver_ty.clone()], receiver_ty.clone()),
5251                    "keys" => mk(
5252                        r,
5253                        vec![],
5254                        Type::Generic(GenericType {
5255                            constructor: "List".into(),
5256                            args: vec![key.clone()],
5257                        }),
5258                    ),
5259                    "values" => mk(
5260                        r,
5261                        vec![],
5262                        Type::Generic(GenericType {
5263                            constructor: "List".into(),
5264                            args: vec![val.clone()],
5265                        }),
5266                    ),
5267                    "entries" | "to_list" => mk(
5268                        r,
5269                        vec![],
5270                        Type::Generic(GenericType {
5271                            constructor: "List".into(),
5272                            args: vec![Type::Tuple(vec![key.clone(), val.clone()])],
5273                        }),
5274                    ),
5275                    "map_values" => {
5276                        let u = self.fresh_var();
5277                        let cb = Type::Function(FnType {
5278                            params: vec![val.clone()],
5279                            ret: Box::new(u.clone()),
5280                            effects: vec![],
5281                        });
5282                        mk(
5283                            r,
5284                            vec![cb],
5285                            Type::Generic(GenericType {
5286                                constructor: "Map".into(),
5287                                args: vec![key.clone(), u],
5288                            }),
5289                        )
5290                    }
5291                    "filter" => {
5292                        let cb = Type::Function(FnType {
5293                            params: vec![key.clone(), val.clone()],
5294                            ret: Box::new(Type::Primitive(PrimitiveType::Bool)),
5295                            effects: vec![],
5296                        });
5297                        mk(r, vec![cb], receiver_ty.clone())
5298                    }
5299                    "for_each" => {
5300                        let cb = Type::Function(FnType {
5301                            params: vec![key.clone(), val.clone()],
5302                            ret: Box::new(Type::Primitive(PrimitiveType::Void)),
5303                            effects: vec![],
5304                        });
5305                        mk(r, vec![cb], Type::Primitive(PrimitiveType::Void))
5306                    }
5307                    _ => None,
5308                }
5309            }
5310            Type::Generic(g) if g.constructor == "Set" && g.args.len() == 1 => {
5311                let elem = &g.args[0];
5312                let r = &receiver_ty;
5313                match method {
5314                    "len" | "length" | "count" => {
5315                        mk(r, vec![], Type::Primitive(PrimitiveType::Int))
5316                    }
5317                    "is_empty" => mk(r, vec![], Type::Primitive(PrimitiveType::Bool)),
5318                    "contains" => mk(r, vec![elem.clone()], Type::Primitive(PrimitiveType::Bool)),
5319                    "add" | "remove" => mk(r, vec![elem.clone()], receiver_ty.clone()),
5320                    "union" | "intersection" | "difference" | "symmetric_difference" => {
5321                        mk(r, vec![receiver_ty.clone()], receiver_ty.clone())
5322                    }
5323                    "is_subset" | "is_superset" | "is_disjoint" => mk(
5324                        r,
5325                        vec![receiver_ty.clone()],
5326                        Type::Primitive(PrimitiveType::Bool),
5327                    ),
5328                    "filter" => {
5329                        let cb = Type::Function(FnType {
5330                            params: vec![elem.clone()],
5331                            ret: Box::new(Type::Primitive(PrimitiveType::Bool)),
5332                            effects: vec![],
5333                        });
5334                        mk(r, vec![cb], receiver_ty.clone())
5335                    }
5336                    "map" => {
5337                        let cb = Type::Function(FnType {
5338                            params: vec![elem.clone()],
5339                            ret: Box::new(elem.clone()),
5340                            effects: vec![],
5341                        });
5342                        mk(r, vec![cb], receiver_ty.clone())
5343                    }
5344                    "for_each" => {
5345                        let cb = Type::Function(FnType {
5346                            params: vec![elem.clone()],
5347                            ret: Box::new(Type::Primitive(PrimitiveType::Void)),
5348                            effects: vec![],
5349                        });
5350                        mk(r, vec![cb], Type::Primitive(PrimitiveType::Void))
5351                    }
5352                    "to_list" => mk(
5353                        r,
5354                        vec![],
5355                        Type::Generic(GenericType {
5356                            constructor: "List".into(),
5357                            args: vec![elem.clone()],
5358                        }),
5359                    ),
5360                    _ => None,
5361                }
5362            }
5363            Type::Primitive(PrimitiveType::String) => {
5364                let r = &receiver_ty;
5365                let str_ty = Type::Primitive(PrimitiveType::String);
5366                let int_ty = Type::Primitive(PrimitiveType::Int);
5367                match method {
5368                    "len" | "length" | "count" | "byte_len" => mk(r, vec![], int_ty),
5369                    "is_empty" => mk(r, vec![], Type::Primitive(PrimitiveType::Bool)),
5370                    "contains" | "starts_with" | "ends_with" => mk(
5371                        r,
5372                        vec![str_ty.clone()],
5373                        Type::Primitive(PrimitiveType::Bool),
5374                    ),
5375                    "regex_match" => mk(
5376                        r,
5377                        vec![str_ty.clone()],
5378                        Type::Primitive(PrimitiveType::Bool),
5379                    ),
5380                    "to_upper" | "to_lower" | "trim" | "trim_start" | "trim_end" | "reverse"
5381                    | "to_string" | "display" => mk(r, vec![], str_ty),
5382                    "repeat" => mk(r, vec![Type::Primitive(PrimitiveType::Int)], str_ty),
5383                    "slice" | "substring" => mk(
5384                        r,
5385                        vec![
5386                            Type::Primitive(PrimitiveType::Int),
5387                            Type::Primitive(PrimitiveType::Int),
5388                        ],
5389                        str_ty,
5390                    ),
5391                    "replace" | "regex_replace" => {
5392                        mk(r, vec![str_ty.clone(), str_ty.clone()], str_ty)
5393                    }
5394                    "pad_start" | "pad_end" => mk(
5395                        r,
5396                        vec![Type::Primitive(PrimitiveType::Int), str_ty.clone()],
5397                        str_ty,
5398                    ),
5399                    "format" => mk(r, vec![], str_ty),
5400                    "join" => mk(
5401                        r,
5402                        vec![Type::Generic(GenericType {
5403                            constructor: "List".into(),
5404                            args: vec![str_ty.clone()],
5405                        })],
5406                        str_ty,
5407                    ),
5408                    "split" => mk(
5409                        r,
5410                        vec![str_ty],
5411                        Type::Generic(GenericType {
5412                            constructor: "List".into(),
5413                            args: vec![Type::Primitive(PrimitiveType::String)],
5414                        }),
5415                    ),
5416                    "regex_find" => mk(
5417                        r,
5418                        vec![Type::Primitive(PrimitiveType::String)],
5419                        Type::Generic(GenericType {
5420                            constructor: "List".into(),
5421                            args: vec![Type::Primitive(PrimitiveType::String)],
5422                        }),
5423                    ),
5424                    "chars" => mk(
5425                        r,
5426                        vec![],
5427                        Type::Generic(GenericType {
5428                            constructor: "List".into(),
5429                            args: vec![Type::Primitive(PrimitiveType::Char)],
5430                        }),
5431                    ),
5432                    "bytes" => mk(
5433                        r,
5434                        vec![],
5435                        Type::Generic(GenericType {
5436                            constructor: "List".into(),
5437                            args: vec![Type::Primitive(PrimitiveType::Int)],
5438                        }),
5439                    ),
5440                    "index_of" => mk(
5441                        r,
5442                        vec![Type::Primitive(PrimitiveType::String)],
5443                        Type::Optional(Box::new(Type::Primitive(PrimitiveType::Int))),
5444                    ),
5445                    "char_at" => mk(
5446                        r,
5447                        vec![Type::Primitive(PrimitiveType::Int)],
5448                        Type::Optional(Box::new(Type::Primitive(PrimitiveType::Char))),
5449                    ),
5450                    _ => None,
5451                }
5452            }
5453            Type::Primitive(PrimitiveType::Int) => {
5454                let r = &receiver_ty;
5455                let int_ty = Type::Primitive(PrimitiveType::Int);
5456                match method {
5457                    "abs" => mk(r, vec![], int_ty),
5458                    "min" | "max" | "shift_left" | "shift_right" | "compare" => {
5459                        mk(r, vec![int_ty.clone()], int_ty)
5460                    }
5461                    "clamp" => mk(r, vec![int_ty.clone(), int_ty.clone()], int_ty),
5462                    "equals" => mk(
5463                        r,
5464                        vec![Type::Primitive(PrimitiveType::Int)],
5465                        Type::Primitive(PrimitiveType::Bool),
5466                    ),
5467                    "hash_code" => mk(r, vec![], Type::Primitive(PrimitiveType::Int)),
5468                    "to_float" => mk(r, vec![], Type::Primitive(PrimitiveType::Float)),
5469                    "to_string" | "display" => {
5470                        mk(r, vec![], Type::Primitive(PrimitiveType::String))
5471                    }
5472                    _ => None,
5473                }
5474            }
5475            Type::Primitive(PrimitiveType::Float) => {
5476                let r = &receiver_ty;
5477                let float_ty = Type::Primitive(PrimitiveType::Float);
5478                match method {
5479                    "abs" | "floor" | "ceil" | "round" | "sqrt" => mk(r, vec![], float_ty),
5480                    "min" | "max" => mk(r, vec![float_ty.clone()], float_ty),
5481                    "clamp" => mk(r, vec![float_ty.clone(), float_ty.clone()], float_ty),
5482                    "to_int" => mk(r, vec![], Type::Primitive(PrimitiveType::Int)),
5483                    "to_string" | "display" => {
5484                        mk(r, vec![], Type::Primitive(PrimitiveType::String))
5485                    }
5486                    "is_nan" | "is_infinite" | "equals" => {
5487                        mk(r, vec![], Type::Primitive(PrimitiveType::Bool))
5488                    }
5489                    "compare" | "hash_code" => mk(r, vec![], Type::Primitive(PrimitiveType::Int)),
5490                    _ => None,
5491                }
5492            }
5493            Type::Primitive(PrimitiveType::Bool) => {
5494                let r = &receiver_ty;
5495                match method {
5496                    "negate" | "equals" => mk(r, vec![], Type::Primitive(PrimitiveType::Bool)),
5497                    "to_int" | "compare" | "hash_code" => {
5498                        mk(r, vec![], Type::Primitive(PrimitiveType::Int))
5499                    }
5500                    "to_string" | "display" => {
5501                        mk(r, vec![], Type::Primitive(PrimitiveType::String))
5502                    }
5503                    _ => None,
5504                }
5505            }
5506            Type::Primitive(PrimitiveType::Char) => {
5507                let r = &receiver_ty;
5508                match method {
5509                    "to_upper" | "to_lower" => mk(r, vec![], Type::Primitive(PrimitiveType::Char)),
5510                    "is_alpha" | "is_digit" | "is_whitespace" | "equals" => {
5511                        mk(r, vec![], Type::Primitive(PrimitiveType::Bool))
5512                    }
5513                    "to_int" | "compare" | "hash_code" => {
5514                        mk(r, vec![], Type::Primitive(PrimitiveType::Int))
5515                    }
5516                    "to_string" | "display" => {
5517                        mk(r, vec![], Type::Primitive(PrimitiveType::String))
5518                    }
5519                    _ => None,
5520                }
5521            }
5522            // Optional[T] methods
5523            Type::Optional(inner_ty) => {
5524                let r = &receiver_ty;
5525                let inner = *inner_ty.clone();
5526                match method {
5527                    "is_some" | "is_none" => mk(r, vec![], Type::Primitive(PrimitiveType::Bool)),
5528                    "unwrap" => mk(r, vec![], inner),
5529                    "unwrap_or" => mk(r, vec![inner.clone()], inner),
5530                    "map" => {
5531                        let u = self.fresh_var();
5532                        let cb = Type::Function(FnType {
5533                            params: vec![inner],
5534                            ret: Box::new(u.clone()),
5535                            effects: vec![],
5536                        });
5537                        mk(r, vec![cb], Type::Optional(Box::new(u)))
5538                    }
5539                    "flat_map" => {
5540                        let u = self.fresh_var();
5541                        let opt_u = Type::Optional(Box::new(u));
5542                        let cb = Type::Function(FnType {
5543                            params: vec![inner],
5544                            ret: Box::new(opt_u.clone()),
5545                            effects: vec![],
5546                        });
5547                        mk(r, vec![cb], opt_u)
5548                    }
5549                    _ => None,
5550                }
5551            }
5552            // Result[T, E] methods
5553            Type::Result(ok_ty, err_ty) => {
5554                let r = &receiver_ty;
5555                let ok = *ok_ty.clone();
5556                let err = *err_ty.clone();
5557                match method {
5558                    "is_ok" | "is_err" => mk(r, vec![], Type::Primitive(PrimitiveType::Bool)),
5559                    "unwrap" => mk(r, vec![], ok),
5560                    "unwrap_or" => mk(r, vec![ok.clone()], ok),
5561                    "map" => {
5562                        let u = self.fresh_var();
5563                        let cb = Type::Function(FnType {
5564                            params: vec![ok],
5565                            ret: Box::new(u.clone()),
5566                            effects: vec![],
5567                        });
5568                        mk(r, vec![cb], Type::Result(Box::new(u), Box::new(err)))
5569                    }
5570                    "map_err" => {
5571                        let e2 = self.fresh_var();
5572                        let cb = Type::Function(FnType {
5573                            params: vec![err],
5574                            ret: Box::new(e2.clone()),
5575                            effects: vec![],
5576                        });
5577                        mk(r, vec![cb], Type::Result(Box::new(ok), Box::new(e2)))
5578                    }
5579                    _ => None,
5580                }
5581            }
5582            _ => None,
5583        }
5584    }
5585
5586    // ── Generic type-var replacement ──────────────────────────────────────
5587
5588    /// Walk `ty` and replace any [`TypeVarId`] found in `map` with the
5589    /// corresponding fresh type. Used to create per-call-site instantiations
5590    /// of generic function types.
5591    fn replace_type_vars(&self, ty: &Type, map: &HashMap<TypeVarId, Type>) -> Type {
5592        match ty {
5593            Type::TypeVar(id) => map.get(id).cloned().unwrap_or_else(|| ty.clone()),
5594            Type::Function(f) => Type::Function(FnType {
5595                params: f
5596                    .params
5597                    .iter()
5598                    .map(|t| self.replace_type_vars(t, map))
5599                    .collect(),
5600                ret: Box::new(self.replace_type_vars(&f.ret, map)),
5601                effects: f.effects.clone(),
5602            }),
5603            Type::Generic(g) => Type::Generic(GenericType {
5604                constructor: g.constructor.clone(),
5605                args: g
5606                    .args
5607                    .iter()
5608                    .map(|t| self.replace_type_vars(t, map))
5609                    .collect(),
5610            }),
5611            Type::Tuple(elems) => Type::Tuple(
5612                elems
5613                    .iter()
5614                    .map(|t| self.replace_type_vars(t, map))
5615                    .collect(),
5616            ),
5617            Type::Optional(inner) => Type::Optional(Box::new(self.replace_type_vars(inner, map))),
5618            Type::Result(ok, err) => Type::Result(
5619                Box::new(self.replace_type_vars(ok, map)),
5620                Box::new(self.replace_type_vars(err, map)),
5621            ),
5622            _ => ty.clone(),
5623        }
5624    }
5625
5626    // ── Binary / unary op typing ─────────────────────────────────────────────
5627
5628    /// §18.5 operator gating: require a `<`/`>`/`<=`/`>=` operand to be
5629    /// `Comparable`.
5630    ///
5631    /// The gate fires only for a **user** (`Type::Named`) operand whose type is
5632    /// resolved and provably *not* `Comparable` in the current `impl_table`. It
5633    /// is intentionally conservative everywhere else:
5634    ///
5635    /// - **No `impl_table`** (e.g. a unit-test checker, or pre-module setup):
5636    ///   skipped, mirroring the `where`-clause bound check, which cannot prove
5637    ///   non-conformance without the table.
5638    /// - **Inference variables / `Flexible` / `Error`:** skipped — the operand
5639    ///   type is not yet concrete, so a bounded generic param (`T: Comparable`)
5640    ///   reaches `compare` via its where-clause obligation, not this gate.
5641    /// - **Primitives / generics / tuples / functions:** the canonical
5642    ///   conformances registered in `impl_table` decide; a primitive that *is*
5643    ///   `Comparable` (Int, Float, String, Char, sized numerics) passes, and one
5644    ///   that is not (e.g. `Bool`) is rejected here, matching §18.5's matrix.
5645    ///
5646    /// On failure it emits [`E_WHERE_CLAUSE`] — the trait-bound error code —
5647    /// with a message suggesting `impl Comparable`.
5648    fn require_comparable_operand(&mut self, operand: &Type, span: Span) {
5649        let resolved = self.subst.apply(operand);
5650        // Only gate concrete operands; leave inference vars / sketch types /
5651        // poison untouched so bounded generics and error recovery are unharmed.
5652        match &resolved {
5653            Type::TypeVar(_) | Type::Flexible(_) | Type::Error => return,
5654            _ => {}
5655        }
5656        let impl_table = match self.impl_table.as_ref() {
5657            Some(t) => t,
5658            None => return, // no table → cannot prove non-conformance.
5659        };
5660        let trait_ref = TraitRef::new("Comparable");
5661        if resolve_impl(&trait_ref, &resolved, impl_table).is_none() {
5662            let key = crate::traits::type_key(&resolved);
5663            // A primitive that is not `Comparable` (only `Bool`, per the §18.5
5664            // sealed-conformance matrix) cannot be given an `impl` — core trait
5665            // conformances for primitives are sealed — so point at the newtype
5666            // escape hatch instead of suggesting an impossible `impl`.
5667            let suggestion = if matches!(resolved, Type::Primitive(_)) {
5668                format!(
5669                    "`{key}` is not `Comparable` (the `(core trait, primitive)` \
5670                     conformances are sealed); wrap it in a newtype with its own \
5671                     `impl Comparable`"
5672                )
5673            } else {
5674                format!("implement `Comparable` for `{key}`")
5675            };
5676            self.diags.error(
5677                E_WHERE_CLAUSE,
5678                format!(
5679                    "type `{key}` does not implement `Comparable`; the \
5680                     `<`/`>`/`<=`/`>=` operators require it — {suggestion}"
5681                ),
5682                span,
5683            );
5684        }
5685    }
5686
5687    /// True when `operand` resolves to a **user** (`Named` record / class) type
5688    /// that implements `Comparable` in the current `impl_table`.
5689    ///
5690    /// This is the codegen-routing companion of [`require_comparable_operand`]:
5691    /// once the gate has accepted an ordering comparison, this answers whether the
5692    /// operands are a *user* `Comparable` type, so the body pass can stamp the
5693    /// `BinaryOp` node with [`USER_COMPARE_META_KEY`] (the operator must be lowered
5694    /// through `compare`, not the broken native `<`). Primitives — which the
5695    /// canonical `impl_table` also marks `Comparable` — are intentionally excluded:
5696    /// their native ordering operator already works on every target. Inference
5697    /// variables / flexible / poison types and the absence of an `impl_table`
5698    /// likewise return `false` (a bounded generic `T: Comparable` lowers through
5699    /// the trait-bound bridge, not this stamp).
5700    fn is_user_comparable(&self, operand: &Type) -> bool {
5701        let resolved = self.subst.apply(operand);
5702        let Type::Named(_) = &resolved else {
5703            return false;
5704        };
5705        let Some(impl_table) = self.impl_table.as_ref() else {
5706            return false;
5707        };
5708        let trait_ref = TraitRef::new("Comparable");
5709        resolve_impl(&trait_ref, &resolved, impl_table).is_some()
5710    }
5711
5712    // ── DQ29: structural Equatable (§18.5) ───────────────────────────────────
5713
5714    /// DQ29 (§18.5): decide whether `ty` conforms to `Equatable`, returning
5715    /// `None` when it does and the poisoning [`NonEquatableWitness`] when it
5716    /// does not.
5717    ///
5718    /// The decision is **structural and on-demand** (computed at the use site;
5719    /// no conditional trait-table entries):
5720    ///
5721    /// 1. **Explicit impl wins** — any type with a resolvable `impl Equatable`
5722    ///    conforms outright (the structural rules below are the compiler-provided
5723    ///    default, suppressed by the impl).
5724    /// 2. **Primitives** — decided by the canonical sealed conformances in
5725    ///    `impl_table` (all v1 scalars conform; `Void` conforms vacuously).
5726    /// 3. **Records** — conform iff every field type conforms (recursively).
5727    /// 4. **Enums** — conform iff every payload type of every variant conforms.
5728    /// 5. **Compound built-ins** — `List[T]`/`Set[T]`/`Optional[T]` iff `T`;
5729    ///    `Map[K, V]` iff `K` and `V`; `Result[T, E]` iff `T` and `E`; tuples
5730    ///    iff all components.
5731    /// 6. **Generic user types** — instantiate conditionally: the constructor's
5732    ///    declared field/payload types are checked with the instantiation's
5733    ///    type arguments substituted for the symbolic `Named(param)`
5734    ///    placeholders.
5735    /// 7. **Classes** — never conform structurally (data/identity line); only
5736    ///    rule 1 admits them.
5737    /// 8. **`Fn` types** — never conform (the poisoning leaf).
5738    ///
5739    /// Unknowns are **conservatively conforming**: unsolved type vars /
5740    /// flexible (sketch) types / `Error`, and `Named` types whose structure
5741    /// this checker cannot see (imported enums' payloads do not cross the
5742    /// export ABI; imported classes are not distinguishable from records).
5743    /// The gate only rejects what it can *prove* non-Equatable — mirroring
5744    /// [`Self::require_comparable_operand`]'s conservatism.
5745    ///
5746    /// Recursive types terminate co-inductively: a type currently being
5747    /// checked (`in_progress`) is assumed conforming, so `record Tree { kids:
5748    /// List[Tree] }` resolves to whatever its non-recursive leaves decide.
5749    fn structural_equatable_witness(
5750        &self,
5751        ty: &Type,
5752        in_progress: &mut HashSet<String>,
5753        path: &mut Vec<String>,
5754    ) -> Option<NonEquatableWitness> {
5755        let resolved = self.subst.apply(ty);
5756        let witness_here = |path: &[String], class_name: Option<String>| {
5757            Some(NonEquatableWitness {
5758                path: path.to_vec(),
5759                leaf: resolved.clone(),
5760                class_name,
5761            })
5762        };
5763        match &resolved {
5764            // Unknowns: conservatively conforming (cannot prove otherwise).
5765            Type::TypeVar(_) | Type::Flexible(_) | Type::Error => None,
5766            // The poisoning leaf: function types have no equality.
5767            Type::Function(_) => witness_here(path, None),
5768            Type::Primitive(p) => {
5769                // `Void` is a vacuous unit — always equal to itself.
5770                if matches!(p, PrimitiveType::Void) {
5771                    return None;
5772                }
5773                let table = self.impl_table.as_ref()?;
5774                if resolve_impl(&TraitRef::new("Equatable"), &resolved, table).is_some() {
5775                    None
5776                } else {
5777                    witness_here(path, None)
5778                }
5779            }
5780            Type::Named(n) => {
5781                // Explicit impl wins (rule 1) — including impls folded in from
5782                // imported modules.
5783                if let Some(table) = self.impl_table.as_ref() {
5784                    if resolve_impl(&TraitRef::new("Equatable"), &resolved, table).is_some() {
5785                        return None;
5786                    }
5787                }
5788                // Co-inductive assumption for recursive types.
5789                if !in_progress.insert(n.name.clone()) {
5790                    return None;
5791                }
5792                let result = if self.class_names.contains(&n.name) {
5793                    // Rule 7: classes are excluded from the structural default.
5794                    witness_here(path, Some(n.name.clone()))
5795                } else if let Some(variants) = self.enum_variant_payloads.get(&n.name) {
5796                    self.enum_payloads_witness(
5797                        &variants.clone(),
5798                        &HashMap::new(),
5799                        in_progress,
5800                        path,
5801                    )
5802                } else if let Some(fields) = self.record_field_types.get(&n.name) {
5803                    self.record_fields_witness(&fields.clone(), &HashMap::new(), in_progress, path)
5804                } else {
5805                    // Unknown structure (imported enum/class, opaque type):
5806                    // conservatively conforming.
5807                    None
5808                };
5809                in_progress.remove(&n.name);
5810                result
5811            }
5812            Type::Generic(g) => {
5813                match (g.constructor.as_str(), g.args.as_slice()) {
5814                    // Rule 5: compound built-ins compose conditionally.
5815                    ("List" | "Set", [elem]) => {
5816                        path.push("[..]".to_string());
5817                        let w = self.structural_equatable_witness(elem, in_progress, path);
5818                        path.pop();
5819                        w
5820                    }
5821                    ("Map", [key, value]) => {
5822                        path.push("[key]".to_string());
5823                        if let Some(w) = self.structural_equatable_witness(key, in_progress, path) {
5824                            path.pop();
5825                            return Some(w);
5826                        }
5827                        path.pop();
5828                        path.push("[value]".to_string());
5829                        let w = self.structural_equatable_witness(value, in_progress, path);
5830                        path.pop();
5831                        w
5832                    }
5833                    // Rule 6: generic user types instantiate conditionally.
5834                    _ => {
5835                        if let Some(table) = self.impl_table.as_ref() {
5836                            if resolve_impl(&TraitRef::new("Equatable"), &resolved, table).is_some()
5837                            {
5838                                return None;
5839                            }
5840                        }
5841                        let key = crate::traits::type_key(&resolved);
5842                        if !in_progress.insert(key.clone()) {
5843                            return None;
5844                        }
5845                        let subst_map: HashMap<String, Type> = self
5846                            .record_generic_params
5847                            .get(&g.constructor)
5848                            .map(|params| {
5849                                params.iter().cloned().zip(g.args.iter().cloned()).collect()
5850                            })
5851                            .unwrap_or_default();
5852                        let result = if let Some(variants) =
5853                            self.enum_variant_payloads.get(&g.constructor)
5854                        {
5855                            self.enum_payloads_witness(
5856                                &variants.clone(),
5857                                &subst_map,
5858                                in_progress,
5859                                path,
5860                            )
5861                        } else if let Some(fields) = self.record_field_types.get(&g.constructor) {
5862                            if self.class_names.contains(&g.constructor) {
5863                                witness_here(path, Some(g.constructor.clone()))
5864                            } else {
5865                                self.record_fields_witness(
5866                                    &fields.clone(),
5867                                    &subst_map,
5868                                    in_progress,
5869                                    path,
5870                                )
5871                            }
5872                        } else {
5873                            // Unknown constructor: conservatively conforming.
5874                            None
5875                        };
5876                        in_progress.remove(&key);
5877                        result
5878                    }
5879                }
5880            }
5881            Type::Tuple(elems) => {
5882                for (i, elem) in elems.iter().enumerate() {
5883                    path.push(i.to_string());
5884                    if let Some(w) = self.structural_equatable_witness(elem, in_progress, path) {
5885                        path.pop();
5886                        return Some(w);
5887                    }
5888                    path.pop();
5889                }
5890                None
5891            }
5892            Type::Optional(inner) => {
5893                path.push("[..]".to_string());
5894                let w = self.structural_equatable_witness(inner, in_progress, path);
5895                path.pop();
5896                w
5897            }
5898            Type::Result(ok, err) => {
5899                path.push("[ok]".to_string());
5900                if let Some(w) = self.structural_equatable_witness(ok, in_progress, path) {
5901                    path.pop();
5902                    return Some(w);
5903                }
5904                path.pop();
5905                path.push("[err]".to_string());
5906                let w = self.structural_equatable_witness(err, in_progress, path);
5907                path.pop();
5908                w
5909            }
5910            Type::Refined(base, _) => self.structural_equatable_witness(base, in_progress, path),
5911        }
5912    }
5913
5914    /// Probe every field of a record (or class admitted via explicit impl
5915    /// elsewhere) for structural Equatable conformance, substituting
5916    /// `subst_map` for symbolic `Named(param)` placeholders first (rule 6).
5917    fn record_fields_witness(
5918        &self,
5919        fields: &[(String, Type)],
5920        subst_map: &HashMap<String, Type>,
5921        in_progress: &mut HashSet<String>,
5922        path: &mut Vec<String>,
5923    ) -> Option<NonEquatableWitness> {
5924        for (fname, fty) in fields {
5925            let fty = substitute_named_params(fty, subst_map);
5926            path.push(fname.clone());
5927            if let Some(w) = self.structural_equatable_witness(&fty, in_progress, path) {
5928                path.pop();
5929                return Some(w);
5930            }
5931            path.pop();
5932        }
5933        None
5934    }
5935
5936    /// Probe every payload component of every enum variant for structural
5937    /// Equatable conformance (rule 4), substituting `subst_map` for symbolic
5938    /// `Named(param)` placeholders first (rule 6).
5939    fn enum_payloads_witness(
5940        &self,
5941        variants: &[EnumVariantPayloadTypes],
5942        subst_map: &HashMap<String, Type>,
5943        in_progress: &mut HashSet<String>,
5944        path: &mut Vec<String>,
5945    ) -> Option<NonEquatableWitness> {
5946        for (vname, components) in variants {
5947            for (label, cty) in components {
5948                let cty = substitute_named_params(cty, subst_map);
5949                path.push(format!("{vname}.{label}"));
5950                if let Some(w) = self.structural_equatable_witness(&cty, in_progress, path) {
5951                    path.pop();
5952                    return Some(w);
5953                }
5954                path.pop();
5955            }
5956        }
5957        None
5958    }
5959
5960    // ── DQ31: three-state Equatable provenance (§18.5) ───────────────────────
5961
5962    /// DQ31 (§18.5 "Container equality defers to element conformance"): the
5963    /// three-state extension of [`Self::structural_equatable_witness`].
5964    /// Returns the [`EqProvenance`] of `ty` — whether its `Equatable`
5965    /// conformance is the compiler-provided structural default, derives from an
5966    /// explicit `impl Equatable` somewhere in its tree, or is absent entirely.
5967    ///
5968    /// A type is [`EqProvenance::StructuralDefault`] only if it carries no
5969    /// explicit `impl Equatable` AND every field / element / payload type is
5970    /// itself `StructuralDefault`. ANY explicit impl anywhere in the element
5971    /// tree yields [`EqProvenance::CustomImpl`] (the container must take the
5972    /// per-element loop calling that `eq`); any non-Equatable leaf yields
5973    /// [`EqProvenance::NotEquatable`] (the DQ29 poison rule — `==` is rejected).
5974    ///
5975    /// Same recursion shape and co-inductive termination as the witness probe:
5976    /// a type currently being checked (`in_progress`) is assumed
5977    /// `StructuralDefault` (the recursive cycle contributes nothing stronger).
5978    /// Unknowns are conservatively `StructuralDefault`.
5979    pub(crate) fn equatable_provenance(
5980        &self,
5981        ty: &Type,
5982        in_progress: &mut HashSet<String>,
5983    ) -> EqProvenance {
5984        let resolved = self.subst.apply(ty);
5985        match &resolved {
5986            // Unknowns: conservatively the native structural default.
5987            Type::TypeVar(_) | Type::Flexible(_) | Type::Error => EqProvenance::StructuralDefault,
5988            // The poisoning leaf: function types have no equality.
5989            Type::Function(_) => EqProvenance::NotEquatable,
5990            Type::Primitive(p) => {
5991                if matches!(p, PrimitiveType::Void) {
5992                    return EqProvenance::StructuralDefault;
5993                }
5994                match self.impl_table.as_ref() {
5995                    Some(table)
5996                        if resolve_impl(&TraitRef::new("Equatable"), &resolved, table)
5997                            .is_some() =>
5998                    {
5999                        // Primitives conform via the sealed canonical table,
6000                        // not a user impl: the native operator is correct.
6001                        EqProvenance::StructuralDefault
6002                    }
6003                    // No table or not in the sealed set → cannot prove
6004                    // conformance: poison (mirrors the witness predicate).
6005                    _ => EqProvenance::NotEquatable,
6006                }
6007            }
6008            Type::Named(n) => {
6009                // Explicit impl wins → custom equality (rule 1).
6010                if let Some(table) = self.impl_table.as_ref() {
6011                    if resolve_impl(&TraitRef::new("Equatable"), &resolved, table).is_some() {
6012                        return EqProvenance::CustomImpl;
6013                    }
6014                }
6015                if !in_progress.insert(n.name.clone()) {
6016                    return EqProvenance::StructuralDefault;
6017                }
6018                let result = if self.class_names.contains(&n.name) {
6019                    // A class without an explicit impl has no equality.
6020                    EqProvenance::NotEquatable
6021                } else if let Some(variants) = self.enum_variant_payloads.get(&n.name) {
6022                    self.enum_payloads_provenance(&variants.clone(), &HashMap::new(), in_progress)
6023                } else if let Some(fields) = self.record_field_types.get(&n.name) {
6024                    self.record_fields_provenance(&fields.clone(), &HashMap::new(), in_progress)
6025                } else {
6026                    // Opaque imported structure: conservatively structural.
6027                    EqProvenance::StructuralDefault
6028                };
6029                in_progress.remove(&n.name);
6030                result
6031            }
6032            Type::Generic(g) => match (g.constructor.as_str(), g.args.as_slice()) {
6033                ("List" | "Set", [elem]) => self.equatable_provenance(elem, in_progress),
6034                ("Map", [key, value]) => self
6035                    .equatable_provenance(key, in_progress)
6036                    .join(self.equatable_provenance(value, in_progress)),
6037                _ => {
6038                    if let Some(table) = self.impl_table.as_ref() {
6039                        if resolve_impl(&TraitRef::new("Equatable"), &resolved, table).is_some() {
6040                            return EqProvenance::CustomImpl;
6041                        }
6042                    }
6043                    let key = crate::traits::type_key(&resolved);
6044                    if !in_progress.insert(key.clone()) {
6045                        return EqProvenance::StructuralDefault;
6046                    }
6047                    let subst_map: HashMap<String, Type> = self
6048                        .record_generic_params
6049                        .get(&g.constructor)
6050                        .map(|params| params.iter().cloned().zip(g.args.iter().cloned()).collect())
6051                        .unwrap_or_default();
6052                    let result = if let Some(variants) =
6053                        self.enum_variant_payloads.get(&g.constructor)
6054                    {
6055                        self.enum_payloads_provenance(&variants.clone(), &subst_map, in_progress)
6056                    } else if let Some(fields) = self.record_field_types.get(&g.constructor) {
6057                        if self.class_names.contains(&g.constructor) {
6058                            EqProvenance::NotEquatable
6059                        } else {
6060                            self.record_fields_provenance(&fields.clone(), &subst_map, in_progress)
6061                        }
6062                    } else {
6063                        EqProvenance::StructuralDefault
6064                    };
6065                    in_progress.remove(&key);
6066                    result
6067                }
6068            },
6069            Type::Tuple(elems) => elems
6070                .iter()
6071                .fold(EqProvenance::StructuralDefault, |acc, e| {
6072                    acc.join(self.equatable_provenance(e, in_progress))
6073                }),
6074            Type::Optional(inner) => self.equatable_provenance(inner, in_progress),
6075            Type::Result(ok, err) => self
6076                .equatable_provenance(ok, in_progress)
6077                .join(self.equatable_provenance(err, in_progress)),
6078            Type::Refined(base, _) => self.equatable_provenance(base, in_progress),
6079        }
6080    }
6081
6082    /// Combine the [`EqProvenance`] of every record field (rule 6 substitution
6083    /// applied first). The DQ31 analogue of [`Self::record_fields_witness`].
6084    fn record_fields_provenance(
6085        &self,
6086        fields: &[(String, Type)],
6087        subst_map: &HashMap<String, Type>,
6088        in_progress: &mut HashSet<String>,
6089    ) -> EqProvenance {
6090        fields
6091            .iter()
6092            .fold(EqProvenance::StructuralDefault, |acc, (_, fty)| {
6093                let fty = substitute_named_params(fty, subst_map);
6094                acc.join(self.equatable_provenance(&fty, in_progress))
6095            })
6096    }
6097
6098    /// Combine the [`EqProvenance`] of every enum payload component (rule 6
6099    /// substitution applied first). The DQ31 analogue of
6100    /// [`Self::enum_payloads_witness`].
6101    fn enum_payloads_provenance(
6102        &self,
6103        variants: &[EnumVariantPayloadTypes],
6104        subst_map: &HashMap<String, Type>,
6105        in_progress: &mut HashSet<String>,
6106    ) -> EqProvenance {
6107        variants
6108            .iter()
6109            .flat_map(|(_, components)| components.iter())
6110            .fold(EqProvenance::StructuralDefault, |acc, (_, cty)| {
6111                let cty = substitute_named_params(cty, subst_map);
6112                acc.join(self.equatable_provenance(&cty, in_progress))
6113            })
6114    }
6115
6116    /// §18.5 operator gating (DQ29): require an `==`/`!=` operand to be
6117    /// `Equatable`, mirroring [`Self::require_comparable_operand`]'s shape but
6118    /// deciding conformance with the structural predicate
6119    /// ([`Self::structural_equatable_witness`]) instead of an impl lookup
6120    /// alone.
6121    ///
6122    /// Conservative skips match the Comparable gate: no `impl_table`, or an
6123    /// operand that is still an inference variable / flexible / poison, emit
6124    /// nothing (a bounded generic `T: Equatable` reaches `==` via its
6125    /// where-clause obligation, not this gate).
6126    ///
6127    /// On failure it emits [`E_NOT_EQUATABLE`] naming the offending field path
6128    /// and leaf type, with a note suggesting the fix.
6129    fn require_equatable_operand(&mut self, operand: &Type, span: Span) {
6130        let resolved = self.subst.apply(operand);
6131        match &resolved {
6132            Type::TypeVar(_) | Type::Flexible(_) | Type::Error => return,
6133            _ => {}
6134        }
6135        if self.impl_table.is_none() {
6136            return; // no table → cannot prove non-conformance.
6137        }
6138        let mut in_progress = HashSet::new();
6139        let mut path = Vec::new();
6140        if let Some(witness) =
6141            self.structural_equatable_witness(&resolved, &mut in_progress, &mut path)
6142        {
6143            let key = crate::traits::type_key(&resolved);
6144            let (detail, suggestion) = equatable_failure_wording(&key, &witness);
6145            self.diags
6146                .error(
6147                    E_NOT_EQUATABLE,
6148                    format!(
6149                        "type `{resolved}` does not implement `Equatable`; the `==`/`!=` \
6150                         operators require it — {detail}"
6151                    ),
6152                    span,
6153                )
6154                .note(suggestion);
6155        }
6156    }
6157
6158    /// Classify an `==`/`!=` operand for the [`USER_EQ_META_KEY`] codegen
6159    /// stamp. Returns `None` when the native operator is already correct on
6160    /// every target (primitives, unknowns without an `Equatable` bound, and
6161    /// anything the gate rejected).
6162    fn user_eq_kind(&self, operand: &Type) -> Option<&'static str> {
6163        let resolved = self.subst.apply(operand);
6164        match &resolved {
6165            Type::TypeVar(id) => {
6166                // Inside a generic fn body: `a == b` on a bounded param. Only
6167                // an Equatable-implying bound warrants the JS/TS deep-equality
6168                // routing ("generic"); an unbounded var stays native.
6169                let bounds = self.type_var_bounds.get(id)?;
6170                if bounds.iter().any(|b| b == "Equatable" || b == "Comparable") {
6171                    Some("generic")
6172                } else {
6173                    None
6174                }
6175            }
6176            Type::Flexible(_) | Type::Error | Type::Primitive(_) | Type::Function(_) => None,
6177            Type::Named(_) => {
6178                // Explicit impl (rule 1) → route through the user's `eq`.
6179                if let Some(table) = self.impl_table.as_ref() {
6180                    if resolve_impl(&TraitRef::new("Equatable"), &resolved, table).is_some() {
6181                        return Some("impl");
6182                    }
6183                }
6184                // Structural record/enum (a class without an impl is rejected
6185                // by the gate; stamping is moot on an erroring program).
6186                // DQ31: a record/enum whose field/payload tree carries an
6187                // explicit `impl Equatable` somewhere takes the custom-element
6188                // loop so that element's `eq` governs comparison.
6189                self.container_eq_kind(&resolved)
6190            }
6191            Type::Generic(_) | Type::Tuple(_) | Type::Optional(_) | Type::Result(_, _) => {
6192                self.container_eq_kind(&resolved)
6193            }
6194            Type::Refined(base, _) => self.user_eq_kind(base),
6195        }
6196    }
6197
6198    /// DQ31: pick the [`USER_EQ_META_KEY`] lane for a *container* operand (a
6199    /// record/enum/tuple shape, `List`/`Map`/`Set`, or `Optional`/`Result`
6200    /// wrapper) by consulting its element-tree [`EqProvenance`].
6201    ///
6202    /// - element tree all-[`StructuralDefault`](EqProvenance::StructuralDefault)
6203    ///   → the native path is observably correct and idiomatic: `"deep"` when a
6204    ///   collection is involved (Go needs `reflect.DeepEqual`; JS/TS need
6205    ///   `__bockEq`) or `"structural"` otherwise (record/enum/tuple — Go/Python
6206    ///   native field-wise equality, Rust's structural `PartialEq` derive).
6207    /// - element tree contains a [`CustomImpl`](EqProvenance::CustomImpl) → the
6208    ///   `"deep_custom"` lane: the container must take a per-element loop that
6209    ///   calls the custom element's `eq` (and, for `Map`/`Set`, lets it govern
6210    ///   key-matching and membership/dedup) so the type's ONE equality holds
6211    ///   inside the container as outside, byte-identically across targets.
6212    /// - [`NotEquatable`](EqProvenance::NotEquatable) is unreachable here: the
6213    ///   `==` gate already rejected the program, so stamping is moot. It maps to
6214    ///   the same native lane the old code produced (defensive, never observed).
6215    fn container_eq_kind(&self, resolved: &Type) -> Option<&'static str> {
6216        let native = if self.type_needs_deep_eq(resolved, &mut HashSet::new()) {
6217            "deep"
6218        } else {
6219            "structural"
6220        };
6221        match self.equatable_provenance(resolved, &mut HashSet::new()) {
6222            EqProvenance::CustomImpl => Some("deep_custom"),
6223            EqProvenance::StructuralDefault | EqProvenance::NotEquatable => Some(native),
6224        }
6225    }
6226
6227    /// True when equality over `ty` (transitively) involves a collection —
6228    /// `List`/`Map`/`Set`, or an `Optional`/`Result` wrapper — so Go must
6229    /// route `==` through its deep-equality runtime helper (native `==` on
6230    /// slices/maps is a compile error). Walks record fields and enum payloads
6231    /// with the same co-inductive guard as the conformance probe.
6232    fn type_needs_deep_eq(&self, ty: &Type, in_progress: &mut HashSet<String>) -> bool {
6233        let resolved = self.subst.apply(ty);
6234        match &resolved {
6235            Type::Generic(g) => match (g.constructor.as_str(), g.args.as_slice()) {
6236                ("List" | "Set" | "Map", _) => true,
6237                _ => {
6238                    let key = crate::traits::type_key(&resolved);
6239                    if !in_progress.insert(key.clone()) {
6240                        return false;
6241                    }
6242                    let subst_map: HashMap<String, Type> = self
6243                        .record_generic_params
6244                        .get(&g.constructor)
6245                        .map(|params| params.iter().cloned().zip(g.args.iter().cloned()).collect())
6246                        .unwrap_or_default();
6247                    let deep =
6248                        if let Some(variants) = self.enum_variant_payloads.get(&g.constructor) {
6249                            variants.clone().iter().any(|(_, components)| {
6250                                components.iter().any(|(_, cty)| {
6251                                    self.type_needs_deep_eq(
6252                                        &substitute_named_params(cty, &subst_map),
6253                                        in_progress,
6254                                    )
6255                                })
6256                            })
6257                        } else if let Some(fields) = self.record_field_types.get(&g.constructor) {
6258                            fields.clone().iter().any(|(_, fty)| {
6259                                self.type_needs_deep_eq(
6260                                    &substitute_named_params(fty, &subst_map),
6261                                    in_progress,
6262                                )
6263                            })
6264                        } else {
6265                            false
6266                        };
6267                    in_progress.remove(&key);
6268                    deep
6269                }
6270            },
6271            Type::Optional(_) | Type::Result(_, _) => true,
6272            Type::Named(n) => {
6273                if !in_progress.insert(n.name.clone()) {
6274                    return false;
6275                }
6276                let deep = if let Some(variants) = self.enum_variant_payloads.get(&n.name) {
6277                    variants.clone().iter().any(|(_, components)| {
6278                        components
6279                            .iter()
6280                            .any(|(_, cty)| self.type_needs_deep_eq(cty, in_progress))
6281                    })
6282                } else if let Some(fields) = self.record_field_types.get(&n.name) {
6283                    fields
6284                        .clone()
6285                        .iter()
6286                        .any(|(_, fty)| self.type_needs_deep_eq(fty, in_progress))
6287                } else {
6288                    false
6289                };
6290                in_progress.remove(&n.name);
6291                deep
6292            }
6293            Type::Tuple(elems) => elems
6294                .iter()
6295                .any(|e| self.type_needs_deep_eq(e, in_progress)),
6296            Type::Refined(base, _) => self.type_needs_deep_eq(base, in_progress),
6297            _ => false,
6298        }
6299    }
6300
6301    fn infer_binop(&mut self, op: BinOp, lt: &Type, rt: &Type, span: Span) -> Type {
6302        match op {
6303            // Arithmetic: operands and result are numeric.
6304            // Orientation: the left operand establishes the expected type;
6305            // the right operand is the found type.
6306            BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Rem | BinOp::Pow => {
6307                self.unify_or_error(rt, lt, span, "arithmetic operands");
6308                self.subst.apply(lt)
6309            }
6310
6311            // Comparison: operands must unify; result is Bool.
6312            BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge => {
6313                self.unify_or_error(rt, lt, span, "comparison operands");
6314                // §18.5 trait-language integration: the ordering operators
6315                // (`<`, `>`, `<=`, `>=`) require `impl Comparable` for a
6316                // *user* (Named) operand. Primitives are gated by the canonical
6317                // conformances in `impl_table`; generic type variables are gated
6318                // by their `where`-clause bounds. `==`/`!=` gate behind
6319                // `Equatable` the same way (DQ29), with records/enums/compound
6320                // built-ins conforming STRUCTURALLY — see
6321                // `require_equatable_operand`.
6322                //
6323                // `unify_or_error` above already required the operands to
6324                // share a type, so a single gate check on the (post-unify)
6325                // left operand covers both sides without double-reporting.
6326                // Fall back to the right operand only when the left stayed
6327                // an inference variable (e.g. an open var unified *into* a
6328                // concrete right-hand type).
6329                let probe = match self.subst.apply(lt) {
6330                    Type::TypeVar(_) => rt,
6331                    _ => lt,
6332                };
6333                if matches!(op, BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge) {
6334                    self.require_comparable_operand(probe, span);
6335                } else if matches!(op, BinOp::Eq | BinOp::Ne) {
6336                    self.require_equatable_operand(probe, span);
6337                }
6338                Type::Primitive(PrimitiveType::Bool)
6339            }
6340
6341            // Logical: both sides must be Bool; result is Bool
6342            BinOp::And | BinOp::Or => {
6343                let bool_ty = Type::Primitive(PrimitiveType::Bool);
6344                self.unify_or_error(lt, &bool_ty, span, "logical operand");
6345                self.unify_or_error(rt, &bool_ty, span, "logical operand");
6346                bool_ty
6347            }
6348
6349            // Bitwise: operands must unify (typically Int); result same
6350            BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor => {
6351                self.unify_or_error(rt, lt, span, "bitwise operands");
6352                self.subst.apply(lt)
6353            }
6354
6355            // Compose (>>): Fn(A)->B >> Fn(B)->C = Fn(A)->C
6356            BinOp::Compose => self.fresh_var(),
6357
6358            // Type membership (is): result is Bool
6359            BinOp::Is => Type::Primitive(PrimitiveType::Bool),
6360        }
6361    }
6362
6363    fn infer_unop(&mut self, op: UnaryOp, operand_ty: &Type, span: Span) -> Type {
6364        match op {
6365            UnaryOp::Neg => {
6366                // Numeric negation
6367                self.subst.apply(operand_ty)
6368            }
6369            UnaryOp::Not => {
6370                // Logical not: operand must be Bool
6371                let bool_ty = Type::Primitive(PrimitiveType::Bool);
6372                self.unify_or_error(operand_ty, &bool_ty, span, "logical not operand");
6373                bool_ty
6374            }
6375            UnaryOp::BitNot => {
6376                // Bitwise not: integer operand
6377                self.subst.apply(operand_ty)
6378            }
6379        }
6380    }
6381
6382    // ── Literal typing ───────────────────────────────────────────────────────
6383
6384    fn infer_literal(&self, lit: &Literal) -> Type {
6385        match lit {
6386            Literal::Int(s) => {
6387                let (_, suffix) = bock_ast::strip_type_suffix(s);
6388                match suffix {
6389                    Some("i8") => Type::Primitive(PrimitiveType::Int8),
6390                    Some("i16") => Type::Primitive(PrimitiveType::Int16),
6391                    Some("i32") => Type::Primitive(PrimitiveType::Int32),
6392                    Some("i64") => Type::Primitive(PrimitiveType::Int64),
6393                    Some("i128") => Type::Primitive(PrimitiveType::Int128),
6394                    Some("u8") => Type::Primitive(PrimitiveType::UInt8),
6395                    Some("u16") => Type::Primitive(PrimitiveType::UInt16),
6396                    Some("u32") => Type::Primitive(PrimitiveType::UInt32),
6397                    Some("u64") => Type::Primitive(PrimitiveType::UInt64),
6398                    _ => Type::Primitive(PrimitiveType::Int),
6399                }
6400            }
6401            Literal::Float(s) => {
6402                let (_, suffix) = bock_ast::strip_type_suffix(s);
6403                match suffix {
6404                    Some("f32") => Type::Primitive(PrimitiveType::Float32),
6405                    Some("f64") => Type::Primitive(PrimitiveType::Float64),
6406                    _ => Type::Primitive(PrimitiveType::Float),
6407                }
6408            }
6409            Literal::Bool(_) => Type::Primitive(PrimitiveType::Bool),
6410            Literal::Char(_) => Type::Primitive(PrimitiveType::Char),
6411            Literal::String(_) => Type::Primitive(PrimitiveType::String),
6412            Literal::Unit => Type::Primitive(PrimitiveType::Void),
6413        }
6414    }
6415
6416    // ── Pattern binding ──────────────────────────────────────────────────────
6417
6418    /// Bind variables introduced by `pattern` to the appropriate component
6419    /// types of `ty` in the current scope.
6420    fn bind_pattern_type(&mut self, pattern: &mut AIRNode, ty: &Type) {
6421        match &pattern.kind {
6422            NodeKind::WildcardPat | NodeKind::RestPat => {
6423                self.record(pattern, ty.clone());
6424            }
6425            NodeKind::BindPat { name, .. } => {
6426                let name = name.name.clone();
6427                self.env.define(name, ty.clone());
6428                self.record(pattern, ty.clone());
6429            }
6430            NodeKind::LiteralPat { lit } => {
6431                let lit_ty = self.infer_literal(lit);
6432                self.unify_or_error(&lit_ty, ty, pattern.span, "literal pattern");
6433                self.record(pattern, lit_ty);
6434            }
6435            NodeKind::TuplePat { .. } => {
6436                if let NodeKind::TuplePat { elems } = &mut pattern.kind {
6437                    if let Type::Tuple(elem_tys) = ty {
6438                        for (e, et) in elems.iter_mut().zip(elem_tys.iter()) {
6439                            let et = et.clone();
6440                            self.bind_pattern_type(e, &et);
6441                        }
6442                    } else {
6443                        for e in elems.iter_mut() {
6444                            let fv = self.fresh_var();
6445                            self.bind_pattern_type(e, &fv);
6446                        }
6447                    }
6448                }
6449                self.record(pattern, ty.clone());
6450            }
6451            NodeKind::ConstructorPat { .. } => {
6452                // Extract constructor name before mutable borrow.
6453                let ctor_name = if let NodeKind::ConstructorPat { path, .. } = &pattern.kind {
6454                    type_path_to_name(path)
6455                } else {
6456                    String::new()
6457                };
6458                let resolved_ty = self.subst.apply(ty);
6459                if let NodeKind::ConstructorPat { fields, .. } = &mut pattern.kind {
6460                    match (ctor_name.as_str(), &resolved_ty) {
6461                        // Some(x) on Optional[T] — bind x to T
6462                        ("Some", Type::Optional(inner)) if fields.len() == 1 => {
6463                            let inner_ty = self.subst.apply(inner);
6464                            self.bind_pattern_type(&mut fields[0], &inner_ty);
6465                        }
6466                        // Ok(v) on Result[T, E] — bind v to T
6467                        ("Ok", Type::Result(ok, _)) if fields.len() == 1 => {
6468                            let ok_ty = self.subst.apply(ok);
6469                            self.bind_pattern_type(&mut fields[0], &ok_ty);
6470                        }
6471                        // Err(e) on Result[T, E] — bind e to E
6472                        ("Err", Type::Result(_, err)) if fields.len() == 1 => {
6473                            let err_ty = self.subst.apply(err);
6474                            self.bind_pattern_type(&mut fields[0], &err_ty);
6475                        }
6476                        // Fallback: fresh vars for unknown constructors.
6477                        _ => {
6478                            for f in fields.iter_mut() {
6479                                let fv = self.fresh_var();
6480                                self.bind_pattern_type(f, &fv);
6481                            }
6482                        }
6483                    }
6484                }
6485                self.record(pattern, ty.clone());
6486            }
6487            NodeKind::OrPat { .. } => {
6488                if let NodeKind::OrPat { alternatives } = &mut pattern.kind {
6489                    for alt in alternatives.iter_mut() {
6490                        let t = ty.clone();
6491                        self.bind_pattern_type(alt, &t);
6492                    }
6493                }
6494                self.record(pattern, ty.clone());
6495            }
6496            NodeKind::ListPat { .. } => {
6497                let elem_ty = match ty {
6498                    Type::Generic(g) if g.constructor == "List" && g.args.len() == 1 => {
6499                        g.args[0].clone()
6500                    }
6501                    _ => self.fresh_var(),
6502                };
6503                if let NodeKind::ListPat { elems, rest } = &mut pattern.kind {
6504                    for e in elems.iter_mut() {
6505                        let et = elem_ty.clone();
6506                        self.bind_pattern_type(e, &et);
6507                    }
6508                    if let Some(r) = rest {
6509                        let list_ty = Type::Generic(GenericType {
6510                            constructor: "List".into(),
6511                            args: vec![elem_ty],
6512                        });
6513                        self.bind_pattern_type(r, &list_ty);
6514                    }
6515                }
6516                self.record(pattern, ty.clone());
6517            }
6518            NodeKind::RecordPat { .. } => {
6519                if let NodeKind::RecordPat { fields, .. } = &mut pattern.kind {
6520                    for f in fields.iter_mut() {
6521                        let fv = self.fresh_var();
6522                        if let Some(sub_pat) = &mut f.pattern {
6523                            // Rename form: `{ x: px }` — bind the sub-pattern.
6524                            self.bind_pattern_type(sub_pat, &fv);
6525                        } else {
6526                            // Shorthand: `{ field }` — bind field name as a variable.
6527                            self.env.define(f.name.name.clone(), fv);
6528                        }
6529                    }
6530                }
6531                self.record(pattern, ty.clone());
6532            }
6533            _ => {
6534                self.record(pattern, ty.clone());
6535            }
6536        }
6537    }
6538
6539    // ── Type-expression node conversion ──────────────────────────────────────
6540
6541    /// Convert an AIR type-expression node into a [`Type`], substituting generic
6542    /// parameter names from `gp_map`.
6543    fn air_type_node_to_type(&mut self, node: &AIRNode, gp_map: &HashMap<String, Type>) -> Type {
6544        match &node.kind {
6545            NodeKind::TypeNamed { path, args } => {
6546                let name = type_path_to_name(path);
6547                // Check if it's a known generic param
6548                if let Some(ty) = gp_map.get(&name) {
6549                    return ty.clone();
6550                }
6551                // Check for built-in primitives
6552                if let Some(prim) = name_to_primitive(&name) {
6553                    return Type::Primitive(prim);
6554                }
6555                // Generic application or named type
6556                if args.is_empty() {
6557                    // Resolve type aliases to their underlying type
6558                    if let Some(underlying) = self.type_aliases.get(&name) {
6559                        return underlying.clone();
6560                    }
6561                    Type::Named(crate::NamedType { name })
6562                } else {
6563                    let converted_args: Vec<Type> = args
6564                        .iter()
6565                        .map(|a| self.air_type_node_to_type(a, gp_map))
6566                        .collect();
6567                    // Special-case Result[T, E] and Optional[T] so that
6568                    // annotations produce the same Type variant as
6569                    // Ok(v)/Some(v) constructors.
6570                    match (name.as_str(), converted_args.len()) {
6571                        ("Result", 2) => Type::Result(
6572                            Box::new(converted_args[0].clone()),
6573                            Box::new(converted_args[1].clone()),
6574                        ),
6575                        ("Optional", 1) => Type::Optional(Box::new(converted_args[0].clone())),
6576                        _ => Type::Generic(GenericType {
6577                            constructor: name,
6578                            args: converted_args,
6579                        }),
6580                    }
6581                }
6582            }
6583            NodeKind::TypeTuple { elems } => {
6584                let elem_tys: Vec<Type> = elems
6585                    .iter()
6586                    .map(|e| self.air_type_node_to_type(e, gp_map))
6587                    .collect();
6588                Type::Tuple(elem_tys)
6589            }
6590            NodeKind::TypeFunction { params, ret, .. } => {
6591                let param_tys: Vec<Type> = params
6592                    .iter()
6593                    .map(|p| self.air_type_node_to_type(p, gp_map))
6594                    .collect();
6595                let ret_ty = self.air_type_node_to_type(ret, gp_map);
6596                Type::Function(FnType {
6597                    params: param_tys,
6598                    ret: Box::new(ret_ty),
6599                    effects: vec![],
6600                })
6601            }
6602            NodeKind::TypeOptional { inner } => {
6603                Type::Optional(Box::new(self.air_type_node_to_type(inner, gp_map)))
6604            }
6605            NodeKind::TypeSelf => {
6606                // Inside an impl/class method body the context maps `Self` to
6607                // the concrete target (see `build_impl_context`); honor it so a
6608                // `-> Self` return or `other: Self` param resolves to the target
6609                // type. Outside that context (e.g. trait declarations) `Self`
6610                // stays an abstract `Named("Self")` placeholder.
6611                if let Some(ty) = gp_map.get("Self") {
6612                    ty.clone()
6613                } else {
6614                    Type::Named(crate::NamedType {
6615                        name: "Self".into(),
6616                    })
6617                }
6618            }
6619            NodeKind::Param { ty, .. } => {
6620                if let Some(ty_node) = ty {
6621                    self.air_type_node_to_type(ty_node, gp_map)
6622                } else {
6623                    self.fresh_var()
6624                }
6625            }
6626            _ => self.fresh_var(),
6627        }
6628    }
6629
6630    /// Convert an AST [`TypeExpr`] directly to a [`Type`].
6631    ///
6632    /// Used for record field type declarations where the type is stored as
6633    /// an AST `TypeExpr` rather than a lowered AIR node.
6634    fn type_expr_to_type(&self, ty: &TypeExpr, gp_map: &HashMap<String, Type>) -> Type {
6635        match ty {
6636            TypeExpr::Named { path, args, .. } => {
6637                let name = type_path_to_name(path);
6638                if let Some(t) = gp_map.get(&name) {
6639                    return t.clone();
6640                }
6641                if let Some(prim) = name_to_primitive(&name) {
6642                    return Type::Primitive(prim);
6643                }
6644                if args.is_empty() {
6645                    // Resolve type aliases to their underlying type
6646                    if let Some(underlying) = self.type_aliases.get(&name) {
6647                        return underlying.clone();
6648                    }
6649                    Type::Named(crate::NamedType { name })
6650                } else {
6651                    let converted_args: Vec<Type> = args
6652                        .iter()
6653                        .map(|a| self.type_expr_to_type(a, gp_map))
6654                        .collect();
6655                    match (name.as_str(), converted_args.len()) {
6656                        ("Result", 2) => Type::Result(
6657                            Box::new(converted_args[0].clone()),
6658                            Box::new(converted_args[1].clone()),
6659                        ),
6660                        ("Optional", 1) => Type::Optional(Box::new(converted_args[0].clone())),
6661                        _ => Type::Generic(GenericType {
6662                            constructor: name,
6663                            args: converted_args,
6664                        }),
6665                    }
6666                }
6667            }
6668            TypeExpr::Tuple { elems, .. } => Type::Tuple(
6669                elems
6670                    .iter()
6671                    .map(|e| self.type_expr_to_type(e, gp_map))
6672                    .collect(),
6673            ),
6674            TypeExpr::Function { params, ret, .. } => {
6675                let param_tys: Vec<Type> = params
6676                    .iter()
6677                    .map(|p| self.type_expr_to_type(p, gp_map))
6678                    .collect();
6679                let ret_ty = self.type_expr_to_type(ret, gp_map);
6680                Type::Function(FnType {
6681                    params: param_tys,
6682                    ret: Box::new(ret_ty),
6683                    effects: vec![],
6684                })
6685            }
6686            TypeExpr::Optional { inner, .. } => {
6687                Type::Optional(Box::new(self.type_expr_to_type(inner, gp_map)))
6688            }
6689            TypeExpr::SelfType { .. } => Type::Named(crate::NamedType {
6690                name: "Self".into(),
6691            }),
6692        }
6693    }
6694
6695    // ── Public API ───────────────────────────────────────────────────────────
6696
6697    /// **Synthesis** (public): query the side-table for the type of an expression
6698    /// node, or re-infer it on a temporary clone if not yet visited.
6699    ///
6700    /// Callers that have already called `check_module` should use `type_of`
6701    /// to avoid re-inference overhead.
6702    pub fn infer_expr(&mut self, expr: &AIRNode) -> Type {
6703        if let Some(ty) = self.types.get(&expr.id) {
6704            return ty.clone();
6705        }
6706        // Infer on a temporary clone so we don't need `&mut AIRNode`.
6707        let mut cloned = expr.clone();
6708        self.infer_node(&mut cloned)
6709    }
6710
6711    /// **Checking** (public): verify that `expr` has the given `expected` type.
6712    ///
6713    /// Emits a diagnostic if the types do not unify. Like `infer_expr`, this
6714    /// operates on a clone when the node has not yet been visited.
6715    pub fn check_expr(&mut self, expr: &AIRNode, expected: &Type) {
6716        if let Some(ty) = self.types.get(&expr.id) {
6717            let ty = ty.clone();
6718            self.unify_or_error(&ty, expected, expr.span, "expression");
6719            return;
6720        }
6721        let mut cloned = expr.clone();
6722        self.check_node(&mut cloned, expected);
6723    }
6724}
6725
6726impl Default for TypeChecker {
6727    fn default() -> Self {
6728        Self::new()
6729    }
6730}
6731
6732// ─── DQ29 structural-Equatable support types ─────────────────────────────────
6733
6734/// DQ31 (§18.5 "Container equality defers to element conformance"): the
6735/// *provenance* of a type's `Equatable` conformance — the three-state answer
6736/// the structural predicate yields once recursion is taken into account.
6737///
6738/// A type has ONE equality, the same inside a container as outside. Whether a
6739/// container (`List`/`Map`/`Set`/`Optional`/`Result`/tuple) compares with the
6740/// target's NATIVE structural operator or with a PER-ELEMENT loop calling the
6741/// element's `eq` is decided by walking the element tree:
6742///
6743/// - [`EqProvenance::StructuralDefault`] — the type has no explicit
6744///   `impl Equatable` ANYWHERE in its tree; every field / element / payload is
6745///   itself `StructuralDefault`. The compiler-provided field-wise default is
6746///   the type's equality, so a container of such elements keeps the native,
6747///   idiomatic path (Rust `==`, Go `reflect.DeepEqual`, native structural
6748///   JS/TS/Python). Observably identical to the loop path.
6749/// - [`EqProvenance::CustomImpl`] — the type carries an explicit
6750///   `impl Equatable` (its `eq` IS the type's equality), OR some element /
6751///   field / payload in its tree does. A container whose element tree contains
6752///   ANY `CustomImpl` must take the loop path so the element's `eq` governs
6753///   element comparison (and, for `Map`/`Set`, key-matching and
6754///   membership/dedup) — otherwise the target's native structural equality
6755///   would silently ignore the custom `eq`, diverging per target (the corner
6756///   #347 left un-pinned).
6757/// - [`EqProvenance::NotEquatable`] — some leaf has no equality at all (an `Fn`
6758///   field, a class without an impl). `==` is rejected (the DQ29 poison rule,
6759///   unchanged).
6760///
6761/// Unknowns (unsolved type vars, flexible/sketch types, `Error`, opaque
6762/// imported structure) are conservatively `StructuralDefault`, mirroring the
6763/// witness predicate's conservatism: the gate rejects only what it can prove
6764/// non-Equatable, and the native path is the safe default for an unknown.
6765#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6766pub enum EqProvenance {
6767    /// No explicit `impl Equatable` anywhere in the type's tree — the
6768    /// compiler-provided structural default is the type's equality.
6769    StructuralDefault,
6770    /// An explicit `impl Equatable` on the type itself or on some element /
6771    /// field / payload in its tree — the loop path must call element `eq`.
6772    CustomImpl,
6773    /// Some leaf has no equality at all — `==` is rejected (DQ29 poison rule).
6774    NotEquatable,
6775}
6776
6777impl EqProvenance {
6778    /// Combine two sub-results in the recursive walk, taking the "strongest"
6779    /// answer: `NotEquatable` poisons (rule unchanged), then `CustomImpl`
6780    /// propagates (any custom impl in the tree forces the loop path), and
6781    /// `StructuralDefault` is the identity. Mirrors a lattice join with
6782    /// `NotEquatable > CustomImpl > StructuralDefault`.
6783    #[must_use]
6784    fn join(self, other: EqProvenance) -> EqProvenance {
6785        use EqProvenance::{CustomImpl, NotEquatable, StructuralDefault};
6786        match (self, other) {
6787            (NotEquatable, _) | (_, NotEquatable) => NotEquatable,
6788            (CustomImpl, _) | (_, CustomImpl) => CustomImpl,
6789            (StructuralDefault, StructuralDefault) => StructuralDefault,
6790        }
6791    }
6792}
6793
6794/// The witness a failed structural-Equatable probe returns: which leaf
6795/// poisoned the conformance, and where it sits.
6796///
6797/// `path` is the chain of steps from the probed type to the offending leaf:
6798/// record/class field names, `Variant._0`-style enum payload components,
6799/// `0`-style tuple indices, and `[..]` / `[key]` / `[value]` / `[ok]` /
6800/// `[err]` markers for collection/wrapper elements. Empty when the probed
6801/// type itself is the offending leaf. `leaf` is the non-Equatable type found
6802/// there. `class_name` is `Some` when the failure is a class without an
6803/// explicit `impl Equatable` (rule 7 — classes are excluded from the
6804/// structural default), which gets its own diagnostic wording.
6805struct NonEquatableWitness {
6806    path: Vec<String>,
6807    leaf: Type,
6808    class_name: Option<String>,
6809}
6810
6811/// Replace symbolic `Named(param)` placeholders in `ty` with the concrete
6812/// types `subst_map` assigns them — the use-site instantiation step for
6813/// generic records/enums (DQ29 rule 6). Types without placeholders pass
6814/// through unchanged; an empty map is the identity.
6815fn substitute_named_params(ty: &Type, subst_map: &HashMap<String, Type>) -> Type {
6816    if subst_map.is_empty() {
6817        return ty.clone();
6818    }
6819    match ty {
6820        Type::Named(n) => subst_map
6821            .get(&n.name)
6822            .cloned()
6823            .unwrap_or_else(|| ty.clone()),
6824        Type::Generic(g) => Type::Generic(GenericType {
6825            constructor: g.constructor.clone(),
6826            args: g
6827                .args
6828                .iter()
6829                .map(|a| substitute_named_params(a, subst_map))
6830                .collect(),
6831        }),
6832        Type::Tuple(elems) => Type::Tuple(
6833            elems
6834                .iter()
6835                .map(|e| substitute_named_params(e, subst_map))
6836                .collect(),
6837        ),
6838        Type::Function(f) => Type::Function(FnType {
6839            params: f
6840                .params
6841                .iter()
6842                .map(|p| substitute_named_params(p, subst_map))
6843                .collect(),
6844            ret: Box::new(substitute_named_params(&f.ret, subst_map)),
6845            effects: f.effects.clone(),
6846        }),
6847        Type::Optional(inner) => {
6848            Type::Optional(Box::new(substitute_named_params(inner, subst_map)))
6849        }
6850        Type::Result(ok, err) => Type::Result(
6851            Box::new(substitute_named_params(ok, subst_map)),
6852            Box::new(substitute_named_params(err, subst_map)),
6853        ),
6854        Type::Refined(base, pred) => Type::Refined(
6855            Box::new(substitute_named_params(base, subst_map)),
6856            pred.clone(),
6857        ),
6858        _ => ty.clone(),
6859    }
6860}
6861
6862/// Word a structural-Equatable failure for the [`E_NOT_EQUATABLE`] diagnostic:
6863/// returns `(detail, suggestion)` where `detail` finishes the "… requires it —"
6864/// sentence and `suggestion` is the trailing fix note.
6865///
6866/// `key` is the probed type's [`crate::traits::type_key`] rendering (used in
6867/// the suggestion); the witness decides the wording:
6868/// - class without impl → names the class and the exclusion rule;
6869/// - poisoned field/payload → names the field path and the leaf type
6870///   (machine-actionable per the diagnostics-review criterion);
6871/// - the probed type itself the leaf → names its kind (function type /
6872///   sealed primitive).
6873fn equatable_failure_wording(key: &str, witness: &NonEquatableWitness) -> (String, String) {
6874    if let Some(class_name) = &witness.class_name {
6875        let detail = if witness.path.is_empty() {
6876            format!(
6877                "`{class_name}` is a class, and classes are excluded from structural \
6878                 equality (data/identity line)"
6879            )
6880        } else {
6881            format!(
6882                "field `{}` is the class `{class_name}`, and classes are excluded from \
6883                 structural equality (data/identity line)",
6884                witness.path.join(".")
6885            )
6886        };
6887        return (
6888            detail,
6889            format!("implement `Equatable` for `{class_name}` or remove the comparison"),
6890        );
6891    }
6892    if witness.path.is_empty() {
6893        let detail = match &witness.leaf {
6894            Type::Function(_) => "function types have no equality".to_string(),
6895            Type::Primitive(_) => format!(
6896                "`{key}` has no canonical equality (the `(core trait, primitive)` \
6897                 conformances are sealed)"
6898            ),
6899            other => format!("`{other}` is not Equatable"),
6900        };
6901        let suggestion = match &witness.leaf {
6902            Type::Primitive(_) => format!(
6903                "wrap `{key}` in a newtype with its own `impl Equatable`, or remove the \
6904                 comparison"
6905            ),
6906            _ => "remove the comparison".to_string(),
6907        };
6908        return (detail, suggestion);
6909    }
6910    (
6911        format!(
6912            "field `{}` of type `{}` is not Equatable",
6913            witness.path.join("."),
6914            witness.leaf
6915        ),
6916        format!("implement `Equatable` for `{key}` or remove the comparison"),
6917    )
6918}
6919
6920// ─── NodeKind helpers ─────────────────────────────────────────────────────────
6921
6922/// Extension methods on [`NodeKind`] used internally by the type checker.
6923trait NodeKindExt {
6924    /// If this is a `Param` node, return its type annotation sub-node.
6925    fn param_ty_node(&self) -> &AIRNode;
6926    /// If this is a `Param` node, extract the bound variable name (if any).
6927    fn param_pat_name(&self) -> Option<String>;
6928}
6929
6930impl NodeKindExt for NodeKind {
6931    fn param_ty_node(&self) -> &AIRNode {
6932        // We can't return a reference to a locally-created node, so we use the
6933        // pattern node as a best-effort fallback; callers handle `None` ty specially.
6934        // This method is only called when we have a reference to the param's kind,
6935        // and the Param node has a `ty: Option<Box<AIRNode>>` field.
6936        // Since we need to return a reference, the only case we can handle is
6937        // when ty is Some(_); callers should use `param_ty_type` instead for
6938        // the type value. This method is here for structural reasons and returns
6939        // the pattern node as a fallback (type will be fresh var in that case).
6940        match self {
6941            NodeKind::Param { ty, pattern, .. } => ty.as_deref().unwrap_or(pattern),
6942            // SAFETY: callers only invoke this on Param nodes
6943            _ => unreachable!("param_ty_node called on non-Param node"),
6944        }
6945    }
6946
6947    fn param_pat_name(&self) -> Option<String> {
6948        match self {
6949            NodeKind::Param { pattern, .. } => match &pattern.kind {
6950                NodeKind::BindPat { name, .. } => Some(name.name.clone()),
6951                NodeKind::WildcardPat => None,
6952                _ => None,
6953            },
6954            _ => None,
6955        }
6956    }
6957}
6958
6959// ─── Generic parameter substitution ──────────────────────────────────────────
6960
6961/// Collect unique [`TypeVarId`]s from a function type in order of first
6962/// appearance. Used by [`TypeChecker::seed_imported_generic_fn`] to discover
6963/// which type variables represent generic parameters.
6964fn collect_type_var_ids_fn(fn_ty: &FnType, out: &mut Vec<TypeVarId>) {
6965    for param in &fn_ty.params {
6966        collect_type_var_ids(param, out);
6967    }
6968    collect_type_var_ids(&fn_ty.ret, out);
6969}
6970
6971/// Recursively collect unique [`TypeVarId`]s from a type.
6972fn collect_type_var_ids(ty: &Type, out: &mut Vec<TypeVarId>) {
6973    match ty {
6974        Type::TypeVar(id) if !out.contains(id) => {
6975            out.push(*id);
6976        }
6977        Type::Function(f) => {
6978            for p in &f.params {
6979                collect_type_var_ids(p, out);
6980            }
6981            collect_type_var_ids(&f.ret, out);
6982        }
6983        Type::Generic(g) => {
6984            for a in &g.args {
6985                collect_type_var_ids(a, out);
6986            }
6987        }
6988        Type::Tuple(elems) => {
6989            for e in elems {
6990                collect_type_var_ids(e, out);
6991            }
6992        }
6993        Type::Optional(inner) => collect_type_var_ids(inner, out),
6994        Type::Result(ok, err) => {
6995            collect_type_var_ids(ok, out);
6996            collect_type_var_ids(err, out);
6997        }
6998        _ => {}
6999    }
7000}
7001
7002/// Replace `Named("A")`, `Named("B")`, etc. in `ty` with the corresponding
7003/// type from `args`, based on the positional mapping in `param_names`.
7004///
7005/// This is used when a record declared as `record Foo[A, B] { ... }` stores
7006/// field types containing `Named("A")` / `Named("B")`. At construction sites
7007/// and field accesses, these placeholders are replaced with the actual type
7008/// arguments inferred or provided.
7009fn substitute_type_params(ty: &Type, param_names: &[String], args: &[Type]) -> Type {
7010    match ty {
7011        Type::Named(nt) => {
7012            if let Some(idx) = param_names.iter().position(|n| n == &nt.name) {
7013                if idx < args.len() {
7014                    return args[idx].clone();
7015                }
7016            }
7017            ty.clone()
7018        }
7019        Type::Generic(g) => Type::Generic(GenericType {
7020            constructor: g.constructor.clone(),
7021            args: g
7022                .args
7023                .iter()
7024                .map(|a| substitute_type_params(a, param_names, args))
7025                .collect(),
7026        }),
7027        Type::Optional(inner) => {
7028            Type::Optional(Box::new(substitute_type_params(inner, param_names, args)))
7029        }
7030        Type::Result(ok, err) => Type::Result(
7031            Box::new(substitute_type_params(ok, param_names, args)),
7032            Box::new(substitute_type_params(err, param_names, args)),
7033        ),
7034        Type::Tuple(elems) => Type::Tuple(
7035            elems
7036                .iter()
7037                .map(|e| substitute_type_params(e, param_names, args))
7038                .collect(),
7039        ),
7040        Type::Function(f) => Type::Function(FnType {
7041            params: f
7042                .params
7043                .iter()
7044                .map(|p| substitute_type_params(p, param_names, args))
7045                .collect(),
7046            ret: Box::new(substitute_type_params(&f.ret, param_names, args)),
7047            effects: f.effects.clone(),
7048        }),
7049        _ => ty.clone(),
7050    }
7051}
7052
7053// ─── Type name helpers ────────────────────────────────────────────────────────
7054
7055/// Convert a `TypePath` to a dot-joined string.
7056fn type_path_to_name(path: &TypePath) -> String {
7057    path.segments
7058        .iter()
7059        .map(|s| s.name.as_str())
7060        .collect::<Vec<_>>()
7061        .join(".")
7062}
7063
7064/// Q-checker-unknown-method-concrete: a short, user-facing description of a
7065/// receiver type for the "no method `m` on `<type>`" diagnostic. Reuses
7066/// [`crate::traits::type_key`]'s human-readable encoding (e.g. `List[Int]`,
7067/// `Map[String, Int]`, `String`, `Point`).
7068fn describe_receiver_type(ty: &Type) -> String {
7069    crate::traits::type_key(ty)
7070}
7071
7072/// Q-checker-unknown-method-concrete: the nearest candidate method name to
7073/// `target` by Levenshtein edit distance, used for the "did you mean `…`?"
7074/// suggestion. Returns `None` when no candidate is close enough (distance must
7075/// be at most a third of the longer name's length, and at most 3), so an
7076/// unrelated typo does not produce a misleading suggestion.
7077fn nearest_method_name(target: &str, candidates: &[String]) -> Option<String> {
7078    let mut best: Option<(usize, &String)> = None;
7079    for cand in candidates {
7080        if cand == target {
7081            continue;
7082        }
7083        let dist = levenshtein(target, cand);
7084        if best.is_none_or(|(d, _)| dist < d) {
7085            best = Some((dist, cand));
7086        }
7087    }
7088    let (dist, cand) = best?;
7089    let threshold = (target.len().max(cand.len()) / 3).clamp(1, 3);
7090    if dist <= threshold {
7091        Some(cand.clone())
7092    } else {
7093        None
7094    }
7095}
7096
7097/// Levenshtein edit distance between two ASCII-ish identifier strings. Used by
7098/// [`nearest_method_name`] for the unknown-method suggestion.
7099fn levenshtein(a: &str, b: &str) -> usize {
7100    let a: Vec<char> = a.chars().collect();
7101    let b: Vec<char> = b.chars().collect();
7102    let mut prev: Vec<usize> = (0..=b.len()).collect();
7103    let mut curr: Vec<usize> = vec![0; b.len() + 1];
7104    for (i, &ca) in a.iter().enumerate() {
7105        curr[0] = i + 1;
7106        for (j, &cb) in b.iter().enumerate() {
7107            let cost = usize::from(ca != cb);
7108            curr[j + 1] = (prev[j] + cost).min(prev[j + 1] + 1).min(curr[j] + 1);
7109        }
7110        std::mem::swap(&mut prev, &mut curr);
7111    }
7112    prev[b.len()]
7113}
7114
7115/// Map a built-in type name to its [`PrimitiveType`] variant, if any.
7116/// Q-prim-assoc: `true` when `node` is a `Call` the lowerer classified as an
7117/// **associated-function call** (`Type.method(args)` — no `self` prepended), via
7118/// the [`bock_air::lower::ASSOC_CALL_META_KEY`] stamp. The checker-side mirror of
7119/// `bock_codegen`'s `is_associated_call` (codegen is downstream, so the helper
7120/// cannot be shared); used to recognise the primitive `Prim.from`/`Prim.try_from`
7121/// conversion call shape.
7122fn is_associated_call_node(node: &AIRNode) -> bool {
7123    matches!(
7124        node.metadata.get(bock_air::lower::ASSOC_CALL_META_KEY),
7125        Some(Value::Bool(true))
7126    )
7127}
7128
7129fn name_to_primitive(name: &str) -> Option<PrimitiveType> {
7130    match name {
7131        "Int" => Some(PrimitiveType::Int),
7132        "Float" => Some(PrimitiveType::Float),
7133        "Bool" => Some(PrimitiveType::Bool),
7134        "String" => Some(PrimitiveType::String),
7135        "Char" => Some(PrimitiveType::Char),
7136        "Void" => Some(PrimitiveType::Void),
7137        "Never" => Some(PrimitiveType::Never),
7138        "Byte" => Some(PrimitiveType::Byte),
7139        "Bytes" => Some(PrimitiveType::Bytes),
7140        "Int8" => Some(PrimitiveType::Int8),
7141        "Int16" => Some(PrimitiveType::Int16),
7142        "Int32" => Some(PrimitiveType::Int32),
7143        "Int64" => Some(PrimitiveType::Int64),
7144        "Int128" => Some(PrimitiveType::Int128),
7145        "UInt8" => Some(PrimitiveType::UInt8),
7146        "UInt16" => Some(PrimitiveType::UInt16),
7147        "UInt32" => Some(PrimitiveType::UInt32),
7148        "UInt64" => Some(PrimitiveType::UInt64),
7149        "Float32" => Some(PrimitiveType::Float32),
7150        "Float64" => Some(PrimitiveType::Float64),
7151        "BigInt" => Some(PrimitiveType::BigInt),
7152        "BigFloat" => Some(PrimitiveType::BigFloat),
7153        "Decimal" => Some(PrimitiveType::Decimal),
7154        _ => None,
7155    }
7156}
7157
7158/// Suggest a conversion for common numeric/string primitive mismatches.
7159///
7160/// **Direction-aware**: the suggestion always names the conversion that
7161/// produces the **expected** type from the **found** value. When no such
7162/// conversion exists in Bock's surface, this returns `None` — a hint
7163/// suggesting the wrong-direction conversion is worse than no hint
7164/// (it would steer an agent's repair away from the type the context
7165/// requires).
7166fn conversion_hint(found: &Type, expected: &Type) -> Option<String> {
7167    let f = as_primitive(found)?;
7168    let e = as_primitive(expected)?;
7169    use PrimitiveType as P;
7170    let is_int = |p: &P| {
7171        matches!(
7172            p,
7173            P::Int
7174                | P::Int8
7175                | P::Int16
7176                | P::Int32
7177                | P::Int64
7178                | P::Int128
7179                | P::UInt8
7180                | P::UInt16
7181                | P::UInt32
7182                | P::UInt64
7183                | P::BigInt
7184        )
7185    };
7186    let is_float = |p: &P| matches!(p, P::Float | P::Float32 | P::Float64 | P::BigFloat);
7187
7188    // Found an integer where `Float` is expected: `.to_float()` produces
7189    // exactly `Float` (only suggested for the unsized expected type).
7190    if is_int(&f) && e == P::Float {
7191        return Some(format!(
7192            "call `.to_float()` on the `{f}` value to produce the expected `Float`"
7193        ));
7194    }
7195    // Found a float where `Int` is expected: `.to_int()` produces `Int`.
7196    if is_float(&f) && e == P::Int {
7197        return Some(format!(
7198            "call `.to_int()` on the `{f}` value (truncates toward zero) to produce the expected `Int`"
7199        ));
7200    }
7201    // Found a `String` where a number is expected: a String is *parsed*,
7202    // not converted — `Int.try_from` / `Float.try_from` return a `Result`.
7203    if f == P::String && matches!(e, P::Int | P::Float) {
7204        return Some(format!(
7205            "a `String` is not implicitly converted; parse it with `{e}.try_from(...)` (returns a `Result` — handle the failure case)"
7206        ));
7207    }
7208    // Found any other primitive where `String` is expected: `.to_string()`.
7209    if e == P::String && f != P::String {
7210        return Some(format!(
7211            "call `.to_string()` on the `{f}` value to produce the expected `String`"
7212        ));
7213    }
7214    None
7215}
7216
7217/// Extract the underlying `PrimitiveType` if `ty` is `Type::Primitive(_)`.
7218fn as_primitive(ty: &Type) -> Option<PrimitiveType> {
7219    match ty {
7220        Type::Primitive(p) => Some(p.clone()),
7221        _ => None,
7222    }
7223}
7224
7225// ─── Tests ────────────────────────────────────────────────────────────────────
7226
7227#[cfg(test)]
7228mod tests {
7229    use super::*;
7230    use bock_air::{AIRNode, NodeIdGen, NodeKind};
7231    use bock_ast::{BinOp, Ident, Literal, TypePath};
7232    use bock_errors::{FileId, Span};
7233
7234    fn span() -> Span {
7235        Span {
7236            file: FileId(0),
7237            start: 0,
7238            end: 0,
7239        }
7240    }
7241
7242    fn ident(name: &str) -> Ident {
7243        Ident {
7244            name: name.into(),
7245            span: span(),
7246        }
7247    }
7248
7249    fn make_node(gen: &NodeIdGen, kind: NodeKind) -> AIRNode {
7250        AIRNode::new(gen.next(), span(), kind)
7251    }
7252
7253    fn int_lit(gen: &NodeIdGen) -> AIRNode {
7254        make_node(
7255            gen,
7256            NodeKind::Literal {
7257                lit: Literal::Int("42".into()),
7258            },
7259        )
7260    }
7261
7262    fn bool_lit(gen: &NodeIdGen, v: bool) -> AIRNode {
7263        make_node(
7264            gen,
7265            NodeKind::Literal {
7266                lit: Literal::Bool(v),
7267            },
7268        )
7269    }
7270
7271    fn str_lit(gen: &NodeIdGen) -> AIRNode {
7272        make_node(
7273            gen,
7274            NodeKind::Literal {
7275                lit: Literal::String("hello".into()),
7276            },
7277        )
7278    }
7279
7280    fn float_lit(gen: &NodeIdGen) -> AIRNode {
7281        make_node(
7282            gen,
7283            NodeKind::Literal {
7284                lit: Literal::Float("3.14".into()),
7285            },
7286        )
7287    }
7288
7289    fn type_named_node(gen: &NodeIdGen, name: &str) -> AIRNode {
7290        make_node(
7291            gen,
7292            NodeKind::TypeNamed {
7293                path: TypePath {
7294                    segments: vec![ident(name)],
7295                    span: span(),
7296                },
7297                args: vec![],
7298            },
7299        )
7300    }
7301
7302    // ── Diagnostic quality (Q-diag-e4001-message-quality /
7303    //    Q-diag-effect-violation-errors) ───────────────────────────────────
7304
7305    /// E4001 must read ``expected `T`, found `U``` in surface syntax — never
7306    /// the doubled-prefix Debug leak `type mismatch: Primitive(String) vs
7307    /// Primitive(Int)`.
7308    #[test]
7309    fn type_mismatch_message_reads_expected_then_found() {
7310        let gen = NodeIdGen::new();
7311        let mut checker = TypeChecker::new();
7312        let lit = str_lit(&gen);
7313        checker.check_expr(&lit, &Type::Primitive(PrimitiveType::Int));
7314        let diag = checker.diags.iter().next().expect("a diagnostic");
7315        assert_eq!(diag.code.to_string(), "E4001");
7316        assert!(
7317            diag.message.contains("expected `Int`, found `String`"),
7318            "message: {}",
7319            diag.message
7320        );
7321        assert!(
7322            !diag.message.contains("Primitive("),
7323            "message leaks Debug representation: {}",
7324            diag.message
7325        );
7326    }
7327
7328    /// The E4001 conversion hint must suggest the conversion that produces
7329    /// the **expected** type. A `String` found where `Int` is expected is
7330    /// parsed (`Int.try_from`), NOT `.to_string()`-ed (which would convert
7331    /// the wrong operand and make the code wronger).
7332    #[test]
7333    fn type_mismatch_hint_for_expected_int_found_string_is_parse() {
7334        let gen = NodeIdGen::new();
7335        let mut checker = TypeChecker::new();
7336        let lit = str_lit(&gen);
7337        checker.check_expr(&lit, &Type::Primitive(PrimitiveType::Int));
7338        let diag = checker.diags.iter().next().expect("a diagnostic");
7339        assert!(
7340            diag.notes.iter().any(|n| n.contains("Int.try_from")),
7341            "notes: {:?}",
7342            diag.notes
7343        );
7344        assert!(
7345            !diag.notes.iter().any(|n| n.contains(".to_string()")),
7346            "misleading wrong-direction hint: {:?}",
7347            diag.notes
7348        );
7349    }
7350
7351    #[test]
7352    fn conversion_hint_is_direction_aware() {
7353        let int = Type::Primitive(PrimitiveType::Int);
7354        let float = Type::Primitive(PrimitiveType::Float);
7355        let string = Type::Primitive(PrimitiveType::String);
7356        let bool_t = Type::Primitive(PrimitiveType::Bool);
7357
7358        // found Int, expected Float → convert the Int.
7359        let hint = conversion_hint(&int, &float).expect("hint");
7360        assert!(hint.contains(".to_float()"), "{hint}");
7361        // found Float, expected Int → convert the Float.
7362        let hint = conversion_hint(&float, &int).expect("hint");
7363        assert!(hint.contains(".to_int()"), "{hint}");
7364        // found Int, expected String → stringify the Int.
7365        let hint = conversion_hint(&int, &string).expect("hint");
7366        assert!(hint.contains(".to_string()"), "{hint}");
7367        // found String, expected Int/Float → parse, not `.to_string()`.
7368        let hint = conversion_hint(&string, &int).expect("hint");
7369        assert!(hint.contains("Int.try_from"), "{hint}");
7370        let hint = conversion_hint(&string, &float).expect("hint");
7371        assert!(hint.contains("Float.try_from"), "{hint}");
7372        // No determinable conversion → no hint (a wrong suggestion is
7373        // worse than none).
7374        assert_eq!(conversion_hint(&bool_t, &int), None);
7375        assert_eq!(conversion_hint(&string, &bool_t), None);
7376        // Sized float targets have no `.to_float()` shortcut → no hint.
7377        assert_eq!(
7378            conversion_hint(&int, &Type::Primitive(PrimitiveType::Float32)),
7379            None
7380        );
7381    }
7382
7383    /// The lowerer's method-call desugar duplicates the receiver node, so an
7384    /// undefined name can be inferred twice at the identical span. One root
7385    /// cause → one diagnostic (rubric #6).
7386    #[test]
7387    fn undefined_variable_reported_once_per_name_and_span() {
7388        let gen = NodeIdGen::new();
7389        let mut checker = TypeChecker::new();
7390        // Two distinct nodes (distinct ids), same name and span — the shape
7391        // the desugar produces.
7392        let first = make_node(
7393            &gen,
7394            NodeKind::Identifier {
7395                name: ident("ghost"),
7396            },
7397        );
7398        let second = make_node(
7399            &gen,
7400            NodeKind::Identifier {
7401                name: ident("ghost"),
7402            },
7403        );
7404        checker.infer_expr(&first);
7405        checker.infer_expr(&second);
7406        assert_eq!(
7407            checker.diags.error_count(),
7408            1,
7409            "expected exactly one E4002 for one root cause"
7410        );
7411    }
7412
7413    /// `Effect.handler(...)` (the v1.x-reserved lambda-handler surface) must
7414    /// report the actual rule as a single E6006 — not a doubled, rule-less
7415    /// `E4002 undefined variable` at the effect name.
7416    #[test]
7417    fn reserved_lambda_handler_reports_e6006_once() {
7418        let gen = NodeIdGen::new();
7419        let mut checker = TypeChecker::new();
7420        checker.insert_effect_op_types(
7421            "Log".into(),
7422            vec![(
7423                "log".into(),
7424                Type::Function(FnType {
7425                    params: vec![Type::Primitive(PrimitiveType::String)],
7426                    ret: Box::new(Type::Primitive(PrimitiveType::Void)),
7427                    effects: vec![],
7428                }),
7429            )],
7430        );
7431        let object = make_node(&gen, NodeKind::Identifier { name: ident("Log") });
7432        let field_access = make_node(
7433            &gen,
7434            NodeKind::FieldAccess {
7435                object: Box::new(object),
7436                field: ident("handler"),
7437            },
7438        );
7439        let ty = checker.infer_expr(&field_access);
7440        assert_eq!(ty, Type::Error);
7441        assert_eq!(checker.diags.error_count(), 1, "exactly one diagnostic");
7442        let diag = checker.diags.iter().next().expect("a diagnostic");
7443        assert_eq!(diag.code.to_string(), "E6006");
7444        assert!(
7445            diag.message.contains("`Log.handler(...)`")
7446                && diag.message.contains("reserved until v1.x"),
7447            "message: {}",
7448            diag.message
7449        );
7450        assert!(
7451            diag.notes.iter().any(|n| n.contains("impl Log for")),
7452            "note must state the supported v1 handler form: {:?}",
7453            diag.notes
7454        );
7455    }
7456
7457    /// A `.handler` access on an ordinary undefined name (not an effect)
7458    /// still reports the generic undefined variable, not E6006.
7459    #[test]
7460    fn handler_field_on_non_effect_still_undefined_variable() {
7461        let gen = NodeIdGen::new();
7462        let mut checker = TypeChecker::new();
7463        let object = make_node(
7464            &gen,
7465            NodeKind::Identifier {
7466                name: ident("NotAnEffect"),
7467            },
7468        );
7469        let field_access = make_node(
7470            &gen,
7471            NodeKind::FieldAccess {
7472                object: Box::new(object),
7473                field: ident("handler"),
7474            },
7475        );
7476        checker.infer_expr(&field_access);
7477        let diag = checker.diags.iter().next().expect("a diagnostic");
7478        assert_eq!(diag.code.to_string(), "E4002");
7479    }
7480
7481    // ── Literal inference ──────────────────────────────────────────────────
7482
7483    #[test]
7484    fn infer_int_literal() {
7485        let gen = NodeIdGen::new();
7486        let mut checker = TypeChecker::new();
7487        let node = int_lit(&gen);
7488        let ty = checker.infer_expr(&node);
7489        assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
7490    }
7491
7492    #[test]
7493    fn infer_float_literal() {
7494        let gen = NodeIdGen::new();
7495        let mut checker = TypeChecker::new();
7496        let node = float_lit(&gen);
7497        let ty = checker.infer_expr(&node);
7498        assert_eq!(ty, Type::Primitive(PrimitiveType::Float));
7499    }
7500
7501    #[test]
7502    fn infer_bool_literal() {
7503        let gen = NodeIdGen::new();
7504        let mut checker = TypeChecker::new();
7505        let node = bool_lit(&gen, true);
7506        let ty = checker.infer_expr(&node);
7507        assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
7508    }
7509
7510    #[test]
7511    fn infer_string_literal() {
7512        let gen = NodeIdGen::new();
7513        let mut checker = TypeChecker::new();
7514        let node = str_lit(&gen);
7515        let ty = checker.infer_expr(&node);
7516        assert_eq!(ty, Type::Primitive(PrimitiveType::String));
7517    }
7518
7519    // ── Variable inference ─────────────────────────────────────────────────
7520
7521    #[test]
7522    fn infer_defined_variable() {
7523        let gen = NodeIdGen::new();
7524        let mut checker = TypeChecker::new();
7525        checker.env.define("x", Type::Primitive(PrimitiveType::Int));
7526        let node = make_node(&gen, NodeKind::Identifier { name: ident("x") });
7527        let ty = checker.infer_expr(&node);
7528        assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
7529    }
7530
7531    #[test]
7532    fn infer_undefined_variable_emits_error() {
7533        let gen = NodeIdGen::new();
7534        let mut checker = TypeChecker::new();
7535        let node = make_node(
7536            &gen,
7537            NodeKind::Identifier {
7538                name: ident("unknown"),
7539            },
7540        );
7541        let ty = checker.infer_expr(&node);
7542        assert_eq!(ty, Type::Error);
7543        assert!(checker.diags.has_errors());
7544    }
7545
7546    // ── Binary op inference ────────────────────────────────────────────────
7547
7548    #[test]
7549    fn infer_int_addition() {
7550        let gen = NodeIdGen::new();
7551        let mut checker = TypeChecker::new();
7552        let left = int_lit(&gen);
7553        let right = int_lit(&gen);
7554        let node = make_node(
7555            &gen,
7556            NodeKind::BinaryOp {
7557                op: BinOp::Add,
7558                left: Box::new(left),
7559                right: Box::new(right),
7560            },
7561        );
7562        let ty = checker.infer_expr(&node);
7563        assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
7564    }
7565
7566    #[test]
7567    fn infer_comparison_returns_bool() {
7568        let gen = NodeIdGen::new();
7569        let mut checker = TypeChecker::new();
7570        let left = int_lit(&gen);
7571        let right = int_lit(&gen);
7572        let node = make_node(
7573            &gen,
7574            NodeKind::BinaryOp {
7575                op: BinOp::Lt,
7576                left: Box::new(left),
7577                right: Box::new(right),
7578            },
7579        );
7580        let ty = checker.infer_expr(&node);
7581        assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
7582    }
7583
7584    // ── Operator gating: comparison on user types (§18.5, Q-list-operator-
7585    //    gating-user-types) ──────────────────────────────────────────────────
7586
7587    /// A `<` on a user (Named) type whose definition does NOT `impl Comparable`
7588    /// must be rejected: §18.5 gates the comparison operators behind the trait.
7589    #[test]
7590    fn comparison_on_user_type_without_comparable_errors() {
7591        let gen = NodeIdGen::new();
7592        let mut checker = TypeChecker::new();
7593        // Impl table present, but `Point` does not implement Comparable.
7594        checker.impl_table = Some(make_impl_table(&[(
7595            "Comparable",
7596            Type::Primitive(PrimitiveType::Int),
7597        )]));
7598
7599        checker.env.define(
7600            "a",
7601            Type::Named(crate::NamedType {
7602                name: "Point".into(),
7603            }),
7604        );
7605        checker.env.define(
7606            "b",
7607            Type::Named(crate::NamedType {
7608                name: "Point".into(),
7609            }),
7610        );
7611        let left = make_node(&gen, NodeKind::Identifier { name: ident("a") });
7612        let right = make_node(&gen, NodeKind::Identifier { name: ident("b") });
7613        let node = make_node(
7614            &gen,
7615            NodeKind::BinaryOp {
7616                op: BinOp::Lt,
7617                left: Box::new(left),
7618                right: Box::new(right),
7619            },
7620        );
7621        checker.infer_expr(&node);
7622        assert!(
7623            checker.diags.has_errors(),
7624            "expected error: Point does not implement Comparable"
7625        );
7626    }
7627
7628    /// The same user type WITH `impl Comparable` checks clean under `<`.
7629    #[test]
7630    fn comparison_on_user_type_with_comparable_ok() {
7631        let gen = NodeIdGen::new();
7632        let mut checker = TypeChecker::new();
7633        let point = Type::Named(crate::NamedType {
7634            name: "Point".into(),
7635        });
7636        checker.impl_table = Some(make_impl_table(&[("Comparable", point.clone())]));
7637
7638        checker.env.define("a", point.clone());
7639        checker.env.define("b", point);
7640        let left = make_node(&gen, NodeKind::Identifier { name: ident("a") });
7641        let right = make_node(&gen, NodeKind::Identifier { name: ident("b") });
7642        let node = make_node(
7643            &gen,
7644            NodeKind::BinaryOp {
7645                op: BinOp::Gt,
7646                left: Box::new(left),
7647                right: Box::new(right),
7648            },
7649        );
7650        let ty = checker.infer_expr(&node);
7651        assert!(
7652            !checker.diags.has_errors(),
7653            "expected no errors: Point implements Comparable"
7654        );
7655        assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
7656    }
7657
7658    /// Each of the four ordering operators is gated identically.
7659    #[test]
7660    fn all_ordering_operators_gated_on_user_types() {
7661        for op in [BinOp::Lt, BinOp::Le, BinOp::Gt, BinOp::Ge] {
7662            let gen = NodeIdGen::new();
7663            let mut checker = TypeChecker::new();
7664            checker.impl_table = Some(make_impl_table(&[(
7665                "Comparable",
7666                Type::Primitive(PrimitiveType::Int),
7667            )]));
7668            checker.env.define(
7669                "a",
7670                Type::Named(crate::NamedType {
7671                    name: "Widget".into(),
7672                }),
7673            );
7674            checker.env.define(
7675                "b",
7676                Type::Named(crate::NamedType {
7677                    name: "Widget".into(),
7678                }),
7679            );
7680            let left = make_node(&gen, NodeKind::Identifier { name: ident("a") });
7681            let right = make_node(&gen, NodeKind::Identifier { name: ident("b") });
7682            let node = make_node(
7683                &gen,
7684                NodeKind::BinaryOp {
7685                    op,
7686                    left: Box::new(left),
7687                    right: Box::new(right),
7688                },
7689            );
7690            checker.infer_expr(&node);
7691            assert!(
7692                checker.diags.has_errors(),
7693                "expected error for {op:?} on a non-Comparable user type"
7694            );
7695        }
7696    }
7697
7698    /// Comparison on primitives still works without explicit gating fallout:
7699    /// `Int < Int` with the canonical conformances registered is accepted, and
7700    /// — to mirror the existing `infer_comparison_returns_bool` test — with no
7701    /// impl table at all the gate is skipped (cannot prove non-conformance).
7702    #[test]
7703    fn comparison_on_primitive_not_gated_when_conformant() {
7704        let gen = NodeIdGen::new();
7705        let mut checker = TypeChecker::new();
7706        let mut table = ImplTable::new();
7707        crate::traits::register_canonical_conformances(&mut table);
7708        checker.impl_table = Some(table);
7709
7710        let left = int_lit(&gen);
7711        let right = int_lit(&gen);
7712        let node = make_node(
7713            &gen,
7714            NodeKind::BinaryOp {
7715                op: BinOp::Lt,
7716                left: Box::new(left),
7717                right: Box::new(right),
7718            },
7719        );
7720        let ty = checker.infer_expr(&node);
7721        assert!(
7722            !checker.diags.has_errors(),
7723            "Int is Comparable; `<` must be accepted"
7724        );
7725        assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
7726    }
7727
7728    /// A bounded generic param (`T: Comparable`) compared with `<` must NOT be
7729    /// flagged: the operand type is a `TypeVar`/`TraitBound`, not a Named type,
7730    /// so the user-type gate does not apply (the where-clause check covers it).
7731    #[test]
7732    fn comparison_on_bounded_generic_param_not_gated() {
7733        let gen = NodeIdGen::new();
7734        let mut checker = TypeChecker::new();
7735        checker.impl_table = Some(make_impl_table(&[(
7736            "Comparable",
7737            Type::Primitive(PrimitiveType::Int),
7738        )]));
7739        // A fresh inference variable stands in for the bounded generic param.
7740        let tv = checker.fresh_var();
7741        checker.env.define("a", tv.clone());
7742        checker.env.define("b", tv);
7743        let left = make_node(&gen, NodeKind::Identifier { name: ident("a") });
7744        let right = make_node(&gen, NodeKind::Identifier { name: ident("b") });
7745        let node = make_node(
7746            &gen,
7747            NodeKind::BinaryOp {
7748                op: BinOp::Lt,
7749                left: Box::new(left),
7750                right: Box::new(right),
7751            },
7752        );
7753        checker.infer_expr(&node);
7754        assert!(
7755            !checker.diags.has_errors(),
7756            "comparison on an inference variable must not trigger the user-type gate"
7757        );
7758    }
7759
7760    // ── User-comparison codegen stamp (USER_COMPARE_META_KEY,
7761    //    Q-user-comparison-codegen) ─────────────────────────────────────────────
7762
7763    /// Build a `BinaryOp` over two operands both bound to `operand_ty`, run the
7764    /// body pass over it (`infer_node`, which mutates `node.metadata`), and return
7765    /// the node so a test can inspect its stamps.
7766    fn infer_binop_node(checker: &mut TypeChecker, op: BinOp, operand_ty: Type) -> AIRNode {
7767        let gen = NodeIdGen::new();
7768        checker.env.define("a", operand_ty.clone());
7769        checker.env.define("b", operand_ty);
7770        let left = make_node(&gen, NodeKind::Identifier { name: ident("a") });
7771        let right = make_node(&gen, NodeKind::Identifier { name: ident("b") });
7772        let mut node = make_node(
7773            &gen,
7774            NodeKind::BinaryOp {
7775                op,
7776                left: Box::new(left),
7777                right: Box::new(right),
7778            },
7779        );
7780        checker.infer_node(&mut node);
7781        node
7782    }
7783
7784    /// Each ordering operator on a user `Comparable` type stamps the node so
7785    /// codegen routes the operator through `compare`.
7786    #[test]
7787    fn user_comparison_stamps_ordering_ops() {
7788        let point = Type::Named(crate::NamedType {
7789            name: "Point".into(),
7790        });
7791        for op in [BinOp::Lt, BinOp::Le, BinOp::Gt, BinOp::Ge] {
7792            let mut checker = TypeChecker::new();
7793            checker.impl_table = Some(make_impl_table(&[("Comparable", point.clone())]));
7794            let node = infer_binop_node(&mut checker, op, point.clone());
7795            assert_eq!(
7796                node.metadata.get(USER_COMPARE_META_KEY),
7797                Some(&bock_air::Value::Bool(true)),
7798                "{op:?} on a user Comparable type must be stamped"
7799            );
7800        }
7801    }
7802
7803    /// A primitive ordering comparison is NOT stamped — native `<` already works
7804    /// on every target, so codegen must keep emitting it.
7805    #[test]
7806    fn primitive_comparison_not_stamped() {
7807        let mut checker = TypeChecker::new();
7808        let mut table = ImplTable::new();
7809        crate::traits::register_canonical_conformances(&mut table);
7810        checker.impl_table = Some(table);
7811        let node = infer_binop_node(&mut checker, BinOp::Lt, Type::Primitive(PrimitiveType::Int));
7812        assert!(
7813            !node.metadata.contains_key(USER_COMPARE_META_KEY),
7814            "primitive `<` must not carry the user-compare stamp"
7815        );
7816    }
7817
7818    /// Equality (`==`) on a user type is the sibling Equatable lane — it must NOT
7819    /// be stamped by the comparison arm.
7820    #[test]
7821    fn user_equality_not_stamped_by_comparison_arm() {
7822        let point = Type::Named(crate::NamedType {
7823            name: "Point".into(),
7824        });
7825        let mut checker = TypeChecker::new();
7826        checker.impl_table = Some(make_impl_table(&[("Comparable", point.clone())]));
7827        let node = infer_binop_node(&mut checker, BinOp::Eq, point);
7828        assert!(
7829            !node.metadata.contains_key(USER_COMPARE_META_KEY),
7830            "`==` is the Equatable lane and must not carry the user-compare stamp"
7831        );
7832    }
7833
7834    /// A user type that does NOT implement `Comparable` is not stamped (the
7835    /// comparison is also rejected by the gate; codegen never sees it, but the
7836    /// stamp must be absent regardless).
7837    #[test]
7838    fn non_comparable_user_type_not_stamped() {
7839        let point = Type::Named(crate::NamedType {
7840            name: "Point".into(),
7841        });
7842        let mut checker = TypeChecker::new();
7843        // Impl table present, but `Point` does NOT implement Comparable.
7844        checker.impl_table = Some(make_impl_table(&[(
7845            "Comparable",
7846            Type::Primitive(PrimitiveType::Int),
7847        )]));
7848        let node = infer_binop_node(&mut checker, BinOp::Lt, point);
7849        assert!(
7850            !node.metadata.contains_key(USER_COMPARE_META_KEY),
7851            "a non-Comparable user type must not be stamped"
7852        );
7853    }
7854
7855    #[test]
7856    fn infer_logical_and_requires_bool() {
7857        let gen = NodeIdGen::new();
7858        let mut checker = TypeChecker::new();
7859        let left = bool_lit(&gen, true);
7860        let right = bool_lit(&gen, false);
7861        let node = make_node(
7862            &gen,
7863            NodeKind::BinaryOp {
7864                op: BinOp::And,
7865                left: Box::new(left),
7866                right: Box::new(right),
7867            },
7868        );
7869        let ty = checker.infer_expr(&node);
7870        assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
7871    }
7872
7873    #[test]
7874    fn type_mismatch_in_binop_emits_error() {
7875        let gen = NodeIdGen::new();
7876        let mut checker = TypeChecker::new();
7877        let left = int_lit(&gen);
7878        let right = bool_lit(&gen, true);
7879        let node = make_node(
7880            &gen,
7881            NodeKind::BinaryOp {
7882                op: BinOp::Add,
7883                left: Box::new(left),
7884                right: Box::new(right),
7885            },
7886        );
7887        checker.infer_expr(&node);
7888        assert!(checker.diags.has_errors());
7889    }
7890
7891    // ── Unary op inference ─────────────────────────────────────────────────
7892
7893    #[test]
7894    fn infer_neg_int() {
7895        let gen = NodeIdGen::new();
7896        let mut checker = TypeChecker::new();
7897        let operand = int_lit(&gen);
7898        let node = make_node(
7899            &gen,
7900            NodeKind::UnaryOp {
7901                op: UnaryOp::Neg,
7902                operand: Box::new(operand),
7903            },
7904        );
7905        let ty = checker.infer_expr(&node);
7906        assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
7907    }
7908
7909    #[test]
7910    fn infer_not_bool() {
7911        let gen = NodeIdGen::new();
7912        let mut checker = TypeChecker::new();
7913        let operand = bool_lit(&gen, true);
7914        let node = make_node(
7915            &gen,
7916            NodeKind::UnaryOp {
7917                op: UnaryOp::Not,
7918                operand: Box::new(operand),
7919            },
7920        );
7921        let ty = checker.infer_expr(&node);
7922        assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
7923    }
7924
7925    // ── List literal (check mode) ──────────────────────────────────────────
7926
7927    #[test]
7928    fn check_list_literal_against_list_int() {
7929        let gen = NodeIdGen::new();
7930        let mut checker = TypeChecker::new();
7931        let expected = Type::Generic(GenericType {
7932            constructor: "List".into(),
7933            args: vec![Type::Primitive(PrimitiveType::Int)],
7934        });
7935        let node = make_node(
7936            &gen,
7937            NodeKind::ListLiteral {
7938                elems: vec![int_lit(&gen), int_lit(&gen)],
7939            },
7940        );
7941        checker.check_expr(&node, &expected);
7942        assert!(!checker.diags.has_errors());
7943    }
7944
7945    #[test]
7946    fn list_element_mismatch_emits_error() {
7947        let gen = NodeIdGen::new();
7948        let mut checker = TypeChecker::new();
7949        let expected = Type::Generic(GenericType {
7950            constructor: "List".into(),
7951            args: vec![Type::Primitive(PrimitiveType::Int)],
7952        });
7953        let node = make_node(
7954            &gen,
7955            NodeKind::ListLiteral {
7956                elems: vec![int_lit(&gen), bool_lit(&gen, true)],
7957            },
7958        );
7959        checker.check_expr(&node, &expected);
7960        assert!(checker.diags.has_errors());
7961    }
7962
7963    // ── Infer mode for list ────────────────────────────────────────────────
7964
7965    #[test]
7966    fn infer_list_literal() {
7967        let gen = NodeIdGen::new();
7968        let mut checker = TypeChecker::new();
7969        let node = make_node(
7970            &gen,
7971            NodeKind::ListLiteral {
7972                elems: vec![int_lit(&gen), int_lit(&gen)],
7973            },
7974        );
7975        let ty = checker.infer_expr(&node);
7976        assert!(matches!(&ty, Type::Generic(g) if g.constructor == "List"
7977                && g.args.len() == 1
7978                && g.args[0] == Type::Primitive(PrimitiveType::Int)));
7979    }
7980
7981    // ── Tuple literal ──────────────────────────────────────────────────────
7982
7983    #[test]
7984    fn infer_tuple_literal() {
7985        let gen = NodeIdGen::new();
7986        let mut checker = TypeChecker::new();
7987        let node = make_node(
7988            &gen,
7989            NodeKind::TupleLiteral {
7990                elems: vec![int_lit(&gen), bool_lit(&gen, false)],
7991            },
7992        );
7993        let ty = checker.infer_expr(&node);
7994        assert_eq!(
7995            ty,
7996            Type::Tuple(vec![
7997                Type::Primitive(PrimitiveType::Int),
7998                Type::Primitive(PrimitiveType::Bool),
7999            ])
8000        );
8001    }
8002
8003    // ── Block inference ────────────────────────────────────────────────────
8004
8005    #[test]
8006    fn infer_block_tail_expression() {
8007        let gen = NodeIdGen::new();
8008        let mut checker = TypeChecker::new();
8009        let tail = int_lit(&gen);
8010        let node = make_node(
8011            &gen,
8012            NodeKind::Block {
8013                stmts: vec![],
8014                tail: Some(Box::new(tail)),
8015            },
8016        );
8017        let ty = checker.infer_expr(&node);
8018        assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
8019    }
8020
8021    #[test]
8022    fn infer_block_no_tail_is_void() {
8023        let gen = NodeIdGen::new();
8024        let mut checker = TypeChecker::new();
8025        let node = make_node(
8026            &gen,
8027            NodeKind::Block {
8028                stmts: vec![],
8029                tail: None,
8030            },
8031        );
8032        let ty = checker.infer_expr(&node);
8033        assert_eq!(ty, Type::Primitive(PrimitiveType::Void));
8034    }
8035
8036    // ── Let binding ────────────────────────────────────────────────────────
8037
8038    #[test]
8039    fn let_binding_infers_and_binds() {
8040        let gen = NodeIdGen::new();
8041        let mut checker = TypeChecker::new();
8042        let pat = make_node(
8043            &gen,
8044            NodeKind::BindPat {
8045                name: ident("x"),
8046                is_mut: false,
8047            },
8048        );
8049        let val = int_lit(&gen);
8050        let let_node = make_node(
8051            &gen,
8052            NodeKind::LetBinding {
8053                is_mut: false,
8054                pattern: Box::new(pat),
8055                ty: None,
8056                value: Box::new(val),
8057            },
8058        );
8059        // Wrap in a block with x used after
8060        let ident_x = make_node(&gen, NodeKind::Identifier { name: ident("x") });
8061        let block = make_node(
8062            &gen,
8063            NodeKind::Block {
8064                stmts: vec![let_node],
8065                tail: Some(Box::new(ident_x)),
8066            },
8067        );
8068        let ty = checker.infer_expr(&block);
8069        assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
8070        assert!(!checker.diags.has_errors());
8071    }
8072
8073    // ── Generic instantiation ──────────────────────────────────────────────
8074
8075    #[test]
8076    fn fresh_var_for_generic_params() {
8077        let mut checker = TypeChecker::new();
8078        // Simulate: first[T](list: List[T]) -> Optional[T]
8079        // Build the sig manually
8080        let t_var = checker.fresh_var(); // T placeholder
8081        let t_id = match &t_var {
8082            Type::TypeVar(id) => *id,
8083            _ => unreachable!(),
8084        };
8085        let sig = FnSig {
8086            generic_params: vec!["T".into()],
8087            generic_var_ids: vec![t_id],
8088            param_types: vec![Type::Generic(GenericType {
8089                constructor: "List".into(),
8090                args: vec![t_var.clone()],
8091            })],
8092            return_type: Type::Optional(Box::new(t_var)),
8093            where_clause: vec![],
8094        };
8095
8096        let gen = NodeIdGen::new();
8097        let arg = make_node(
8098            &gen,
8099            NodeKind::ListLiteral {
8100                elems: vec![int_lit(&gen)],
8101            },
8102        );
8103        let args: Vec<bock_air::AirArg> = vec![bock_air::AirArg {
8104            label: None,
8105            value: arg,
8106        }];
8107
8108        let ret = checker.instantiate_and_check("first", &sig, &args, span());
8109        // Return type should be Optional[?fresh_var]; the fresh var is
8110        // distinct from the original t_var.
8111        assert!(!checker.diags.has_errors());
8112        assert!(matches!(ret, Type::Optional(_)));
8113    }
8114
8115    /// Helper: register a generic function in both `env` and `fn_sigs`.
8116    fn register_generic_fn(
8117        checker: &mut TypeChecker,
8118        name: &str,
8119        generic_names: &[&str],
8120        build_sig: impl FnOnce(&[Type]) -> (Vec<Type>, Type),
8121    ) {
8122        let vars: Vec<Type> = generic_names.iter().map(|_| checker.fresh_var()).collect();
8123        let var_ids: Vec<TypeVarId> = vars
8124            .iter()
8125            .map(|t| match t {
8126                Type::TypeVar(id) => *id,
8127                _ => unreachable!(),
8128            })
8129            .collect();
8130        let (param_types, return_type) = build_sig(&vars);
8131        let fn_ty = Type::Function(FnType {
8132            params: param_types.clone(),
8133            ret: Box::new(return_type.clone()),
8134            effects: vec![],
8135        });
8136        checker.env.define(name, fn_ty);
8137        checker.fn_sigs.insert(
8138            name.into(),
8139            FnSig {
8140                generic_params: generic_names.iter().map(|s| (*s).into()).collect(),
8141                generic_var_ids: var_ids,
8142                param_types,
8143                return_type,
8144                where_clause: vec![],
8145            },
8146        );
8147    }
8148
8149    #[test]
8150    fn generic_first_infers_int() {
8151        // fn first[T](list: List[T]) -> T; first([1,2,3]) → Int
8152        let gen = NodeIdGen::new();
8153        let mut checker = TypeChecker::new();
8154        register_generic_fn(&mut checker, "first", &["T"], |vars| {
8155            let t = vars[0].clone();
8156            let params = vec![Type::Generic(GenericType {
8157                constructor: "List".into(),
8158                args: vec![t.clone()],
8159            })];
8160            (params, t)
8161        });
8162
8163        let callee = make_node(
8164            &gen,
8165            NodeKind::Identifier {
8166                name: ident("first"),
8167            },
8168        );
8169        let list_arg = make_node(
8170            &gen,
8171            NodeKind::ListLiteral {
8172                elems: vec![int_lit(&gen), int_lit(&gen), int_lit(&gen)],
8173            },
8174        );
8175        let call = make_node(
8176            &gen,
8177            NodeKind::Call {
8178                callee: Box::new(callee),
8179                type_args: vec![],
8180                args: vec![bock_air::AirArg {
8181                    label: None,
8182                    value: list_arg,
8183                }],
8184            },
8185        );
8186
8187        let ty = checker.infer_expr(&call);
8188        assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
8189        assert!(!checker.diags.has_errors());
8190    }
8191
8192    #[test]
8193    fn generic_identity_infers_string() {
8194        // fn identity[T](x: T) -> T; identity("hello") → String
8195        let gen = NodeIdGen::new();
8196        let mut checker = TypeChecker::new();
8197        register_generic_fn(&mut checker, "identity", &["T"], |vars| {
8198            let t = vars[0].clone();
8199            (vec![t.clone()], t)
8200        });
8201
8202        let callee = make_node(
8203            &gen,
8204            NodeKind::Identifier {
8205                name: ident("identity"),
8206            },
8207        );
8208        let call = make_node(
8209            &gen,
8210            NodeKind::Call {
8211                callee: Box::new(callee),
8212                type_args: vec![],
8213                args: vec![bock_air::AirArg {
8214                    label: None,
8215                    value: str_lit(&gen),
8216                }],
8217            },
8218        );
8219
8220        let ty = checker.infer_expr(&call);
8221        assert_eq!(ty, Type::Primitive(PrimitiveType::String));
8222        assert!(!checker.diags.has_errors());
8223    }
8224
8225    #[test]
8226    fn generic_two_params_swap() {
8227        // fn swap[A, B](a: A, b: B) -> (B, A); swap(1, "hi") → (String, Int)
8228        let gen = NodeIdGen::new();
8229        let mut checker = TypeChecker::new();
8230        register_generic_fn(&mut checker, "swap", &["A", "B"], |vars| {
8231            let a = vars[0].clone();
8232            let b = vars[1].clone();
8233            let params = vec![a.clone(), b.clone()];
8234            let ret = Type::Tuple(vec![b, a]);
8235            (params, ret)
8236        });
8237
8238        let callee = make_node(
8239            &gen,
8240            NodeKind::Identifier {
8241                name: ident("swap"),
8242            },
8243        );
8244        let call = make_node(
8245            &gen,
8246            NodeKind::Call {
8247                callee: Box::new(callee),
8248                type_args: vec![],
8249                args: vec![
8250                    bock_air::AirArg {
8251                        label: None,
8252                        value: int_lit(&gen),
8253                    },
8254                    bock_air::AirArg {
8255                        label: None,
8256                        value: str_lit(&gen),
8257                    },
8258                ],
8259            },
8260        );
8261
8262        let ty = checker.infer_expr(&call);
8263        assert_eq!(
8264            ty,
8265            Type::Tuple(vec![
8266                Type::Primitive(PrimitiveType::String),
8267                Type::Primitive(PrimitiveType::Int),
8268            ])
8269        );
8270        assert!(!checker.diags.has_errors());
8271    }
8272
8273    #[test]
8274    fn method_call_on_known_type_returns_correct_type() {
8275        // [1, 2, 3].len() → Int
8276        let gen = NodeIdGen::new();
8277        let mut checker = TypeChecker::new();
8278        let list = make_node(
8279            &gen,
8280            NodeKind::ListLiteral {
8281                elems: vec![int_lit(&gen), int_lit(&gen), int_lit(&gen)],
8282            },
8283        );
8284        let method_call = make_node(
8285            &gen,
8286            NodeKind::MethodCall {
8287                receiver: Box::new(list),
8288                method: ident("len"),
8289                type_args: vec![],
8290                args: vec![],
8291            },
8292        );
8293        let ty = checker.infer_expr(&method_call);
8294        assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
8295        assert!(!checker.diags.has_errors());
8296    }
8297
8298    #[test]
8299    fn method_call_string_contains_returns_bool() {
8300        // "hello".contains("lo") → Bool
8301        let gen = NodeIdGen::new();
8302        let mut checker = TypeChecker::new();
8303        let receiver = str_lit(&gen);
8304        let method_call = make_node(
8305            &gen,
8306            NodeKind::MethodCall {
8307                receiver: Box::new(receiver),
8308                method: ident("contains"),
8309                type_args: vec![],
8310                args: vec![bock_air::AirArg {
8311                    label: None,
8312                    value: str_lit(&gen),
8313                }],
8314            },
8315        );
8316        let ty = checker.infer_expr(&method_call);
8317        assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
8318        assert!(!checker.diags.has_errors());
8319    }
8320
8321    // ── DQ18: `push`/`append` return Void ──────────────────────────────────
8322
8323    #[test]
8324    fn method_call_list_push_returns_void() {
8325        // [1].push(2) → Void (DQ18: in-place mutator, value-less)
8326        let gen = NodeIdGen::new();
8327        let mut checker = TypeChecker::new();
8328        let list = make_node(
8329            &gen,
8330            NodeKind::ListLiteral {
8331                elems: vec![int_lit(&gen)],
8332            },
8333        );
8334        let method_call = make_node(
8335            &gen,
8336            NodeKind::MethodCall {
8337                receiver: Box::new(list),
8338                method: ident("push"),
8339                type_args: vec![],
8340                args: vec![bock_air::AirArg {
8341                    label: None,
8342                    value: int_lit(&gen),
8343                }],
8344            },
8345        );
8346        let ty = checker.infer_expr(&method_call);
8347        assert_eq!(ty, Type::Primitive(PrimitiveType::Void));
8348    }
8349
8350    #[test]
8351    fn method_call_list_append_returns_void() {
8352        // [1].append(2) → Void (append is the spelling alias for push)
8353        let gen = NodeIdGen::new();
8354        let mut checker = TypeChecker::new();
8355        let list = make_node(
8356            &gen,
8357            NodeKind::ListLiteral {
8358                elems: vec![int_lit(&gen)],
8359            },
8360        );
8361        let method_call = make_node(
8362            &gen,
8363            NodeKind::MethodCall {
8364                receiver: Box::new(list),
8365                method: ident("append"),
8366                type_args: vec![],
8367                args: vec![bock_air::AirArg {
8368                    label: None,
8369                    value: int_lit(&gen),
8370                }],
8371            },
8372        );
8373        let ty = checker.infer_expr(&method_call);
8374        assert_eq!(ty, Type::Primitive(PrimitiveType::Void));
8375    }
8376
8377    // ── DQ22: `contains` is not a `Map` method ─────────────────────────────
8378
8379    /// Build a `{key: val}.<method>(arg)` call in the lowerer's desugared shape
8380    /// (`Call { callee: FieldAccess(map, method), args: [map, arg] }`). The `map`
8381    /// receiver is a single-entry `MapLiteral` so the checker resolves it to
8382    /// `Map[K, V]`; the `self` arg shares the field-access object's NodeId.
8383    fn desugared_map_method_call(
8384        gen: &NodeIdGen,
8385        method: &str,
8386        key: AIRNode,
8387        val: AIRNode,
8388        arg: AIRNode,
8389    ) -> AIRNode {
8390        let map = make_node(
8391            gen,
8392            NodeKind::MapLiteral {
8393                entries: vec![bock_air::AirMapEntry { key, value: val }],
8394            },
8395        );
8396        let map_self = map.clone();
8397        let callee = make_node(
8398            gen,
8399            NodeKind::FieldAccess {
8400                object: Box::new(map),
8401                field: ident(method),
8402            },
8403        );
8404        make_node(
8405            gen,
8406            NodeKind::Call {
8407                callee: Box::new(callee),
8408                type_args: vec![],
8409                args: vec![
8410                    bock_air::AirArg {
8411                        label: None,
8412                        value: map_self,
8413                    },
8414                    bock_air::AirArg {
8415                        label: None,
8416                        value: arg,
8417                    },
8418                ],
8419            },
8420        )
8421    }
8422
8423    #[test]
8424    fn map_contains_is_rejected_with_suggestion() {
8425        // {"a": 1}.contains("a") → error: did you mean `contains_key`?
8426        let gen = NodeIdGen::new();
8427        let mut checker = TypeChecker::new();
8428        let call = desugared_map_method_call(
8429            &gen,
8430            "contains",
8431            str_lit(&gen),
8432            int_lit(&gen),
8433            str_lit(&gen),
8434        );
8435        let _ = checker.infer_expr(&call);
8436        assert!(checker.diags.has_errors());
8437        let err = checker
8438            .diags
8439            .iter()
8440            .find(|d| d.code == E_NO_SUCH_METHOD)
8441            .expect("expected an E4013 Map-contains rejection");
8442        assert!(err.message.contains("contains_key"));
8443        assert!(!err.notes.is_empty(), "expected a suggestion note");
8444    }
8445
8446    #[test]
8447    fn map_contains_key_still_resolves() {
8448        // {"a": 1}.contains_key("a") → Bool, no rejection.
8449        let gen = NodeIdGen::new();
8450        let mut checker = TypeChecker::new();
8451        let call = desugared_map_method_call(
8452            &gen,
8453            "contains_key",
8454            str_lit(&gen),
8455            int_lit(&gen),
8456            str_lit(&gen),
8457        );
8458        let _ = checker.infer_expr(&call);
8459        assert!(
8460            !checker.diags.iter().any(|d| d.code == E_NO_SUCH_METHOD),
8461            "contains_key must not be rejected"
8462        );
8463    }
8464
8465    // ── Q-checker-unknown-method-concrete ────────────────────────────────────
8466
8467    /// Build the desugared method call for `[1].method(...)` on a `List[Int]`.
8468    fn desugared_list_method_call(gen: &NodeIdGen, method: &str, arg: Option<AIRNode>) -> AIRNode {
8469        let list = make_node(
8470            gen,
8471            NodeKind::ListLiteral {
8472                elems: vec![int_lit(gen)],
8473            },
8474        );
8475        let list_self = list.clone();
8476        let callee = make_node(
8477            gen,
8478            NodeKind::FieldAccess {
8479                object: Box::new(list),
8480                field: ident(method),
8481            },
8482        );
8483        let mut args = vec![bock_air::AirArg {
8484            label: None,
8485            value: list_self,
8486        }];
8487        if let Some(a) = arg {
8488            args.push(bock_air::AirArg {
8489                label: None,
8490                value: a,
8491            });
8492        }
8493        make_node(
8494            gen,
8495            NodeKind::Call {
8496                callee: Box::new(callee),
8497                type_args: vec![],
8498                args,
8499            },
8500        )
8501    }
8502
8503    /// An unknown method on a concrete built-in receiver (`List[Int]`) is an
8504    /// `E4013` error, not a silent fresh type variable.
8505    #[test]
8506    fn list_unknown_method_is_rejected() {
8507        let gen = NodeIdGen::new();
8508        let mut checker = TypeChecker::new();
8509        let call = desugared_list_method_call(&gen, "frobnicate", None);
8510        let _ = checker.infer_expr(&call);
8511        let err = checker
8512            .diags
8513            .iter()
8514            .find(|d| d.code == E_NO_SUCH_METHOD)
8515            .expect("expected an E4013 unknown-method rejection");
8516        assert!(err.message.contains("frobnicate"));
8517        assert!(err.message.contains("List[Int]"));
8518    }
8519
8520    /// A near-name typo on a concrete receiver gets a "did you mean `…`?" note.
8521    #[test]
8522    fn list_unknown_method_suggests_nearest() {
8523        let gen = NodeIdGen::new();
8524        let mut checker = TypeChecker::new();
8525        // `lenght` is one transposition away from `length`.
8526        let call = desugared_list_method_call(&gen, "lenght", None);
8527        let _ = checker.infer_expr(&call);
8528        let err = checker
8529            .diags
8530            .iter()
8531            .find(|d| d.code == E_NO_SUCH_METHOD)
8532            .expect("expected an E4013 unknown-method rejection");
8533        assert!(
8534            err.notes.iter().any(|n| n.contains("length")),
8535            "expected a `did you mean `length`?` suggestion, got: {:?}",
8536            err.notes
8537        );
8538    }
8539
8540    /// A real built-in method (List `map`) still resolves cleanly — the check
8541    /// fires only for genuinely-unknown methods.
8542    #[test]
8543    fn list_known_method_not_rejected() {
8544        let gen = NodeIdGen::new();
8545        let mut checker = TypeChecker::new();
8546        let lambda = make_node(
8547            &gen,
8548            NodeKind::Lambda {
8549                params: vec![make_node(
8550                    &gen,
8551                    NodeKind::Param {
8552                        pattern: Box::new(make_node(
8553                            &gen,
8554                            NodeKind::BindPat {
8555                                name: ident("x"),
8556                                is_mut: false,
8557                            },
8558                        )),
8559                        ty: None,
8560                        default: None,
8561                    },
8562                )],
8563                body: Box::new(make_node(&gen, NodeKind::Identifier { name: ident("x") })),
8564            },
8565        );
8566        let call = desugared_list_method_call(&gen, "map", Some(lambda));
8567        let _ = checker.infer_expr(&call);
8568        assert!(
8569            !checker.diags.iter().any(|d| d.code == E_NO_SUCH_METHOD),
8570            "a known List method (`map`) must not be rejected"
8571        );
8572    }
8573
8574    /// The `nearest_method_name` helper returns a suggestion only for a close
8575    /// candidate, and `None` for an unrelated name.
8576    #[test]
8577    fn nearest_method_name_thresholds() {
8578        let cands = vec!["length".to_string(), "len".to_string(), "push".to_string()];
8579        assert_eq!(
8580            nearest_method_name("lenght", &cands).as_deref(),
8581            Some("length")
8582        );
8583        assert_eq!(nearest_method_name("frobnicate", &cands), None);
8584    }
8585
8586    // ── Q-import-reject (§12.2 / DQ8) ────────────────────────────────────────
8587
8588    /// Build a `Module` node with a single `use <segments>` import carrying the
8589    /// given [`ImportItems`].
8590    fn module_with_import(
8591        gen: &NodeIdGen,
8592        segments: &[&str],
8593        items: bock_ast::ImportItems,
8594    ) -> AIRNode {
8595        let dummy = bock_errors::Span {
8596            file: bock_errors::FileId(0),
8597            start: 0,
8598            end: 0,
8599        };
8600        let import = make_node(
8601            gen,
8602            NodeKind::ImportDecl {
8603                path: bock_ast::ModulePath {
8604                    segments: segments
8605                        .iter()
8606                        .map(|s| bock_ast::Ident {
8607                            name: (*s).to_string(),
8608                            span: dummy,
8609                        })
8610                        .collect(),
8611                    span: dummy,
8612                },
8613                items,
8614            },
8615        );
8616        make_node(
8617            gen,
8618            NodeKind::Module {
8619                path: None,
8620                annotations: vec![],
8621                imports: vec![import],
8622                items: vec![],
8623            },
8624        )
8625    }
8626
8627    /// A bare module import (`use core.error`, `ImportItems::Module`) is rejected
8628    /// with `E4014` pointing at the braced form.
8629    #[test]
8630    fn bare_module_import_is_rejected() {
8631        let gen = NodeIdGen::new();
8632        let mut checker = TypeChecker::new();
8633        let mut module =
8634            module_with_import(&gen, &["core", "error"], bock_ast::ImportItems::Module);
8635        checker.check_module(&mut module);
8636        let err = checker
8637            .diags
8638            .iter()
8639            .find(|d| d.code == E_BARE_MODULE_IMPORT)
8640            .expect("expected an E4014 bare-module-import rejection");
8641        assert!(err.message.contains("core.error"));
8642        assert!(
8643            err.notes.iter().any(|n| n.contains("{")),
8644            "expected a braced-form suggestion note, got: {:?}",
8645            err.notes
8646        );
8647    }
8648
8649    /// A braced import (`use core.error.{Error}`, `ImportItems::Named`) is NOT
8650    /// rejected.
8651    #[test]
8652    fn braced_import_not_rejected() {
8653        let gen = NodeIdGen::new();
8654        let mut checker = TypeChecker::new();
8655        let named = bock_ast::ImportItems::Named(vec![bock_ast::ImportedName {
8656            name: bock_ast::Ident {
8657                name: "Error".to_string(),
8658                span: bock_errors::Span {
8659                    file: bock_errors::FileId(0),
8660                    start: 0,
8661                    end: 0,
8662                },
8663            },
8664            alias: None,
8665            span: bock_errors::Span {
8666                file: bock_errors::FileId(0),
8667                start: 0,
8668                end: 0,
8669            },
8670        }]);
8671        let mut module = module_with_import(&gen, &["core", "error"], named);
8672        checker.check_module(&mut module);
8673        assert!(
8674            !checker.diags.iter().any(|d| d.code == E_BARE_MODULE_IMPORT),
8675            "a braced import must not be rejected"
8676        );
8677    }
8678
8679    /// A wildcard import (`use core.error.*`, `ImportItems::Glob`) is NOT
8680    /// rejected.
8681    #[test]
8682    fn wildcard_import_not_rejected() {
8683        let gen = NodeIdGen::new();
8684        let mut checker = TypeChecker::new();
8685        let mut module = module_with_import(&gen, &["core", "error"], bock_ast::ImportItems::Glob);
8686        checker.check_module(&mut module);
8687        assert!(
8688            !checker.diags.iter().any(|d| d.code == E_BARE_MODULE_IMPORT),
8689            "a wildcard import must not be rejected"
8690        );
8691    }
8692
8693    // ── Interpolation ──────────────────────────────────────────────────────
8694
8695    #[test]
8696    fn infer_interpolation_is_string() {
8697        let gen = NodeIdGen::new();
8698        let mut checker = TypeChecker::new();
8699        let node = make_node(
8700            &gen,
8701            NodeKind::Interpolation {
8702                parts: vec![
8703                    bock_air::AirInterpolationPart::Literal("hello ".into()),
8704                    bock_air::AirInterpolationPart::Expr(Box::new(int_lit(&gen))),
8705                ],
8706            },
8707        );
8708        let ty = checker.infer_expr(&node);
8709        assert_eq!(ty, Type::Primitive(PrimitiveType::String));
8710    }
8711
8712    // ── Unreachable / Never ────────────────────────────────────────────────
8713
8714    #[test]
8715    fn infer_unreachable_is_never() {
8716        let gen = NodeIdGen::new();
8717        let mut checker = TypeChecker::new();
8718        let node = make_node(&gen, NodeKind::Unreachable);
8719        let ty = checker.infer_expr(&node);
8720        assert_eq!(ty, Type::Primitive(PrimitiveType::Never));
8721    }
8722
8723    // ── check_module with a simple function ──────────────────────────────
8724
8725    #[test]
8726    fn check_module_simple_fn() {
8727        let gen = NodeIdGen::new();
8728        let mut checker = TypeChecker::new();
8729
8730        // fn add(x: Int, y: Int) -> Int { x + y }
8731        let x_pat = make_node(
8732            &gen,
8733            NodeKind::BindPat {
8734                name: ident("x"),
8735                is_mut: false,
8736            },
8737        );
8738        let y_pat = make_node(
8739            &gen,
8740            NodeKind::BindPat {
8741                name: ident("y"),
8742                is_mut: false,
8743            },
8744        );
8745
8746        let int_ty = type_named_node(&gen, "Int");
8747
8748        let x_param = make_node(
8749            &gen,
8750            NodeKind::Param {
8751                pattern: Box::new(x_pat),
8752                ty: Some(Box::new(int_ty.clone())),
8753                default: None,
8754            },
8755        );
8756        let y_param = make_node(
8757            &gen,
8758            NodeKind::Param {
8759                pattern: Box::new(y_pat),
8760                ty: Some(Box::new(int_ty.clone())),
8761                default: None,
8762            },
8763        );
8764
8765        let x_ref = make_node(&gen, NodeKind::Identifier { name: ident("x") });
8766        let y_ref = make_node(&gen, NodeKind::Identifier { name: ident("y") });
8767        let add_expr = make_node(
8768            &gen,
8769            NodeKind::BinaryOp {
8770                op: BinOp::Add,
8771                left: Box::new(x_ref),
8772                right: Box::new(y_ref),
8773            },
8774        );
8775
8776        let body = make_node(
8777            &gen,
8778            NodeKind::Block {
8779                stmts: vec![],
8780                tail: Some(Box::new(add_expr)),
8781            },
8782        );
8783
8784        let ret_ty = type_named_node(&gen, "Int");
8785
8786        let fn_node = make_node(
8787            &gen,
8788            NodeKind::FnDecl {
8789                annotations: vec![],
8790                visibility: bock_ast::Visibility::Public,
8791                is_async: false,
8792                name: ident("add"),
8793                generic_params: vec![],
8794                params: vec![x_param, y_param],
8795                return_type: Some(Box::new(ret_ty)),
8796                effect_clause: vec![],
8797                where_clause: vec![],
8798                body: Box::new(body),
8799            },
8800        );
8801
8802        let mut module = make_node(
8803            &gen,
8804            NodeKind::Module {
8805                path: None,
8806                annotations: vec![],
8807                imports: vec![],
8808                items: vec![fn_node],
8809            },
8810        );
8811
8812        checker.check_module(&mut module);
8813        assert!(
8814            !checker.diags.has_errors(),
8815            "errors: {:?}",
8816            checker.diags.iter().collect::<Vec<_>>()
8817        );
8818    }
8819
8820    // ── impl-method `Self` substitution (Q-self-subst) ───────────────────
8821
8822    /// Build an `impl <target> { <method>(self, <extra params>) -> <ret> }`
8823    /// AIR module node and return it ready for `collect_sig`.
8824    ///
8825    /// `extra_params` are `(name, type_node)` pairs appended after the
8826    /// untyped `self`; `ret` is the method's return-type node.
8827    fn impl_with_method(
8828        gen: &NodeIdGen,
8829        target: &str,
8830        method: &str,
8831        extra_params: Vec<(&str, AIRNode)>,
8832        ret: AIRNode,
8833    ) -> AIRNode {
8834        let self_pat = make_node(
8835            gen,
8836            NodeKind::BindPat {
8837                name: ident("self"),
8838                is_mut: false,
8839            },
8840        );
8841        let self_param = make_node(
8842            gen,
8843            NodeKind::Param {
8844                pattern: Box::new(self_pat),
8845                ty: None,
8846                default: None,
8847            },
8848        );
8849        let mut params = vec![self_param];
8850        for (pname, pty) in extra_params {
8851            let pat = make_node(
8852                gen,
8853                NodeKind::BindPat {
8854                    name: ident(pname),
8855                    is_mut: false,
8856                },
8857            );
8858            params.push(make_node(
8859                gen,
8860                NodeKind::Param {
8861                    pattern: Box::new(pat),
8862                    ty: Some(Box::new(pty)),
8863                    default: None,
8864                },
8865            ));
8866        }
8867        let body = make_node(
8868            gen,
8869            NodeKind::Block {
8870                stmts: vec![],
8871                tail: None,
8872            },
8873        );
8874        let method_node = make_node(
8875            gen,
8876            NodeKind::FnDecl {
8877                annotations: vec![],
8878                visibility: bock_ast::Visibility::Public,
8879                is_async: false,
8880                name: ident(method),
8881                generic_params: vec![],
8882                params,
8883                return_type: Some(Box::new(ret)),
8884                effect_clause: vec![],
8885                where_clause: vec![],
8886                body: Box::new(body),
8887            },
8888        );
8889        make_node(
8890            gen,
8891            NodeKind::ImplBlock {
8892                annotations: vec![],
8893                generic_params: vec![],
8894                trait_path: None,
8895                trait_args: vec![],
8896                target: Box::new(type_named_node(gen, target)),
8897                where_clause: vec![],
8898                methods: vec![method_node],
8899            },
8900        )
8901    }
8902
8903    /// `impl Doubler { fn double(self) -> Self }` must register `double` with a
8904    /// concrete `Doubler` *return* type, not the un-substituted `Named("Self")`
8905    /// that previously leaked to call sites as E4001.
8906    #[test]
8907    fn impl_method_self_in_return_is_substituted() {
8908        let gen = NodeIdGen::new();
8909        let mut checker = TypeChecker::new();
8910
8911        let self_ret = make_node(&gen, NodeKind::TypeSelf);
8912        let impl_node = impl_with_method(&gen, "Doubler", "double", vec![], self_ret);
8913        checker.collect_sig(&impl_node);
8914
8915        let method_ty = checker
8916            .method_types
8917            .get("Doubler")
8918            .and_then(|m| m.get("double"))
8919            .expect("double should be registered on Doubler");
8920        let Type::Function(fn_ty) = method_ty else {
8921            panic!("expected a function type, got {method_ty:?}");
8922        };
8923        // `self` param resolves to the target type, and `-> Self` is now the
8924        // concrete target — no residual `Named("Self")` anywhere.
8925        let doubler = Type::Named(crate::NamedType {
8926            name: "Doubler".into(),
8927        });
8928        assert_eq!(*fn_ty.ret, doubler, "return `Self` should become Doubler");
8929        assert_eq!(fn_ty.params, vec![doubler]);
8930    }
8931
8932    /// `impl Counter { fn combine(self, other: Self) -> Int }` must register
8933    /// `combine` with the `other` *parameter* typed as the concrete target,
8934    /// not the un-substituted `Named("Self")`.
8935    #[test]
8936    fn impl_method_self_in_param_is_substituted() {
8937        let gen = NodeIdGen::new();
8938        let mut checker = TypeChecker::new();
8939
8940        let other_ty = make_node(&gen, NodeKind::TypeSelf);
8941        let int_ret = type_named_node(&gen, "Int");
8942        let impl_node = impl_with_method(
8943            &gen,
8944            "Counter",
8945            "combine",
8946            vec![("other", other_ty)],
8947            int_ret,
8948        );
8949        checker.collect_sig(&impl_node);
8950
8951        let method_ty = checker
8952            .method_types
8953            .get("Counter")
8954            .and_then(|m| m.get("combine"))
8955            .expect("combine should be registered on Counter");
8956        let Type::Function(fn_ty) = method_ty else {
8957            panic!("expected a function type, got {method_ty:?}");
8958        };
8959        let counter = Type::Named(crate::NamedType {
8960            name: "Counter".into(),
8961        });
8962        // params: [self -> Counter, other: Self -> Counter]
8963        assert_eq!(fn_ty.params, vec![counter.clone(), counter]);
8964        assert_eq!(*fn_ty.ret, Type::Primitive(PrimitiveType::Int));
8965    }
8966
8967    // ── impl/class method-body checking (Q-impl-body-typecheck) ───────────
8968
8969    /// Build `impl <target> { fn <method>(self) -> <ret> { <tail> } }`, i.e. an
8970    /// inherent impl whose single method has a real (non-empty) body. Used to
8971    /// exercise the body-checking pass that `check_item` now performs.
8972    fn impl_with_bodied_method(
8973        gen: &NodeIdGen,
8974        target: &str,
8975        method: &str,
8976        ret: AIRNode,
8977        tail: AIRNode,
8978    ) -> AIRNode {
8979        let self_pat = make_node(
8980            gen,
8981            NodeKind::BindPat {
8982                name: ident("self"),
8983                is_mut: false,
8984            },
8985        );
8986        let self_param = make_node(
8987            gen,
8988            NodeKind::Param {
8989                pattern: Box::new(self_pat),
8990                ty: None,
8991                default: None,
8992            },
8993        );
8994        let body = make_node(
8995            gen,
8996            NodeKind::Block {
8997                stmts: vec![],
8998                tail: Some(Box::new(tail)),
8999            },
9000        );
9001        let method_node = make_node(
9002            gen,
9003            NodeKind::FnDecl {
9004                annotations: vec![],
9005                visibility: bock_ast::Visibility::Public,
9006                is_async: false,
9007                name: ident(method),
9008                generic_params: vec![],
9009                params: vec![self_param],
9010                return_type: Some(Box::new(ret)),
9011                effect_clause: vec![],
9012                where_clause: vec![],
9013                body: Box::new(body),
9014            },
9015        );
9016        make_node(
9017            gen,
9018            NodeKind::ImplBlock {
9019                annotations: vec![],
9020                generic_params: vec![],
9021                trait_path: None,
9022                trait_args: vec![],
9023                target: Box::new(type_named_node(gen, target)),
9024                where_clause: vec![],
9025                methods: vec![method_node],
9026            },
9027        )
9028    }
9029
9030    /// A method body whose tail expression's type disagrees with the declared
9031    /// return type must now be reported — before the fix, `check_item` skipped
9032    /// `ImplBlock`, so the body was never walked and the mismatch was silent.
9033    #[test]
9034    fn impl_method_body_type_error_is_reported() {
9035        let gen = NodeIdGen::new();
9036        let mut checker = TypeChecker::new();
9037
9038        // impl Widget { fn id(self) -> Int { "hello" } }  — String vs Int.
9039        let ret = type_named_node(&gen, "Int");
9040        let tail = str_lit(&gen);
9041        let impl_node = impl_with_bodied_method(&gen, "Widget", "id", ret, tail);
9042        let mut module = make_node(
9043            &gen,
9044            NodeKind::Module {
9045                path: None,
9046                annotations: vec![],
9047                imports: vec![],
9048                items: vec![impl_node],
9049            },
9050        );
9051
9052        checker.check_module(&mut module);
9053        assert!(
9054            checker.diags.has_errors(),
9055            "expected a method-body type error, got none"
9056        );
9057    }
9058
9059    /// A well-typed method body must still check clean (no false positive).
9060    #[test]
9061    fn impl_method_body_well_typed_is_clean() {
9062        let gen = NodeIdGen::new();
9063        let mut checker = TypeChecker::new();
9064
9065        // impl Widget { fn name(self) -> String { "hello" } }
9066        let ret = type_named_node(&gen, "String");
9067        let tail = str_lit(&gen);
9068        let impl_node = impl_with_bodied_method(&gen, "Widget", "name", ret, tail);
9069        let mut module = make_node(
9070            &gen,
9071            NodeKind::Module {
9072                path: None,
9073                annotations: vec![],
9074                imports: vec![],
9075                items: vec![impl_node],
9076            },
9077        );
9078
9079        checker.check_module(&mut module);
9080        assert!(
9081            !checker.diags.has_errors(),
9082            "well-typed method body should not error: {:?}",
9083            checker.diags.iter().collect::<Vec<_>>()
9084        );
9085    }
9086
9087    /// A getter method whose name matches a record field (`fn message(self) ->
9088    /// String { self.message }`) must read the *field* in value position, not
9089    /// resolve `self.message` to the method's own function type (which would be
9090    /// `Fn(Self) -> String`, mismatching the `-> String` return). This is the
9091    /// `core.error` shape the body-checking pass first surfaced; the
9092    /// `FieldAccess` handler now prefers the same-named field.
9093    #[test]
9094    fn impl_getter_named_like_field_reads_the_field() {
9095        let gen = NodeIdGen::new();
9096        let mut checker = TypeChecker::new();
9097
9098        // record Err { message: String }
9099        let field = bock_ast::RecordDeclField {
9100            id: gen.next(),
9101            span: span(),
9102            name: ident("message"),
9103            ty: TypeExpr::Named {
9104                id: gen.next(),
9105                span: span(),
9106                path: TypePath {
9107                    segments: vec![ident("String")],
9108                    span: span(),
9109                },
9110                args: vec![],
9111            },
9112            default: None,
9113        };
9114        let record_node = make_node(
9115            &gen,
9116            NodeKind::RecordDecl {
9117                annotations: vec![],
9118                visibility: bock_ast::Visibility::Public,
9119                name: ident("Err"),
9120                generic_params: vec![],
9121                fields: vec![field],
9122            },
9123        );
9124
9125        // impl Err { fn message(self) -> String { self.message } }
9126        let self_ref = make_node(
9127            &gen,
9128            NodeKind::Identifier {
9129                name: ident("self"),
9130            },
9131        );
9132        let field_access = make_node(
9133            &gen,
9134            NodeKind::FieldAccess {
9135                object: Box::new(self_ref),
9136                field: ident("message"),
9137            },
9138        );
9139        let ret = type_named_node(&gen, "String");
9140        let impl_node = impl_with_bodied_method(&gen, "Err", "message", ret, field_access);
9141
9142        let mut module = make_node(
9143            &gen,
9144            NodeKind::Module {
9145                path: None,
9146                annotations: vec![],
9147                imports: vec![],
9148                items: vec![record_node, impl_node],
9149            },
9150        );
9151
9152        checker.check_module(&mut module);
9153        assert!(
9154            !checker.diags.has_errors(),
9155            "field-named getter should read the field, not the method: {:?}",
9156            checker.diags.iter().collect::<Vec<_>>()
9157        );
9158    }
9159
9160    // ── check_mode: lambda from context ──────────────────────────────────
9161
9162    #[test]
9163    fn check_lambda_from_context() {
9164        let gen = NodeIdGen::new();
9165        let mut checker = TypeChecker::new();
9166
9167        // let f: Fn(Int) -> Int = (x) => x + 1
9168        let x_pat = make_node(
9169            &gen,
9170            NodeKind::BindPat {
9171                name: ident("x"),
9172                is_mut: false,
9173            },
9174        );
9175        let x_param = make_node(
9176            &gen,
9177            NodeKind::Param {
9178                pattern: Box::new(x_pat),
9179                ty: None,
9180                default: None,
9181            },
9182        );
9183        let x_ref = make_node(&gen, NodeKind::Identifier { name: ident("x") });
9184        let one = make_node(
9185            &gen,
9186            NodeKind::Literal {
9187                lit: Literal::Int("1".into()),
9188            },
9189        );
9190        let body = make_node(
9191            &gen,
9192            NodeKind::BinaryOp {
9193                op: BinOp::Add,
9194                left: Box::new(x_ref),
9195                right: Box::new(one),
9196            },
9197        );
9198
9199        let lambda = make_node(
9200            &gen,
9201            NodeKind::Lambda {
9202                params: vec![x_param],
9203                body: Box::new(body),
9204            },
9205        );
9206
9207        let expected = Type::Function(FnType {
9208            params: vec![Type::Primitive(PrimitiveType::Int)],
9209            ret: Box::new(Type::Primitive(PrimitiveType::Int)),
9210            effects: vec![],
9211        });
9212
9213        checker.check_expr(&lambda, &expected);
9214        assert!(!checker.diags.has_errors());
9215    }
9216
9217    // ── Error propagation: Type::Error unifies with anything ──────────────
9218
9219    #[test]
9220    fn error_type_prevents_cascade() {
9221        let gen = NodeIdGen::new();
9222        let mut checker = TypeChecker::new();
9223
9224        // undefined + 1  → Error + Int = Error (no cascade error from second op)
9225        let undef = make_node(
9226            &gen,
9227            NodeKind::Identifier {
9228                name: ident("undefined_var"),
9229            },
9230        );
9231        let one = int_lit(&gen);
9232        let add = make_node(
9233            &gen,
9234            NodeKind::BinaryOp {
9235                op: BinOp::Add,
9236                left: Box::new(undef),
9237                right: Box::new(one),
9238            },
9239        );
9240        let ty = checker.infer_expr(&add);
9241        // Should have exactly 1 error (the undefined var), not 2.
9242        assert_eq!(checker.diags.error_count(), 1);
9243        assert_eq!(ty, Type::Error);
9244    }
9245
9246    // ── where_clause verification ─────────────────────────────────────────
9247
9248    #[test]
9249    fn where_clause_unknown_param_emits_error() {
9250        let mut checker = TypeChecker::new();
9251        let clauses = vec![TypeConstraint {
9252            id: 0,
9253            span: span(),
9254            param: ident("X"), // not in generic_params
9255            bounds: vec![TypePath {
9256                segments: vec![ident("Equatable")],
9257                span: span(),
9258            }],
9259        }];
9260        checker.check_where_clause(&clauses, &HashMap::new(), span());
9261        assert!(checker.diags.has_errors());
9262    }
9263
9264    // ── Result / Optional annotation unification (F2.06) ─────────────────
9265
9266    fn type_named_node_with_args(gen: &NodeIdGen, name: &str, args: Vec<AIRNode>) -> AIRNode {
9267        make_node(
9268            gen,
9269            NodeKind::TypeNamed {
9270                path: TypePath {
9271                    segments: vec![ident(name)],
9272                    span: span(),
9273                },
9274                args,
9275            },
9276        )
9277    }
9278
9279    #[test]
9280    fn result_annotation_produces_type_result() {
9281        let gen = NodeIdGen::new();
9282        let mut checker = TypeChecker::new();
9283        let int_node = type_named_node(&gen, "Int");
9284        let string_node = type_named_node(&gen, "String");
9285        let result_node = type_named_node_with_args(&gen, "Result", vec![int_node, string_node]);
9286        let ty = checker.air_type_node_to_type(&result_node, &HashMap::new());
9287        assert_eq!(
9288            ty,
9289            Type::Result(
9290                Box::new(Type::Primitive(PrimitiveType::Int)),
9291                Box::new(Type::Primitive(PrimitiveType::String)),
9292            )
9293        );
9294    }
9295
9296    #[test]
9297    fn optional_annotation_produces_type_optional() {
9298        let gen = NodeIdGen::new();
9299        let mut checker = TypeChecker::new();
9300        let int_node = type_named_node(&gen, "Int");
9301        let optional_node = type_named_node_with_args(&gen, "Optional", vec![int_node]);
9302        let ty = checker.air_type_node_to_type(&optional_node, &HashMap::new());
9303        assert_eq!(
9304            ty,
9305            Type::Optional(Box::new(Type::Primitive(PrimitiveType::Int)))
9306        );
9307    }
9308
9309    #[test]
9310    fn result_annotation_unifies_with_ok_construction() {
9311        // Result[Int, String] from annotation must unify with
9312        // Type::Result(Int, ?E) from Ok(42)
9313        let annotated = Type::Result(
9314            Box::new(Type::Primitive(PrimitiveType::Int)),
9315            Box::new(Type::Primitive(PrimitiveType::String)),
9316        );
9317        let constructed = Type::Result(
9318            Box::new(Type::Primitive(PrimitiveType::Int)),
9319            Box::new(Type::TypeVar(99)),
9320        );
9321        let mut subst = crate::Substitution::new();
9322        assert!(crate::unify(&annotated, &constructed, &mut subst).is_ok());
9323        assert_eq!(subst.lookup(99), Type::Primitive(PrimitiveType::String));
9324    }
9325
9326    #[test]
9327    fn optional_annotation_unifies_with_some_construction() {
9328        // Optional[Int] from annotation must unify with
9329        // Type::Optional(Int) from Some(5)
9330        let annotated = Type::Optional(Box::new(Type::Primitive(PrimitiveType::Int)));
9331        let constructed = Type::Optional(Box::new(Type::Primitive(PrimitiveType::Int)));
9332        let mut subst = crate::Substitution::new();
9333        assert!(crate::unify(&annotated, &constructed, &mut subst).is_ok());
9334    }
9335
9336    // ── Trait-bound enforcement at call sites (F2.08) ────────────────────
9337
9338    /// Helper: register a generic function with where-clause bounds.
9339    fn register_generic_fn_with_bounds(
9340        checker: &mut TypeChecker,
9341        name: &str,
9342        generic_names: &[&str],
9343        bounds: Vec<TypeConstraint>,
9344        build_sig: impl FnOnce(&[Type]) -> (Vec<Type>, Type),
9345    ) {
9346        let vars: Vec<Type> = generic_names.iter().map(|_| checker.fresh_var()).collect();
9347        let var_ids: Vec<TypeVarId> = vars
9348            .iter()
9349            .map(|t| match t {
9350                Type::TypeVar(id) => *id,
9351                _ => unreachable!(),
9352            })
9353            .collect();
9354        let (param_types, return_type) = build_sig(&vars);
9355        let fn_ty = Type::Function(FnType {
9356            params: param_types.clone(),
9357            ret: Box::new(return_type.clone()),
9358            effects: vec![],
9359        });
9360        checker.env.define(name, fn_ty);
9361        checker.fn_sigs.insert(
9362            name.into(),
9363            FnSig {
9364                generic_params: generic_names.iter().map(|s| (*s).into()).collect(),
9365                generic_var_ids: var_ids,
9366                param_types,
9367                return_type,
9368                where_clause: bounds,
9369            },
9370        );
9371    }
9372
9373    /// Build a `TypeConstraint` for `param: Bound1 + Bound2 + ...`.
9374    fn make_constraint(param: &str, bound_names: &[&str]) -> TypeConstraint {
9375        use bock_ast::TypeConstraint;
9376        TypeConstraint {
9377            id: 0,
9378            span: span(),
9379            param: ident(param),
9380            bounds: bound_names
9381                .iter()
9382                .map(|b| TypePath {
9383                    segments: vec![ident(b)],
9384                    span: span(),
9385                })
9386                .collect(),
9387        }
9388    }
9389
9390    /// Build an `ImplTable` with specific (trait, type) registrations.
9391    fn make_impl_table(impls: &[(&str, Type)]) -> ImplTable {
9392        let mut table = ImplTable::new();
9393        for (trait_name, ty) in impls {
9394            table.register_trait_impl(*trait_name, ty);
9395        }
9396        table
9397    }
9398
9399    #[test]
9400    fn trait_bound_satisfied_no_error() {
9401        // fn sort[T](list: List[T]) -> List[T] where (T: Comparable)
9402        // Calling sort([1, 2, 3]) with Int implementing Comparable — no error.
9403        let gen = NodeIdGen::new();
9404        let mut checker = TypeChecker::new();
9405
9406        // Set up impl table: Int implements Comparable.
9407        checker.impl_table = Some(make_impl_table(&[(
9408            "Comparable",
9409            Type::Primitive(PrimitiveType::Int),
9410        )]));
9411
9412        let bounds = vec![make_constraint("T", &["Comparable"])];
9413        register_generic_fn_with_bounds(&mut checker, "sort", &["T"], bounds, |vars| {
9414            let t = vars[0].clone();
9415            let list_t = Type::Generic(GenericType {
9416                constructor: "List".into(),
9417                args: vec![t.clone()],
9418            });
9419            (vec![list_t.clone()], list_t)
9420        });
9421
9422        let callee = make_node(
9423            &gen,
9424            NodeKind::Identifier {
9425                name: ident("sort"),
9426            },
9427        );
9428        let list_arg = make_node(
9429            &gen,
9430            NodeKind::ListLiteral {
9431                elems: vec![int_lit(&gen), int_lit(&gen)],
9432            },
9433        );
9434        let call = make_node(
9435            &gen,
9436            NodeKind::Call {
9437                callee: Box::new(callee),
9438                type_args: vec![],
9439                args: vec![bock_air::AirArg {
9440                    label: None,
9441                    value: list_arg,
9442                }],
9443            },
9444        );
9445
9446        checker.infer_expr(&call);
9447        assert!(
9448            !checker.diags.has_errors(),
9449            "expected no errors for Int: Comparable"
9450        );
9451    }
9452
9453    #[test]
9454    fn trait_bound_violated_emits_diagnostic() {
9455        // fn sort[T](list: List[T]) -> List[T] where (T: Comparable)
9456        // Calling sort with a Bool list — Bool does NOT implement Comparable.
9457        let gen = NodeIdGen::new();
9458        let mut checker = TypeChecker::new();
9459
9460        // Impl table: only Int implements Comparable (not Bool).
9461        checker.impl_table = Some(make_impl_table(&[(
9462            "Comparable",
9463            Type::Primitive(PrimitiveType::Int),
9464        )]));
9465
9466        let bounds = vec![make_constraint("T", &["Comparable"])];
9467        register_generic_fn_with_bounds(&mut checker, "sort", &["T"], bounds, |vars| {
9468            let t = vars[0].clone();
9469            let list_t = Type::Generic(GenericType {
9470                constructor: "List".into(),
9471                args: vec![t.clone()],
9472            });
9473            (vec![list_t.clone()], list_t)
9474        });
9475
9476        let callee = make_node(
9477            &gen,
9478            NodeKind::Identifier {
9479                name: ident("sort"),
9480            },
9481        );
9482        let list_arg = make_node(
9483            &gen,
9484            NodeKind::ListLiteral {
9485                elems: vec![bool_lit(&gen, true), bool_lit(&gen, false)],
9486            },
9487        );
9488        let call = make_node(
9489            &gen,
9490            NodeKind::Call {
9491                callee: Box::new(callee),
9492                type_args: vec![],
9493                args: vec![bock_air::AirArg {
9494                    label: None,
9495                    value: list_arg,
9496                }],
9497            },
9498        );
9499
9500        checker.infer_expr(&call);
9501        assert!(
9502            checker.diags.has_errors(),
9503            "expected error: Bool does not implement Comparable"
9504        );
9505        assert_eq!(checker.diags.error_count(), 1);
9506    }
9507
9508    #[test]
9509    fn multiple_trait_bounds_both_satisfied() {
9510        // fn display_sorted[T](list: List[T]) -> Void
9511        //   where (T: Comparable, T: Displayable)
9512        // Call with Int — Int implements both.
9513        let gen = NodeIdGen::new();
9514        let mut checker = TypeChecker::new();
9515
9516        checker.impl_table = Some(make_impl_table(&[
9517            ("Comparable", Type::Primitive(PrimitiveType::Int)),
9518            ("Displayable", Type::Primitive(PrimitiveType::Int)),
9519        ]));
9520
9521        let bounds = vec![make_constraint("T", &["Comparable", "Displayable"])];
9522        register_generic_fn_with_bounds(&mut checker, "display_sorted", &["T"], bounds, |vars| {
9523            let t = vars[0].clone();
9524            let list_t = Type::Generic(GenericType {
9525                constructor: "List".into(),
9526                args: vec![t],
9527            });
9528            (vec![list_t], Type::Primitive(PrimitiveType::Void))
9529        });
9530
9531        let callee = make_node(
9532            &gen,
9533            NodeKind::Identifier {
9534                name: ident("display_sorted"),
9535            },
9536        );
9537        let list_arg = make_node(
9538            &gen,
9539            NodeKind::ListLiteral {
9540                elems: vec![int_lit(&gen)],
9541            },
9542        );
9543        let call = make_node(
9544            &gen,
9545            NodeKind::Call {
9546                callee: Box::new(callee),
9547                type_args: vec![],
9548                args: vec![bock_air::AirArg {
9549                    label: None,
9550                    value: list_arg,
9551                }],
9552            },
9553        );
9554
9555        checker.infer_expr(&call);
9556        assert!(
9557            !checker.diags.has_errors(),
9558            "expected no errors: Int satisfies both bounds"
9559        );
9560    }
9561
9562    #[test]
9563    fn multiple_trait_bounds_one_missing() {
9564        // fn display_sorted[T](list: List[T]) -> Void
9565        //   where (T: Comparable, T: Displayable)
9566        // Call with Int — Int implements Comparable but NOT Displayable.
9567        let gen = NodeIdGen::new();
9568        let mut checker = TypeChecker::new();
9569
9570        // Only Comparable is registered for Int.
9571        checker.impl_table = Some(make_impl_table(&[(
9572            "Comparable",
9573            Type::Primitive(PrimitiveType::Int),
9574        )]));
9575
9576        let bounds = vec![make_constraint("T", &["Comparable", "Displayable"])];
9577        register_generic_fn_with_bounds(&mut checker, "display_sorted", &["T"], bounds, |vars| {
9578            let t = vars[0].clone();
9579            let list_t = Type::Generic(GenericType {
9580                constructor: "List".into(),
9581                args: vec![t],
9582            });
9583            (vec![list_t], Type::Primitive(PrimitiveType::Void))
9584        });
9585
9586        let callee = make_node(
9587            &gen,
9588            NodeKind::Identifier {
9589                name: ident("display_sorted"),
9590            },
9591        );
9592        let list_arg = make_node(
9593            &gen,
9594            NodeKind::ListLiteral {
9595                elems: vec![int_lit(&gen)],
9596            },
9597        );
9598        let call = make_node(
9599            &gen,
9600            NodeKind::Call {
9601                callee: Box::new(callee),
9602                type_args: vec![],
9603                args: vec![bock_air::AirArg {
9604                    label: None,
9605                    value: list_arg,
9606                }],
9607            },
9608        );
9609
9610        checker.infer_expr(&call);
9611        assert!(
9612            checker.diags.has_errors(),
9613            "expected error: Int missing Displayable"
9614        );
9615        assert_eq!(checker.diags.error_count(), 1);
9616    }
9617
9618    #[test]
9619    fn no_impl_table_skips_bound_checking() {
9620        // Without an impl_table, trait bounds should not be checked.
9621        let gen = NodeIdGen::new();
9622        let mut checker = TypeChecker::new();
9623        // impl_table is None by default.
9624
9625        let bounds = vec![make_constraint("T", &["Comparable"])];
9626        register_generic_fn_with_bounds(&mut checker, "sort", &["T"], bounds, |vars| {
9627            let t = vars[0].clone();
9628            (vec![t.clone()], t)
9629        });
9630
9631        let callee = make_node(
9632            &gen,
9633            NodeKind::Identifier {
9634                name: ident("sort"),
9635            },
9636        );
9637        let call = make_node(
9638            &gen,
9639            NodeKind::Call {
9640                callee: Box::new(callee),
9641                type_args: vec![],
9642                args: vec![bock_air::AirArg {
9643                    label: None,
9644                    value: int_lit(&gen),
9645                }],
9646            },
9647        );
9648
9649        checker.infer_expr(&call);
9650        // No impl_table → no bound-check errors.
9651        assert!(!checker.diags.has_errors());
9652    }
9653
9654    // ── M-064: Char literal inference ─────────────────────────────────────
9655
9656    #[test]
9657    fn infer_char_literal() {
9658        let gen = NodeIdGen::new();
9659        let mut checker = TypeChecker::new();
9660        let node = make_node(
9661            &gen,
9662            NodeKind::Literal {
9663                lit: Literal::Char("a".into()),
9664            },
9665        );
9666        let ty = checker.infer_expr(&node);
9667        assert_eq!(ty, Type::Primitive(PrimitiveType::Char));
9668    }
9669
9670    // ── M-063: Function types carry effects ───────────────────────────────
9671
9672    #[test]
9673    fn fn_type_carries_effects() {
9674        let gen = NodeIdGen::new();
9675        let mut checker = TypeChecker::new();
9676
9677        // Build a FnDecl with effect_clause: [Log, Clock]
9678        let body = make_node(
9679            &gen,
9680            NodeKind::Block {
9681                stmts: vec![],
9682                tail: None,
9683            },
9684        );
9685        let fn_decl = make_node(
9686            &gen,
9687            NodeKind::FnDecl {
9688                annotations: vec![],
9689                visibility: bock_ast::Visibility::Public,
9690                is_async: false,
9691                name: ident("greet"),
9692                generic_params: vec![],
9693                params: vec![],
9694                return_type: None,
9695                effect_clause: vec![
9696                    TypePath {
9697                        segments: vec![ident("Log")],
9698                        span: span(),
9699                    },
9700                    TypePath {
9701                        segments: vec![ident("Clock")],
9702                        span: span(),
9703                    },
9704                ],
9705                where_clause: vec![],
9706                body: Box::new(body),
9707            },
9708        );
9709
9710        let module = make_node(
9711            &gen,
9712            NodeKind::Module {
9713                path: None,
9714                annotations: vec![],
9715                imports: vec![],
9716                items: vec![fn_decl],
9717            },
9718        );
9719
9720        let mut module = module;
9721        checker.check_module(&mut module);
9722
9723        // Look up the function type and verify effects are present.
9724        let fn_ty = checker
9725            .env
9726            .lookup("greet")
9727            .expect("greet should be defined");
9728        match fn_ty {
9729            Type::Function(f) => {
9730                assert_eq!(f.effects.len(), 2);
9731                assert_eq!(f.effects[0].name, "Log");
9732                assert_eq!(f.effects[1].name, "Clock");
9733            }
9734            other => panic!("expected Function type, got {other:?}"),
9735        }
9736    }
9737
9738    // ── M-067: Method calls on known types return correct types ───────────
9739
9740    #[test]
9741    fn method_call_float_abs_returns_float() {
9742        let gen = NodeIdGen::new();
9743        let mut checker = TypeChecker::new();
9744        let receiver = float_lit(&gen);
9745        let method_call = make_node(
9746            &gen,
9747            NodeKind::MethodCall {
9748                receiver: Box::new(receiver),
9749                method: ident("abs"),
9750                type_args: vec![],
9751                args: vec![],
9752            },
9753        );
9754        let ty = checker.infer_expr(&method_call);
9755        assert_eq!(ty, Type::Primitive(PrimitiveType::Float));
9756        assert!(!checker.diags.has_errors());
9757    }
9758
9759    #[test]
9760    fn method_call_float_to_int_returns_int() {
9761        let gen = NodeIdGen::new();
9762        let mut checker = TypeChecker::new();
9763        let receiver = float_lit(&gen);
9764        let method_call = make_node(
9765            &gen,
9766            NodeKind::MethodCall {
9767                receiver: Box::new(receiver),
9768                method: ident("to_int"),
9769                type_args: vec![],
9770                args: vec![],
9771            },
9772        );
9773        let ty = checker.infer_expr(&method_call);
9774        assert_eq!(ty, Type::Primitive(PrimitiveType::Int));
9775    }
9776
9777    #[test]
9778    fn method_call_bool_negate_returns_bool() {
9779        let gen = NodeIdGen::new();
9780        let mut checker = TypeChecker::new();
9781        let receiver = bool_lit(&gen, true);
9782        let method_call = make_node(
9783            &gen,
9784            NodeKind::MethodCall {
9785                receiver: Box::new(receiver),
9786                method: ident("negate"),
9787                type_args: vec![],
9788                args: vec![],
9789            },
9790        );
9791        let ty = checker.infer_expr(&method_call);
9792        assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
9793    }
9794
9795    #[test]
9796    fn method_call_char_is_alpha_returns_bool() {
9797        let gen = NodeIdGen::new();
9798        let mut checker = TypeChecker::new();
9799        let receiver = make_node(
9800            &gen,
9801            NodeKind::Literal {
9802                lit: Literal::Char("a".into()),
9803            },
9804        );
9805        let method_call = make_node(
9806            &gen,
9807            NodeKind::MethodCall {
9808                receiver: Box::new(receiver),
9809                method: ident("is_alpha"),
9810                type_args: vec![],
9811                args: vec![],
9812            },
9813        );
9814        let ty = checker.infer_expr(&method_call);
9815        assert_eq!(ty, Type::Primitive(PrimitiveType::Bool));
9816    }
9817
9818    #[test]
9819    fn method_call_char_to_upper_returns_char() {
9820        let gen = NodeIdGen::new();
9821        let mut checker = TypeChecker::new();
9822        let receiver = make_node(
9823            &gen,
9824            NodeKind::Literal {
9825                lit: Literal::Char("a".into()),
9826            },
9827        );
9828        let method_call = make_node(
9829            &gen,
9830            NodeKind::MethodCall {
9831                receiver: Box::new(receiver),
9832                method: ident("to_upper"),
9833                type_args: vec![],
9834                args: vec![],
9835            },
9836        );
9837        let ty = checker.infer_expr(&method_call);
9838        assert_eq!(ty, Type::Primitive(PrimitiveType::Char));
9839    }
9840
9841    /// Q-checker-unknown-method-concrete: an unknown method on a *concrete*
9842    /// receiver (here `Int`) is now an `E4013` error — the soundness hole where
9843    /// it silently resolved to a fresh type variable is closed. The result type
9844    /// is still a fresh var for error recovery, but the diagnostic fires.
9845    #[test]
9846    fn method_call_unknown_method_on_concrete_errors() {
9847        let gen = NodeIdGen::new();
9848        let mut checker = TypeChecker::new();
9849        let receiver = int_lit(&gen);
9850        let method_call = make_node(
9851            &gen,
9852            NodeKind::MethodCall {
9853                receiver: Box::new(receiver),
9854                method: ident("nonexistent"),
9855                type_args: vec![],
9856                args: vec![],
9857            },
9858        );
9859        let _ = checker.infer_expr(&method_call);
9860        assert!(
9861            checker.diags.iter().any(|d| d.code == E_NO_SUCH_METHOD
9862                && d.message.contains("nonexistent")
9863                && d.message.contains("Int")),
9864            "unknown method on a concrete `Int` receiver must raise E4013"
9865        );
9866    }
9867
9868    /// The new check must NOT fire when the receiver is an unresolved inference
9869    /// variable — methods may resolve once it is unified, and §4.9 sketch-mode
9870    /// narrowing resolves aggressively by design.
9871    #[test]
9872    fn method_call_unknown_method_on_typevar_does_not_error() {
9873        let gen = NodeIdGen::new();
9874        let mut checker = TypeChecker::new();
9875        // A bare identifier with no binding yields a fresh var receiver in
9876        // inference; build a MethodCall whose receiver is an unresolved var via
9877        // a lambda parameter (inferred to a fresh var, never unified).
9878        let lambda = make_node(
9879            &gen,
9880            NodeKind::Lambda {
9881                params: vec![make_node(
9882                    &gen,
9883                    NodeKind::Param {
9884                        pattern: Box::new(make_node(
9885                            &gen,
9886                            NodeKind::BindPat {
9887                                name: ident("x"),
9888                                is_mut: false,
9889                            },
9890                        )),
9891                        ty: None,
9892                        default: None,
9893                    },
9894                )],
9895                body: Box::new(make_node(
9896                    &gen,
9897                    NodeKind::MethodCall {
9898                        receiver: Box::new(make_node(
9899                            &gen,
9900                            NodeKind::Identifier { name: ident("x") },
9901                        )),
9902                        method: ident("whatever"),
9903                        type_args: vec![],
9904                        args: vec![],
9905                    },
9906                )),
9907            },
9908        );
9909        let _ = checker.infer_expr(&lambda);
9910        assert!(
9911            !checker.diags.iter().any(|d| d.code == E_NO_SUCH_METHOD),
9912            "an unknown method on an unresolved type-var receiver must NOT error"
9913        );
9914    }
9915
9916    // ── Receiver-type annotation (checker → codegen) ─────────────────────────
9917
9918    #[test]
9919    fn recv_kind_tag_maps_each_category() {
9920        use crate::NamedType;
9921        assert_eq!(
9922            recv_kind_tag(&Type::Primitive(PrimitiveType::Int)).as_deref(),
9923            Some("Primitive:Int")
9924        );
9925        assert_eq!(
9926            recv_kind_tag(&Type::Primitive(PrimitiveType::Float)).as_deref(),
9927            Some("Primitive:Float")
9928        );
9929        assert_eq!(
9930            recv_kind_tag(&Type::Primitive(PrimitiveType::String)).as_deref(),
9931            Some("Primitive:String")
9932        );
9933        assert_eq!(
9934            recv_kind_tag(&Type::Optional(Box::new(Type::Primitive(
9935                PrimitiveType::Int
9936            ))))
9937            .as_deref(),
9938            Some("Optional")
9939        );
9940        assert_eq!(
9941            recv_kind_tag(&Type::Result(
9942                Box::new(Type::Primitive(PrimitiveType::Int)),
9943                Box::new(Type::Primitive(PrimitiveType::String)),
9944            ))
9945            .as_deref(),
9946            Some("Result")
9947        );
9948        assert_eq!(
9949            recv_kind_tag(&Type::Generic(GenericType {
9950                constructor: "List".into(),
9951                args: vec![Type::Primitive(PrimitiveType::Int)],
9952            }))
9953            .as_deref(),
9954            Some("List")
9955        );
9956        assert_eq!(
9957            recv_kind_tag(&Type::Named(NamedType {
9958                name: "Point".into(),
9959            }))
9960            .as_deref(),
9961            Some("User:Point")
9962        );
9963        // No tag for inference vars / function types.
9964        assert_eq!(recv_kind_tag(&Type::TypeVar(0)), None);
9965    }
9966
9967    /// Build the desugared method-call shape the lowerer produces for
9968    /// `recv.method(args)`: `Call { callee: FieldAccess(recv, method),
9969    /// args: [recv, ...args] }`. The receiver node is shared (same id) between
9970    /// the field-access object and the first (self) argument.
9971    fn desugared_method_call(
9972        gen: &NodeIdGen,
9973        receiver: AIRNode,
9974        method: &str,
9975        extra_args: Vec<AIRNode>,
9976    ) -> AIRNode {
9977        let field_access = make_node(
9978            gen,
9979            NodeKind::FieldAccess {
9980                object: Box::new(receiver.clone()),
9981                field: ident(method),
9982            },
9983        );
9984        let mut args = vec![bock_air::AirArg {
9985            label: None,
9986            value: receiver,
9987        }];
9988        for a in extra_args {
9989            args.push(bock_air::AirArg {
9990                label: None,
9991                value: a,
9992            });
9993        }
9994        make_node(
9995            gen,
9996            NodeKind::Call {
9997                callee: Box::new(field_access),
9998                type_args: vec![],
9999                args,
10000            },
10001        )
10002    }
10003
10004    /// Register a `Comparable { compare(self, Self) -> Ordering }` /
10005    /// `Equatable { eq(self, Self) -> Bool }` model + an `impl_table` granting
10006    /// the named primitive both conformances, mirroring the canonical
10007    /// primitive-bridge wiring.
10008    fn with_primitive_comparable(checker: &mut TypeChecker, prim: PrimitiveType) {
10009        let self_ty = Type::Named(crate::NamedType {
10010            name: "Self".into(),
10011        });
10012        let mut comparable = HashMap::new();
10013        comparable.insert(
10014            "compare".to_string(),
10015            Type::Function(FnType {
10016                params: vec![self_ty.clone(), self_ty.clone()],
10017                ret: Box::new(Type::Named(crate::NamedType {
10018                    name: "Ordering".into(),
10019                })),
10020                effects: vec![],
10021            }),
10022        );
10023        checker.insert_trait_method_types("Comparable".to_string(), comparable);
10024        let mut equatable = HashMap::new();
10025        equatable.insert(
10026            "eq".to_string(),
10027            Type::Function(FnType {
10028                params: vec![self_ty.clone(), self_ty.clone()],
10029                ret: Box::new(Type::Primitive(PrimitiveType::Bool)),
10030                effects: vec![],
10031            }),
10032        );
10033        checker.insert_trait_method_types("Equatable".to_string(), equatable);
10034        checker.impl_table = Some(make_impl_table(&[
10035            ("Comparable", Type::Primitive(prim.clone())),
10036            ("Equatable", Type::Primitive(prim)),
10037        ]));
10038    }
10039
10040    #[test]
10041    fn stamps_recv_kind_on_primitive_compare() {
10042        // (1).compare(2) → desugared Call. The checker resolves the receiver as
10043        // Int and stamps `recv_kind = "Primitive:Int"` on the call node.
10044        let gen = NodeIdGen::new();
10045        let mut checker = TypeChecker::new();
10046        with_primitive_comparable(&mut checker, PrimitiveType::Int);
10047
10048        let mut call = desugared_method_call(&gen, int_lit(&gen), "compare", vec![int_lit(&gen)]);
10049        let ty = checker.infer_node(&mut call);
10050        // Resolves to the trait's declared return (Ordering), not the intrinsic.
10051        assert_eq!(
10052            ty,
10053            Type::Named(crate::NamedType {
10054                name: "Ordering".into()
10055            })
10056        );
10057        assert_eq!(
10058            call.metadata.get(RECV_KIND_META_KEY),
10059            Some(&Value::String("Primitive:Int".to_string())),
10060            "expected recv_kind stamped on the compare call node"
10061        );
10062    }
10063
10064    #[test]
10065    fn stamps_recv_kind_on_primitive_eq_and_to_string() {
10066        let gen = NodeIdGen::new();
10067        let mut checker = TypeChecker::new();
10068        with_primitive_comparable(&mut checker, PrimitiveType::Int);
10069
10070        let mut eq_call = desugared_method_call(&gen, int_lit(&gen), "eq", vec![int_lit(&gen)]);
10071        checker.infer_node(&mut eq_call);
10072        assert_eq!(
10073            eq_call.metadata.get(RECV_KIND_META_KEY),
10074            Some(&Value::String("Primitive:Int".to_string())),
10075        );
10076
10077        // `.to_string()` is an intrinsic (not a trait method), but the receiver
10078        // kind is still stamped so codegen can lower it.
10079        let mut ts_call = desugared_method_call(&gen, int_lit(&gen), "to_string", vec![]);
10080        checker.infer_node(&mut ts_call);
10081        assert_eq!(
10082            ts_call.metadata.get(RECV_KIND_META_KEY),
10083            Some(&Value::String("Primitive:Int".to_string())),
10084        );
10085    }
10086
10087    #[test]
10088    fn stamps_recv_kind_optional_and_list() {
10089        // The annotation is comprehensive: it also serves the P1-c consumer
10090        // (Optional/Result method dispatch). An `Int?` receiver → "Optional";
10091        // a `List[Int]` receiver → "List".
10092        let gen = NodeIdGen::new();
10093        let mut checker = TypeChecker::new();
10094
10095        // Bind a variable `o: Int?` and call `o.unwrap_or(0)`.
10096        checker.env.define(
10097            "o",
10098            Type::Optional(Box::new(Type::Primitive(PrimitiveType::Int))),
10099        );
10100        let o_ref = make_node(&gen, NodeKind::Identifier { name: ident("o") });
10101        let mut opt_call = desugared_method_call(&gen, o_ref, "unwrap_or", vec![int_lit(&gen)]);
10102        checker.infer_node(&mut opt_call);
10103        assert_eq!(
10104            opt_call.metadata.get(RECV_KIND_META_KEY),
10105            Some(&Value::String("Optional".to_string())),
10106        );
10107
10108        checker.env.define(
10109            "xs",
10110            Type::Generic(GenericType {
10111                constructor: "List".into(),
10112                args: vec![Type::Primitive(PrimitiveType::Int)],
10113            }),
10114        );
10115        let xs_ref = make_node(&gen, NodeKind::Identifier { name: ident("xs") });
10116        let mut list_call = desugared_method_call(&gen, xs_ref, "len", vec![]);
10117        checker.infer_node(&mut list_call);
10118        assert_eq!(
10119            list_call.metadata.get(RECV_KIND_META_KEY),
10120            Some(&Value::String("List".to_string())),
10121        );
10122    }
10123
10124    /// Build a binary-op node `left <op> right`.
10125    fn binop_node(gen: &NodeIdGen, op: BinOp, left: AIRNode, right: AIRNode) -> AIRNode {
10126        make_node(
10127            gen,
10128            NodeKind::BinaryOp {
10129                op,
10130                left: Box::new(left),
10131                right: Box::new(right),
10132            },
10133        )
10134    }
10135
10136    /// An integer literal of a *sized* type (e.g. `42_i32` → `Int32`).
10137    fn sized_int_lit(gen: &NodeIdGen, suffix: &str) -> AIRNode {
10138        make_node(
10139            gen,
10140            NodeKind::Literal {
10141                lit: Literal::Int(format!("42_{suffix}")),
10142            },
10143        )
10144    }
10145
10146    #[test]
10147    fn stamps_int_arith_on_integer_div_and_rem() {
10148        // `17 / 5` and `17 % 5` — both operands `Int` — get the `int_arith` stamp
10149        // so codegen lowers them to DQ23's truncate-toward-zero / dividend-sign
10150        // semantics (§3.6).
10151        let gen = NodeIdGen::new();
10152        let mut checker = TypeChecker::new();
10153
10154        let mut div = binop_node(&gen, BinOp::Div, int_lit(&gen), int_lit(&gen));
10155        checker.infer_node(&mut div);
10156        assert_eq!(
10157            div.metadata.get(INT_ARITH_META_KEY),
10158            Some(&Value::Bool(true)),
10159            "expected int_arith stamped on Int / Int",
10160        );
10161
10162        let mut rem = binop_node(&gen, BinOp::Rem, int_lit(&gen), int_lit(&gen));
10163        checker.infer_node(&mut rem);
10164        assert_eq!(
10165            rem.metadata.get(INT_ARITH_META_KEY),
10166            Some(&Value::Bool(true)),
10167            "expected int_arith stamped on Int % Int",
10168        );
10169    }
10170
10171    #[test]
10172    fn stamps_int_arith_on_sized_integer_div() {
10173        // "All sized integer types divide the same way" (DQ23): a `Int32 / Int32`
10174        // is stamped just like `Int / Int`.
10175        let gen = NodeIdGen::new();
10176        let mut checker = TypeChecker::new();
10177
10178        let mut div = binop_node(
10179            &gen,
10180            BinOp::Div,
10181            sized_int_lit(&gen, "i32"),
10182            sized_int_lit(&gen, "i32"),
10183        );
10184        checker.infer_node(&mut div);
10185        assert_eq!(
10186            div.metadata.get(INT_ARITH_META_KEY),
10187            Some(&Value::Bool(true)),
10188            "expected int_arith stamped on Int32 / Int32",
10189        );
10190
10191        // UInt64 too.
10192        let mut udiv = binop_node(
10193            &gen,
10194            BinOp::Div,
10195            sized_int_lit(&gen, "u64"),
10196            sized_int_lit(&gen, "u64"),
10197        );
10198        checker.infer_node(&mut udiv);
10199        assert_eq!(
10200            udiv.metadata.get(INT_ARITH_META_KEY),
10201            Some(&Value::Bool(true)),
10202            "expected int_arith stamped on UInt64 / UInt64",
10203        );
10204    }
10205
10206    #[test]
10207    fn no_int_arith_stamp_on_float_div_or_addition() {
10208        // Float division is IEEE true division — NOT integer division — so it is
10209        // not stamped. And `+` (even on integers) is never integer division.
10210        let gen = NodeIdGen::new();
10211        let mut checker = TypeChecker::new();
10212
10213        let mut fdiv = binop_node(&gen, BinOp::Div, float_lit(&gen), float_lit(&gen));
10214        checker.infer_node(&mut fdiv);
10215        assert!(
10216            !fdiv.metadata.contains_key(INT_ARITH_META_KEY),
10217            "Float / Float must not be stamped int_arith",
10218        );
10219
10220        let mut add = binop_node(&gen, BinOp::Add, int_lit(&gen), int_lit(&gen));
10221        checker.infer_node(&mut add);
10222        assert!(
10223            !add.metadata.contains_key(INT_ARITH_META_KEY),
10224            "Int + Int is not integer division",
10225        );
10226    }
10227
10228    #[test]
10229    fn stamps_bool_stringify_on_bool_interpolation_part() {
10230        // A `Bool`-typed `${expr}` part is stamped so the Python backend prints
10231        // the canonical lowercase `true`/`false` (§3.5). A non-Bool part is not.
10232        let gen = NodeIdGen::new();
10233        let mut checker = TypeChecker::new();
10234
10235        let mut interp = make_node(
10236            &gen,
10237            NodeKind::Interpolation {
10238                parts: vec![
10239                    bock_air::AirInterpolationPart::Expr(Box::new(bool_lit(&gen, true))),
10240                    bock_air::AirInterpolationPart::Expr(Box::new(int_lit(&gen))),
10241                ],
10242            },
10243        );
10244        checker.infer_node(&mut interp);
10245        let NodeKind::Interpolation { parts } = &interp.kind else {
10246            panic!("expected interpolation");
10247        };
10248        let bock_air::AirInterpolationPart::Expr(bool_part) = &parts[0] else {
10249            panic!("expected expr part 0");
10250        };
10251        assert_eq!(
10252            bool_part.metadata.get(BOOL_STRINGIFY_META_KEY),
10253            Some(&Value::Bool(true)),
10254            "expected bool_stringify stamped on the Bool interpolation part",
10255        );
10256        let bock_air::AirInterpolationPart::Expr(int_part) = &parts[1] else {
10257            panic!("expected expr part 1");
10258        };
10259        assert!(
10260            !int_part.metadata.contains_key(BOOL_STRINGIFY_META_KEY),
10261            "Int interpolation part must not be stamped bool_stringify",
10262        );
10263    }
10264}