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 ¶ms {
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, <, &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(<) || 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(<) || 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(<) && 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(<) {
2763 Type::TypeVar(_) => &rt,
2764 _ => <,
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(<) {
2778 Type::TypeVar(_) => &rt,
2779 _ => <,
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(<y, ¶m_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, <y, 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}