Skip to main content

gruel_air/inference/
generate.rs

1//! Constraint generation for Hindley-Milner type inference.
2//!
3//! This module provides the constraint generation phase (Phase 1 of HM inference):
4//! - [`ConstraintContext`] - Scoped variable tracking during generation
5//! - [`ExprInfo`] - Result of constraint generation for an expression
6//! - [`ConstraintGenerator`] - Walks RIR and generates type constraints
7//! - Function/method signature types for type checking
8
9use super::constraint::Constraint;
10use super::types::{InferType, TypeVarAllocator, TypeVarId};
11use crate::Type;
12use crate::intern_pool::TypeInternPool;
13use crate::scope::ScopedContext;
14use crate::sema::InferenceContext;
15use crate::types::{
16    EnumId, PtrMutability, StructId, TypeKind, parse_array_type_syntax, parse_pointer_type_syntax,
17    parse_type_call_syntax,
18};
19use gruel_builtins::BuiltinTypeConstructorKind;
20use gruel_intrinsics::{IntrinsicId, lookup_by_name};
21use gruel_rir::{InstData, InstRef, Rir};
22use gruel_util::Span;
23use gruel_util::{BinOp, UnaryOp};
24use lasso::{Spur, ThreadedRodeo};
25use rustc_hash::FxHashMap as HashMap;
26
27/// Information about a local variable during constraint generation.
28#[derive(Debug, Clone)]
29pub struct LocalVarInfo {
30    /// The inferred type of this variable.
31    pub ty: InferType,
32    /// Whether the variable is mutable.
33    pub is_mut: bool,
34    /// Span of the variable declaration.
35    pub span: Span,
36}
37
38/// Information about a function parameter during constraint generation.
39#[derive(Debug, Clone)]
40pub struct ParamVarInfo {
41    /// The type of this parameter, as InferType for uniform handling.
42    pub ty: InferType,
43    /// Internal-mode after Phase-2/`analyze_function` normalization. A
44    /// `MutRef(T)` parameter has been lowered to `(T, Inout)`; a `Ref(T)`
45    /// parameter to `(T, Borrow)`. The mode is needed at HM constraint
46    /// generation so that ADR-0076's implicit forwarding can relax the
47    /// constraint when a `Ref(T)` / `MutRef(T)`-shaped binding is passed
48    /// onward to a `Ref(T)` / `MutRef(T)` callee param.
49    pub mode: gruel_rir::RirParamMode,
50}
51
52/// Information about a function during constraint generation.
53///
54/// Uses `InferType` rather than `Type` so that array types are represented
55/// structurally (as `InferType::Array { element, length }`) rather than by
56/// opaque IDs. This allows uniform handling during inference.
57#[derive(Debug, Clone)]
58pub struct FunctionSig {
59    /// Parameter types (in order), as InferTypes for uniform handling.
60    pub param_types: Vec<InferType>,
61    /// Return type, as InferType for uniform handling.
62    pub return_type: InferType,
63    /// Whether this is a generic function (has comptime type parameters).
64    /// Generic functions skip type checking during constraint generation -
65    /// they'll be checked during specialization.
66    pub is_generic: bool,
67    /// Parameter modes (Normal, Inout, Borrow, Comptime).
68    pub param_modes: Vec<gruel_rir::RirParamMode>,
69    /// Which parameters are comptime (declared with `comptime` keyword).
70    /// This is separate from param_modes because `comptime T: type` sets
71    /// is_comptime=true but mode=Normal.
72    pub param_comptime: Vec<bool>,
73    /// Parameter names, needed for type substitution in generic returns.
74    pub param_names: Vec<lasso::Spur>,
75    /// The return type as a symbol (used for substitution lookup).
76    pub return_type_sym: lasso::Spur,
77}
78
79/// Information about a method during constraint generation.
80///
81/// Used for method calls (receiver.method()) and associated function calls (Type::function()).
82#[derive(Debug, Clone)]
83pub struct MethodSig {
84    /// The struct type this method belongs to (as concrete Type::Struct)
85    pub struct_type: Type,
86    /// Whether this is a method (has self) or associated function (no self)
87    pub has_self: bool,
88    /// Parameter types (excluding self), as InferTypes for uniform handling.
89    pub param_types: Vec<InferType>,
90    /// Return type, as InferType for uniform handling.
91    pub return_type: InferType,
92}
93
94/// Context for constraint generation within a single function.
95pub struct ConstraintContext<'a> {
96    /// Local variables in scope.
97    pub locals: HashMap<Spur, LocalVarInfo>,
98    /// Function parameters.
99    pub params: &'a HashMap<Spur, ParamVarInfo>,
100    /// Return type of the current function.
101    pub return_type: Type,
102    /// How many loops we're nested inside (for break/continue validation).
103    pub loop_depth: u32,
104    /// Scope stack for efficient scope management.
105    scope_stack: Vec<Vec<(Spur, Option<LocalVarInfo>)>>,
106}
107
108impl<'a> ConstraintContext<'a> {
109    /// Create a new context for a function.
110    pub fn new(params: &'a HashMap<Spur, ParamVarInfo>, return_type: Type) -> Self {
111        Self {
112            locals: HashMap::default(),
113            params,
114            return_type,
115            loop_depth: 0,
116            scope_stack: Vec::new(),
117        }
118    }
119}
120
121impl ScopedContext for ConstraintContext<'_> {
122    type VarInfo = LocalVarInfo;
123
124    fn locals_mut(&mut self) -> &mut HashMap<Spur, Self::VarInfo> {
125        &mut self.locals
126    }
127
128    fn scope_stack_mut(&mut self) -> &mut Vec<Vec<(Spur, Option<Self::VarInfo>)>> {
129        &mut self.scope_stack
130    }
131}
132
133/// Result of constraint generation for an expression.
134#[derive(Debug, Clone)]
135pub struct ExprInfo {
136    /// The inferred type of this expression.
137    pub ty: InferType,
138    /// The span of this expression (for error reporting).
139    pub span: Span,
140}
141
142impl ExprInfo {
143    /// Create a new expression info.
144    pub fn new(ty: InferType, span: Span) -> Self {
145        Self { ty, span }
146    }
147}
148
149/// Constraint generator that walks RIR and generates type constraints.
150/// Return type of [`ConstraintGenerator::into_parts`].
151pub type ConstraintGeneratorParts = (
152    Vec<Constraint>,
153    Vec<TypeVarId>,
154    Vec<TypeVarId>,
155    HashMap<InstRef, InferType>,
156    u32,
157);
158
159///
160/// This is Phase 1 of HM inference: constraint generation. The constraints
161/// are later solved by the `Unifier` to determine concrete types.
162pub struct ConstraintGenerator<'a> {
163    /// The RIR being analyzed.
164    rir: &'a Rir,
165    /// String interner for resolving symbols.
166    interner: &'a ThreadedRodeo,
167    /// Type variable allocator.
168    type_vars: TypeVarAllocator,
169    /// Collected constraints.
170    constraints: Vec<Constraint>,
171    /// Mapping from RIR instruction to its inferred type.
172    expr_types: HashMap<InstRef, InferType>,
173    /// Function signatures (for call type checking).
174    functions: &'a HashMap<Spur, FunctionSig>,
175    /// Struct types (name -> Type::new_struct(id)).
176    structs: &'a HashMap<Spur, Type>,
177    /// Enum types (name -> Type::new_enum(id)).
178    enums: &'a HashMap<Spur, Type>,
179    /// Method signatures: (struct_id, method_name) -> MethodSig
180    methods: &'a HashMap<(StructId, Spur), MethodSig>,
181    /// Enum method signatures: (enum_id, method_name) -> MethodSig
182    enum_methods: &'a HashMap<(EnumId, Spur), MethodSig>,
183    /// Type variables allocated for integer literals.
184    /// These start as unbound and need to be defaulted to i32 if unconstrained.
185    int_literal_vars: Vec<TypeVarId>,
186    /// Type variables allocated for float literals.
187    /// These start as unbound and need to be defaulted to f64 if unconstrained.
188    float_literal_vars: Vec<TypeVarId>,
189    /// Type substitutions for Self and type parameters (used in method bodies).
190    /// Maps type names (like "Self") to their concrete types.
191    type_subst: Option<&'a HashMap<Spur, Type>>,
192    /// Type intern pool for creating pointer and array types during constraint generation.
193    type_pool: &'a TypeInternPool,
194}
195
196impl<'a> ConstraintGenerator<'a> {
197    /// Create a new constraint generator.
198    pub fn new(
199        rir: &'a Rir,
200        interner: &'a ThreadedRodeo,
201        infer_ctx: &'a InferenceContext,
202        type_pool: &'a TypeInternPool,
203    ) -> Self {
204        Self {
205            rir,
206            interner,
207            type_vars: TypeVarAllocator::new(),
208            constraints: Vec::new(),
209            expr_types: HashMap::default(),
210            functions: &infer_ctx.func_sigs,
211            structs: &infer_ctx.struct_types,
212            enums: &infer_ctx.enum_types,
213            methods: &infer_ctx.method_sigs,
214            enum_methods: &infer_ctx.enum_method_sigs,
215            int_literal_vars: Vec::new(),
216            float_literal_vars: Vec::new(),
217            type_subst: None,
218            type_pool,
219        }
220    }
221
222    /// Set type substitutions for `Self` and type parameters (builder pattern).
223    ///
224    /// The `type_subst` map provides type substitutions for names like "Self"
225    /// that should be resolved to concrete types during constraint generation.
226    /// This is used for method bodies where `Self { ... }` struct literals
227    /// need to know the concrete struct type.
228    pub fn with_type_subst(mut self, type_subst: Option<&'a HashMap<Spur, Type>>) -> Self {
229        self.type_subst = type_subst;
230        self
231    }
232
233    /// Get the type variables allocated for integer literals.
234    pub fn int_literal_vars(&self) -> &[TypeVarId] {
235        &self.int_literal_vars
236    }
237
238    /// Allocate a fresh type variable.
239    pub fn fresh_var(&mut self) -> TypeVarId {
240        self.type_vars.fresh()
241    }
242
243    /// Add a constraint.
244    pub fn add_constraint(&mut self, constraint: Constraint) {
245        self.constraints.push(constraint);
246    }
247
248    /// ADR-0076: when a call argument is a bare `Var(name)` referencing a
249    /// `Borrow`/`Inout`-mode parameter, and the callee expects `Ref(T)` /
250    /// `MutRef(T)` of the param's inner type, the arg's inferred type is
251    /// the inner `T` (the parameter has been normalized post-Phase-2). The
252    /// caller would have written `&name` / `&mut name` explicitly under
253    /// the old surface; per ADR-0076 the reference is implicit. Relax
254    /// the constraint target to `T` so the call type-checks.
255    fn relax_ref_param_for_implicit_forwarding(
256        &self,
257        arg_ref: InstRef,
258        param_ty: &InferType,
259        ctx: &ConstraintContext<'_>,
260    ) -> Option<InferType> {
261        let InferType::Concrete(param_concrete) = param_ty else {
262            return None;
263        };
264        let inner_ty = match param_concrete.kind() {
265            crate::types::TypeKind::Ref(id) => self.type_pool.ref_def(id),
266            crate::types::TypeKind::MutRef(id) => self.type_pool.mut_ref_def(id),
267            _ => return None,
268        };
269        let InstData::VarRef { name } = &self.rir.get(arg_ref).data else {
270            return None;
271        };
272        let p = ctx.params.get(name)?;
273        // The arg binding's normalized inner type must match the callee's
274        // referent. The mode must be `Ref` or `MutRef` (i.e. the binding
275        // came from an original `Ref(T)` / `MutRef(T)` declaration that
276        // routes through the legacy by-pointer mode for interface-typed
277        // params per ADR-0076).
278        if !matches!(
279            p.mode,
280            gruel_rir::RirParamMode::Ref | gruel_rir::RirParamMode::MutRef
281        ) {
282            return None;
283        }
284        // Convert the referent Type to InferType for the comparison —
285        // arrays are stored structurally as `InferType::Array { ... }`
286        // rather than `InferType::Concrete(Type::Array(...))`.
287        let inner_infer = self.type_to_infer(inner_ty);
288        if p.ty != inner_infer {
289            return None;
290        }
291        Some(inner_infer)
292    }
293
294    /// ADR-0076: auto-deref a `Ref(T)` / `MutRef(T)` value type to its
295    /// referent `T` for constraint purposes. Used at every site that
296    /// expects a "value" type (arithmetic, comparison, equality with a
297    /// callee param). Non-ref types pass through unchanged.
298    fn auto_deref(&self, ty: InferType) -> InferType {
299        match &ty {
300            InferType::Concrete(t) => match t.kind() {
301                crate::types::TypeKind::Ref(id) => InferType::Concrete(self.type_pool.ref_def(id)),
302                crate::types::TypeKind::MutRef(id) => {
303                    InferType::Concrete(self.type_pool.mut_ref_def(id))
304                }
305                _ => ty,
306            },
307            _ => ty,
308        }
309    }
310
311    /// Convert a concrete `Type` to `InferType`, preserving the structural
312    /// form for arrays (mirrors `Sema::type_to_infer_type`).
313    fn type_to_infer(&self, ty: Type) -> InferType {
314        match ty.kind() {
315            crate::types::TypeKind::Array(array_id) => {
316                let (element_type, length) = self.type_pool.array_def(array_id);
317                let element_infer = self.type_to_infer(element_type);
318                InferType::Array {
319                    element: Box::new(element_infer),
320                    length,
321                }
322            }
323            crate::types::TypeKind::ComptimeInt => InferType::IntLiteral,
324            _ => InferType::Concrete(ty),
325        }
326    }
327
328    /// Record the type of an expression.
329    pub fn record_type(&mut self, inst_ref: InstRef, ty: InferType) {
330        self.expr_types.insert(inst_ref, ty);
331    }
332
333    /// Get the recorded type of an expression.
334    pub fn get_type(&self, inst_ref: InstRef) -> Option<&InferType> {
335        self.expr_types.get(&inst_ref)
336    }
337
338    /// Get all collected constraints.
339    pub fn constraints(&self) -> &[Constraint] {
340        &self.constraints
341    }
342
343    /// Take ownership of the collected constraints.
344    pub fn take_constraints(self) -> Vec<Constraint> {
345        self.constraints
346    }
347
348    /// Get the expression type mapping.
349    pub fn expr_types(&self) -> &HashMap<InstRef, InferType> {
350        &self.expr_types
351    }
352
353    /// Consume the constraint generator and return (constraints, int_literal_vars, float_literal_vars, expr_types, type_var_count).
354    ///
355    /// This is useful when you need ownership of the expression types map.
356    /// The `type_var_count` can be used to pre-size the unifier's substitution for better performance.
357    pub fn into_parts(self) -> ConstraintGeneratorParts {
358        (
359            self.constraints,
360            self.int_literal_vars,
361            self.float_literal_vars,
362            self.expr_types,
363            self.type_vars.count(),
364        )
365    }
366
367    /// Generate constraints for an expression.
368    ///
369    /// Returns the inferred type of the expression. Records the type in
370    /// `expr_types` and adds constraints to `constraints`.
371    pub fn generate(&mut self, inst_ref: InstRef, ctx: &mut ConstraintContext) -> ExprInfo {
372        let inst = self.rir.get(inst_ref);
373        let span = inst.span;
374
375        let ty = match &inst.data {
376            InstData::IntConst(_) => {
377                // Integer literals get a fresh type variable that we immediately
378                // bind to IntLiteral. This allows unification to track when the
379                // literal is constrained to a specific integer type.
380                //
381                // Example: `let x: i64 = 42` generates:
382                //   - type_var(?0) for the literal 42
383                //   - substitution: ?0 -> IntLiteral
384                //   - constraint: Equal(Var(?0), Concrete(i64))
385                //
386                // During unification, Equal(IntLiteral, Concrete(i64)) succeeds
387                // and rebinds ?0 -> Concrete(i64) via rebind_int_literal_to_concrete.
388                let var = self.fresh_var();
389                self.int_literal_vars.push(var);
390                InferType::Var(var)
391            }
392
393            InstData::FloatConst(_) => {
394                // Float literals work like int literals but default to f64.
395                let var = self.fresh_var();
396                self.float_literal_vars.push(var);
397                InferType::Var(var)
398            }
399
400            InstData::BoolConst(_) => InferType::Concrete(Type::BOOL),
401
402            // ADR-0071: char literal — Unicode scalar value, type is char.
403            InstData::CharConst(_) => InferType::Concrete(Type::CHAR),
404
405            // String constants use the builtin String struct type.
406            InstData::StringConst(_) => {
407                // Look up the String type from the structs map
408                if let Some(string_spur) = self.interner.get("String") {
409                    if let Some(&string_ty) = self.structs.get(&string_spur) {
410                        InferType::Concrete(string_ty)
411                    } else {
412                        // Fallback if String struct not found (shouldn't happen after builtin injection)
413                        InferType::Concrete(Type::ERROR)
414                    }
415                } else {
416                    InferType::Concrete(Type::ERROR)
417                }
418            }
419
420            InstData::UnitConst => InferType::Concrete(Type::UNIT),
421
422            InstData::Bin { op, lhs, rhs } => match op {
423                BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod => {
424                    self.generate_binary_arith(*lhs, *rhs, ctx)
425                }
426                BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor | BinOp::Shl | BinOp::Shr => {
427                    self.generate_binary_bitwise(*lhs, *rhs, ctx)
428                }
429                BinOp::Eq | BinOp::Ne | BinOp::Lt | BinOp::Gt | BinOp::Le | BinOp::Ge => {
430                    let lhs_info = self.generate(*lhs, ctx);
431                    let rhs_info = self.generate(*rhs, ctx);
432                    self.add_constraint(Constraint::equal(lhs_info.ty, rhs_info.ty, span));
433                    InferType::Concrete(Type::BOOL)
434                }
435                BinOp::And | BinOp::Or => {
436                    let lhs_info = self.generate(*lhs, ctx);
437                    let rhs_info = self.generate(*rhs, ctx);
438                    self.add_constraint(Constraint::equal(
439                        lhs_info.ty,
440                        InferType::Concrete(Type::BOOL),
441                        lhs_info.span,
442                    ));
443                    self.add_constraint(Constraint::equal(
444                        rhs_info.ty,
445                        InferType::Concrete(Type::BOOL),
446                        rhs_info.span,
447                    ));
448                    InferType::Concrete(Type::BOOL)
449                }
450            },
451
452            InstData::Unary { op, operand } => match op {
453                UnaryOp::Neg => {
454                    let operand_info = self.generate(*operand, ctx);
455                    let result_ty = operand_info.ty.clone();
456                    self.add_constraint(Constraint::is_signed(result_ty.clone(), span));
457                    result_ty
458                }
459                UnaryOp::Not => {
460                    let operand_info = self.generate(*operand, ctx);
461                    self.add_constraint(Constraint::equal(
462                        operand_info.ty,
463                        InferType::Concrete(Type::BOOL),
464                        operand_info.span,
465                    ));
466                    InferType::Concrete(Type::BOOL)
467                }
468                UnaryOp::BitNot => {
469                    let operand_info = self.generate(*operand, ctx);
470                    let result_ty = operand_info.ty.clone();
471                    self.add_constraint(Constraint::is_integer(result_ty.clone(), span));
472                    result_ty
473                }
474            },
475
476            // ADR-0062: `&x` / `&mut x` produces `Ref(T)` / `MutRef(T)`.
477            // The result type depends on the operand's resolved type, which
478            // inference may not have nailed down yet. Defer the construction
479            // of the actual `Ref`/`MutRef` type to sema (`analyze_inst`),
480            // and just propagate a fresh type variable that sema will set.
481            InstData::MakeRef { operand, .. } => {
482                let _ = self.generate(*operand, ctx);
483                InferType::Var(self.fresh_var())
484            }
485
486            // ADR-0064: `&arr[range]` / `&mut arr[range]` produces a slice.
487            // Defer the actual `Slice(T)` / `MutSlice(T)` type to sema; record
488            // the sub-expressions so they receive types.
489            InstData::MakeSlice { base, lo, hi, .. } => {
490                let _ = self.generate(*base, ctx);
491                if let Some(lo) = lo {
492                    self.generate(*lo, ctx);
493                }
494                if let Some(hi) = hi {
495                    self.generate(*hi, ctx);
496                }
497                InferType::Var(self.fresh_var())
498            }
499
500            // ADR-0064: a range subscript without `&` / `&mut`. Sema rejects.
501            InstData::BareRangeSubscript => InferType::Concrete(Type::ERROR),
502
503            // Variable reference
504            InstData::VarRef { name } => {
505                if let Some(local) = ctx.locals.get(name) {
506                    local.ty.clone()
507                } else if let Some(param) = ctx.params.get(name) {
508                    param.ty.clone()
509                } else {
510                    // Unknown variable - will be caught during semantic analysis
511                    InferType::Concrete(Type::ERROR)
512                }
513            }
514
515            // Parameter reference
516            InstData::ParamRef { name, .. } => {
517                if let Some(param) = ctx.params.get(name) {
518                    param.ty.clone()
519                } else {
520                    InferType::Concrete(Type::ERROR)
521                }
522            }
523
524            // Local variable allocation
525            InstData::Alloc {
526                directives_start: _,
527                directives_len: _,
528                name,
529                is_mut,
530                ty: type_annotation,
531                init,
532            } => {
533                let init_info = self.generate(*init, ctx);
534
535                let var_ty = if let Some(ty_sym) = type_annotation {
536                    // Explicit type annotation - use it and constrain init to match
537                    let ty_name = self.interner.resolve(ty_sym);
538                    if let Some(annotated_ty) = self.resolve_type_name(ty_name) {
539                        self.add_constraint(Constraint::equal(
540                            init_info.ty,
541                            annotated_ty.clone(),
542                            span,
543                        ));
544                        annotated_ty
545                    } else {
546                        // Unknown type name (e.g., struct/enum) - use init type for now.
547                        // Semantic analysis will catch undefined types and verify struct/enum
548                        // field types match the definition.
549                        init_info.ty
550                    }
551                } else {
552                    // No annotation - use the init expression's type
553                    init_info.ty
554                };
555
556                // Record the variable in scope (if it has a name)
557                if let Some(var_name) = name {
558                    ctx.insert_local(
559                        *var_name,
560                        LocalVarInfo {
561                            ty: var_ty.clone(),
562                            is_mut: *is_mut,
563                            span,
564                        },
565                    );
566                }
567
568                // Alloc produces unit type
569                InferType::Concrete(Type::UNIT)
570            }
571
572            // Struct destructuring — register field bindings for type inference
573            InstData::StructDestructure {
574                type_name,
575                fields_start,
576                fields_len,
577                init,
578            } => {
579                self.generate(*init, ctx);
580
581                // Look up the struct type to get field types
582                if let Some(&struct_ty) = self.structs.get(type_name)
583                    && let Some(struct_id) = struct_ty.as_struct()
584                {
585                    let struct_def = self.type_pool.struct_def(struct_id);
586                    let rir_fields = self.rir.get_destructure_fields(*fields_start, *fields_len);
587                    for field in &rir_fields {
588                        if field.is_wildcard {
589                            continue;
590                        }
591                        let field_name = self.interner.resolve(&field.field_name);
592                        if let Some((_, struct_field)) = struct_def.find_field(field_name) {
593                            let binding_name = field.binding_name.unwrap_or(field.field_name);
594                            ctx.insert_local(
595                                binding_name,
596                                LocalVarInfo {
597                                    ty: InferType::Concrete(struct_field.ty),
598                                    is_mut: field.is_mut,
599                                    span,
600                                },
601                            );
602                        }
603                    }
604                }
605
606                InferType::Concrete(Type::UNIT)
607            }
608
609            // Assignment
610            InstData::Assign { name, value } => {
611                let value_info = self.generate(*value, ctx);
612                // ADR-0076: a `MutRef(T)`-typed binding (param or local)
613                // makes bare-name assign a write-through; the value's
614                // type must match the referent `T`, not `MutRef(T)`. A
615                // `Ref(T)`-typed binding is a read-only ref — the
616                // through-write is rejected later by sema, but for
617                // constraint-generation symmetry we still constrain
618                // against the referent so the user gets a clean
619                // diagnostic from sema rather than a confused
620                // "literal out of range for `<ref>`".
621                let binding_ty = ctx
622                    .locals
623                    .get(name)
624                    .map(|l| l.ty.clone())
625                    .or_else(|| ctx.params.get(name).map(|p| p.ty.clone()));
626                if let Some(ty) = binding_ty {
627                    let target_ty = self.auto_deref(ty);
628                    // Both sides auto-deref so `a = b` where both bindings
629                    // are `MutRef(T)` constrains `T ≈ T`, not `MutRef(T) ≈ T`.
630                    let value_ty = self.auto_deref(value_info.ty);
631                    self.add_constraint(Constraint::equal(value_ty, target_ty, span));
632                }
633                // Assignment produces unit
634                InferType::Concrete(Type::UNIT)
635            }
636
637            // Return statement
638            InstData::Ret(value) => {
639                if let Some(val_ref) = value {
640                    let value_info = self.generate(*val_ref, ctx);
641                    // Constrain return value to match function return type.
642                    // ADR-0076: auto-deref so `return d` for a `Ref(T)` /
643                    // `MutRef(T)` binding constrains against `T`. Sema
644                    // separately rejects moving out of a borrow.
645                    self.add_constraint(Constraint::equal(
646                        self.auto_deref(value_info.ty),
647                        InferType::Concrete(ctx.return_type),
648                        span,
649                    ));
650                } else {
651                    // Return without value - function must return unit
652                    self.add_constraint(Constraint::equal(
653                        InferType::Concrete(Type::UNIT),
654                        InferType::Concrete(ctx.return_type),
655                        span,
656                    ));
657                }
658                // Return diverges
659                InferType::Concrete(Type::NEVER)
660            }
661
662            // Function call
663            InstData::Call {
664                name,
665                args_start,
666                args_len,
667            } => {
668                let args = self.rir.get_call_args(*args_start, *args_len);
669                if let Some(func) = self.functions.get(name) {
670                    // For generic functions, skip constraint generation for arguments.
671                    // The types will be checked during specialization when we know
672                    // the concrete type substitutions.
673                    if func.is_generic {
674                        // Process all arguments and build type substitution map
675                        let mut type_subst: rustc_hash::FxHashMap<lasso::Spur, Type> =
676                            rustc_hash::FxHashMap::default();
677
678                        for (i, arg) in args.iter().enumerate() {
679                            let arg_info = self.generate(arg.value, ctx);
680
681                            // If this is a comptime parameter, extract the type for substitution.
682                            if i < func.param_comptime.len() && func.param_comptime[i] {
683                                let arg_inst = self.rir.get(arg.value);
684                                // A bare comptime type-param identifier (e.g., `T`
685                                // in `helper(Self, T, ...)`) parses as
686                                // `Expr::Ident` and lowers to `VarRef`, not
687                                // `TypeConst`. Its inferred type is the
688                                // substituted concrete type (e.g., `i32`),
689                                // which fails the `COMPTIME_TYPE` check below.
690                                // Look it up in the outer `type_subst` so the
691                                // callee's type parameter gets bound.
692                                if let gruel_rir::InstData::VarRef { name } = &arg_inst.data
693                                    && let Some(subst) = self.type_subst
694                                    && let Some(&forwarded_ty) = subst.get(name)
695                                    && i < func.param_names.len()
696                                {
697                                    type_subst.insert(func.param_names[i], forwarded_ty);
698                                    continue;
699                                }
700                                // The argument should be a TypeConst - extract the concrete type
701                                if let InferType::Concrete(Type::COMPTIME_TYPE) = &arg_info.ty {
702                                    // This is a type value - get the actual type from the RIR
703                                    if let gruel_rir::InstData::TypeConst { type_name } =
704                                        &arg_inst.data
705                                    {
706                                        // Resolve `type_name` to a concrete Type.
707                                        // Checks (in order):
708                                        //   1. The outer method body's `type_subst`
709                                        //      — covers `Self` and method-level
710                                        //      `comptime T: type` parameters being
711                                        //      forwarded to a callee.
712                                        //   2. Registered structs (`self.structs`)
713                                        //      — covers user-named types passed
714                                        //      directly: `helper(MyStruct)`.
715                                        //   3. A small primitive table — covers
716                                        //      `i32`/`bool`/etc. parsed as
717                                        //      `TypeLit` from primitive keywords,
718                                        //      which never reach
719                                        //      `register_type_names` so they
720                                        //      aren't in `self.structs`.
721                                        let type_name_str = self.interner.resolve(type_name);
722                                        let concrete_ty = self
723                                            .type_subst
724                                            .and_then(|subst| subst.get(type_name).copied())
725                                            .or_else(|| self.structs.get(type_name).copied())
726                                            .unwrap_or(match type_name_str {
727                                                "i8" => Type::I8,
728                                                "i16" => Type::I16,
729                                                "i32" => Type::I32,
730                                                "i64" => Type::I64,
731                                                "isize" => Type::ISIZE,
732                                                "u8" => Type::U8,
733                                                "u16" => Type::U16,
734                                                "u32" => Type::U32,
735                                                "u64" => Type::U64,
736                                                "usize" => Type::USIZE,
737                                                "f16" => Type::F16,
738                                                "f32" => Type::F32,
739                                                "f64" => Type::F64,
740                                                "bool" => Type::BOOL,
741                                                "char" => Type::CHAR,
742                                                "()" => Type::UNIT,
743                                                _ => Type::ERROR,
744                                            });
745                                        if i < func.param_names.len() {
746                                            type_subst.insert(func.param_names[i], concrete_ty);
747                                        }
748                                    }
749                                }
750                            }
751                        }
752
753                        // Compute the actual return type by substituting type parameters
754
755                        if func.return_type == InferType::Concrete(Type::COMPTIME_TYPE) {
756                            // Return type position references at least one
757                            // type parameter (declared as `Type::COMPTIME_TYPE`
758                            // in the sig — see `references_type_param` in
759                            // declarations.rs). Two shapes:
760                            //   1. Bare type-param: `-> T` → look up
761                            //      `return_type_sym` ("T") in `type_subst`.
762                            //   2. Compound: `-> Ptr(T)` / `-> MutRef(Vec(T))`
763                            //      → recursively resolve via the local subst
764                            //      so the inner `T` gets bound to the concrete
765                            //      type just gathered from the call's args.
766                            let sym_str = self.interner.resolve(&func.return_type_sym);
767                            if let Some(&concrete_ty) = type_subst.get(&func.return_type_sym) {
768                                InferType::Concrete(concrete_ty)
769                            } else if let Some(resolved) =
770                                self.resolve_type_with_local_subst(sym_str, &type_subst)
771                            {
772                                resolved
773                            } else {
774                                func.return_type.clone()
775                            }
776                        } else {
777                            func.return_type.clone()
778                        }
779                    } else if args.len() != func.param_types.len() {
780                        // Check argument count matches parameter count.
781                        // Semantic analysis will emit a proper error; we just need to avoid
782                        // panicking and process what we can.
783                        // Still process all arguments to catch type errors within them
784                        for arg in args.iter() {
785                            self.generate(arg.value, ctx);
786                        }
787                        // Return the declared return type (error will be caught in sema)
788                        func.return_type.clone()
789                    } else {
790                        // Generate constraints for each argument.
791                        // ADR-0056: skip the equality constraint when the
792                        // parameter type is an interface — sema applies
793                        // structural conformance (not equality) and inserts
794                        // a `MakeInterfaceRef` coercion at the call site.
795                        for (arg, param_ty) in args.iter().zip(func.param_types.iter()) {
796                            let arg_info = self.generate(arg.value, ctx);
797                            let is_iface = matches!(
798                                param_ty,
799                                InferType::Concrete(t) if matches!(
800                                    t.kind(),
801                                    crate::types::TypeKind::Interface(_)
802                                )
803                            );
804                            if !is_iface {
805                                // ADR-0076: implicit forwarding. If the
806                                // callee expects `Ref(T)` / `MutRef(T)` and
807                                // the arg is a bare reference to a
808                                // Borrow/Inout-mode parameter, the arg's
809                                // inferred type is the inner `T`. Relax
810                                // the constraint to be against the inner
811                                // referent type so the call type-checks.
812                                let constraint_target = self
813                                    .relax_ref_param_for_implicit_forwarding(
814                                        arg.value, param_ty, ctx,
815                                    )
816                                    .unwrap_or_else(|| param_ty.clone());
817                                self.add_constraint(Constraint::equal(
818                                    arg_info.ty,
819                                    constraint_target,
820                                    arg_info.span,
821                                ));
822                            }
823                        }
824                        func.return_type.clone()
825                    }
826                } else {
827                    // ADR-0082: when a `Call` targets a method on an
828                    // anonymous struct (the dispatch flip emits these
829                    // for `Vec(I32)::new()`, `v.push(x)`, etc.), the
830                    // mangled name `{struct_name}::{method}` or
831                    // `{struct_name}.{method}` won't be in `functions`.
832                    // Parse it and look up `method_sigs` so the literal
833                    // arg constraints are emitted, otherwise int literals
834                    // default to `i32` and codegen sees i32-vs-usize
835                    // mismatches at the call boundary.
836                    let mangled = self.interner.resolve(name).to_string();
837                    let split = mangled
838                        .rfind("::")
839                        .map(|i| (&mangled[..i], &mangled[i + 2..]))
840                        .or_else(|| {
841                            mangled
842                                .rfind('.')
843                                .map(|i| (&mangled[..i], &mangled[i + 1..]))
844                        });
845                    // Resolve the struct via the live type pool — anonymous
846                    // structs created post-`build_inference_context` (which
847                    // is when `populate_vec_instance` builds Vec(I32)
848                    // instances) aren't in `self.structs` yet.
849                    let struct_id_opt: Option<crate::types::StructId> = split
850                        .and_then(|(struct_name, _)| self.interner.get(struct_name))
851                        .and_then(|s| self.type_pool.get_struct_by_name(s))
852                        .and_then(|i| i.pool_index())
853                        .map(crate::types::StructId::from_pool_index);
854                    let method_sym_opt =
855                        split.and_then(|(_, method_name)| self.interner.get(method_name));
856                    if let (Some(struct_id), Some(method_sym)) = (struct_id_opt, method_sym_opt)
857                        && let Some(method) = self.methods.get(&(struct_id, method_sym))
858                    {
859                        // Check arg/param-count parity; constrain each
860                        // arg to its expected param type. Mirrors the
861                        // top-level fn-call logic above.
862                        let m_param_types = method.param_types.clone();
863                        if args.len() != m_param_types.len() {
864                            for arg in args.iter() {
865                                self.generate(arg.value, ctx);
866                            }
867                        } else {
868                            for (arg, param_ty) in args.iter().zip(m_param_types.iter()) {
869                                let arg_info = self.generate(arg.value, ctx);
870                                let is_iface = matches!(
871                                    param_ty,
872                                    InferType::Concrete(t) if matches!(
873                                        t.kind(),
874                                        crate::types::TypeKind::Interface(_)
875                                    )
876                                );
877                                if !is_iface {
878                                    self.add_constraint(Constraint::equal(
879                                        arg_info.ty,
880                                        param_ty.clone(),
881                                        arg_info.span,
882                                    ));
883                                }
884                            }
885                        }
886                        method.return_type.clone()
887                    } else {
888                        // Unknown function - still process arguments for constraint generation
889                        for arg in args.iter() {
890                            self.generate(arg.value, ctx);
891                        }
892                        InferType::Concrete(Type::ERROR)
893                    }
894                }
895            }
896
897            // Intrinsic call
898            InstData::Intrinsic {
899                name,
900                args_start,
901                args_len,
902            } => {
903                let intrinsic_name = self.interner.resolve(name);
904                let id = lookup_by_name(intrinsic_name).map(|d| d.id);
905                // Collect arg InstRefs so we can iterate without holding a
906                // borrow on self.rir across the dispatch match.
907                let arg_refs: Vec<InstRef> =
908                    self.rir.get_inst_refs(*args_start, *args_len).to_vec();
909
910                // Visit args in a side-effectful pass so constraints on them
911                // are emitted regardless of which intrinsic we hit below.
912                let visit_args = |this: &mut Self, ctx: &mut ConstraintContext| {
913                    for &arg_ref in arg_refs.iter() {
914                        this.generate(arg_ref, ctx);
915                    }
916                };
917
918                match id {
919                    Some(IntrinsicId::Cast) => {
920                        if let Some(&first) = arg_refs.first() {
921                            let _ = self.generate(first, ctx);
922                        }
923                        InferType::Var(self.fresh_var())
924                    }
925                    // ADR-0087 Phase 3: @read_line / @parse_* / @random_*
926                    // intrinsics retired in favour of prelude fns. Their
927                    // inference arms went with them.
928                    Some(IntrinsicId::Syscall) => {
929                        visit_args(self, ctx);
930                        InferType::Concrete(Type::I64)
931                    }
932                    Some(IntrinsicId::PtrToInt) => {
933                        visit_args(self, ctx);
934                        InferType::Concrete(Type::U64)
935                    }
936                    Some(IntrinsicId::PtrWrite) | Some(IntrinsicId::PtrWriteVolatile) => {
937                        visit_args(self, ctx);
938                        InferType::Concrete(Type::UNIT)
939                    }
940                    Some(IntrinsicId::IsNull) => {
941                        visit_args(self, ctx);
942                        InferType::Concrete(Type::BOOL)
943                    }
944                    Some(IntrinsicId::PtrRead) | Some(IntrinsicId::PtrReadVolatile) => {
945                        // Return type depends on pointee type of the argument —
946                        // resolved in sema once the concrete pointer type is known.
947                        visit_args(self, ctx);
948                        InferType::Var(self.fresh_var())
949                    }
950                    Some(IntrinsicId::PtrOffset) => {
951                        // Return type matches the input pointer type.
952                        visit_args(self, ctx);
953                        InferType::Var(self.fresh_var())
954                    }
955                    Some(IntrinsicId::Raw) | Some(IntrinsicId::RawMut) => {
956                        // Returns ptr const T / ptr mut T — resolved in sema.
957                        visit_args(self, ctx);
958                        InferType::Var(self.fresh_var())
959                    }
960                    Some(IntrinsicId::IntToPtr) | Some(IntrinsicId::NullPtr) => {
961                        // Pointer type inferred from context.
962                        visit_args(self, ctx);
963                        InferType::Var(self.fresh_var())
964                    }
965                    Some(IntrinsicId::PtrCopy) => {
966                        visit_args(self, ctx);
967                        InferType::Concrete(Type::UNIT)
968                    }
969                    Some(IntrinsicId::TargetArch) => {
970                        if let Some(arch_spur) = self.interner.get("Arch") {
971                            if let Some(&arch_ty) = self.enums.get(&arch_spur) {
972                                InferType::Concrete(arch_ty)
973                            } else {
974                                InferType::Concrete(Type::ERROR)
975                            }
976                        } else {
977                            InferType::Concrete(Type::ERROR)
978                        }
979                    }
980                    Some(IntrinsicId::TargetOs) => {
981                        if let Some(os_spur) = self.interner.get("Os") {
982                            if let Some(&os_ty) = self.enums.get(&os_spur) {
983                                InferType::Concrete(os_ty)
984                            } else {
985                                InferType::Concrete(Type::ERROR)
986                            }
987                        } else {
988                            InferType::Concrete(Type::ERROR)
989                        }
990                    }
991                    Some(IntrinsicId::Range) => {
992                        // @range: 1-3 integer args; returns the same integer type
993                        // (used as an iterable in for-in loops).
994                        if let Some((&first_ref, rest)) = arg_refs.split_first() {
995                            let first = self.generate(first_ref, ctx);
996                            for &arg_ref in rest {
997                                let arg_info = self.generate(arg_ref, ctx);
998                                self.add_constraint(Constraint::equal(
999                                    first.ty.clone(),
1000                                    arg_info.ty,
1001                                    span,
1002                                ));
1003                            }
1004                            first.ty
1005                        } else {
1006                            InferType::Concrete(Type::ERROR)
1007                        }
1008                    }
1009                    Some(IntrinsicId::Field) => {
1010                        // Return type depends on which field is accessed — fresh var.
1011                        visit_args(self, ctx);
1012                        InferType::Var(self.fresh_var())
1013                    }
1014                    // ADR-0064: slice intrinsics. Sema produces the actual
1015                    // type once it has the receiver/argument types resolved;
1016                    // here we just emit a fresh variable.
1017                    Some(IntrinsicId::SliceLen) => {
1018                        visit_args(self, ctx);
1019                        InferType::Concrete(Type::USIZE)
1020                    }
1021                    Some(IntrinsicId::SliceIsEmpty) => {
1022                        visit_args(self, ctx);
1023                        InferType::Concrete(Type::BOOL)
1024                    }
1025                    Some(IntrinsicId::SliceIndexRead) => {
1026                        visit_args(self, ctx);
1027                        InferType::Var(self.fresh_var())
1028                    }
1029                    Some(IntrinsicId::SliceIndexWrite) => {
1030                        visit_args(self, ctx);
1031                        InferType::Concrete(Type::UNIT)
1032                    }
1033                    Some(IntrinsicId::SlicePtr)
1034                    | Some(IntrinsicId::SlicePtrMut)
1035                    | Some(IntrinsicId::PartsToSlice)
1036                    | Some(IntrinsicId::PartsToMutSlice) => {
1037                        visit_args(self, ctx);
1038                        InferType::Var(self.fresh_var())
1039                    }
1040                    Some(IntrinsicId::EmbedFile) => {
1041                        // `@embed_file("path")` always produces a `Slice(u8)`,
1042                        // independent of context. Visiting args is a no-op
1043                        // here (string literal) but kept for consistency.
1044                        visit_args(self, ctx);
1045                        let slice_id = self.type_pool.intern_slice_from_type(Type::U8);
1046                        InferType::Concrete(Type::new_slice(slice_id))
1047                    }
1048                    Some(IntrinsicId::Panic) | Some(IntrinsicId::CompileError) => {
1049                        // Diverging intrinsics return Never so they unify with any
1050                        // expected type (e.g. `if c { 42 } else { @panic("..") }`).
1051                        visit_args(self, ctx);
1052                        InferType::Concrete(Type::NEVER)
1053                    }
1054                    // ADR-0087 Phase 3: @utf8_validate retired — see prelude
1055                    // `utf8_validate(s)` fn.
1056                    Some(IntrinsicId::CStrToVec) => {
1057                        // ADR-0072: returns Vec(u8).
1058                        visit_args(self, ctx);
1059                        let vec_id = self.type_pool.intern_vec_from_type(Type::U8);
1060                        InferType::Concrete(Type::new_vec(vec_id))
1061                    }
1062                    // ADR-0087 Phase 4: @alloc / @realloc / @free retired —
1063                    // see prelude `mem_alloc` / `mem_realloc` / `mem_free`
1064                    // fns. `@ptr_cast` remains and still infers its result
1065                    // from the binding context.
1066                    Some(IntrinsicId::PtrCast) => {
1067                        visit_args(self, ctx);
1068                        InferType::Var(self.fresh_var())
1069                    }
1070                    // ADR-0087 Phase 3: @bytes_eq retired — see prelude
1071                    // `bytes_eq(a, b, n)` fn.
1072                    // ADR-0079 Phase 2b: `@field_set` is a unit-yielding side
1073                    // effect on an uninit handle. `@finalize` returns the
1074                    // host type — but at HM time we don't yet know the
1075                    // handle's target type, so a fresh var lets sema
1076                    // resolve it from the side-table. `@variant_field`
1077                    // similarly resolves only with the handle's variant.
1078                    Some(IntrinsicId::FieldSet) => {
1079                        visit_args(self, ctx);
1080                        InferType::Concrete(Type::UNIT)
1081                    }
1082                    Some(IntrinsicId::Finalize) | Some(IntrinsicId::VariantField) => {
1083                        visit_args(self, ctx);
1084                        InferType::Var(self.fresh_var())
1085                    }
1086                    // `@variant_uninit(T, tag)` and `@uninit(T)` only
1087                    // appear in `let h = …` position, where sema captures
1088                    // them via the side-table; the `Alloc` itself yields
1089                    // unit. Use a fresh var so escapes (currently
1090                    // diagnosed in sema) don't get a phantom unit
1091                    // constraint that confuses error messages.
1092                    Some(IntrinsicId::VariantUninit) => {
1093                        visit_args(self, ctx);
1094                        InferType::Var(self.fresh_var())
1095                    }
1096                    // ADR-0084: @spawn returns a JoinHandle(R) whose
1097                    // R is the spawned function's return type. Sema's
1098                    // analyze_spawn_intrinsic instantiates the
1099                    // parameterized type and writes the concrete
1100                    // result into AIR; the inference layer just
1101                    // hands back a fresh var so HM doesn't constrain
1102                    // it prematurely. (Mirrors the @uninit pattern.)
1103                    Some(IntrinsicId::Spawn) => {
1104                        visit_args(self, ctx);
1105                        InferType::Var(self.fresh_var())
1106                    }
1107                    // ADR-0084: @thread_join's result type comes from
1108                    // the binding context (let-annotation or function
1109                    // return). Same pattern as @cast / @alloc — emit
1110                    // a fresh var so HM unifies it with the user's
1111                    // annotation.
1112                    Some(IntrinsicId::ThreadJoin) => {
1113                        visit_args(self, ctx);
1114                        InferType::Var(self.fresh_var())
1115                    }
1116                    // Other intrinsics (@dbg, @assert, @test_preview_gate, @import)
1117                    // and any unknown name return Unit. Sema handles the unknown case
1118                    // with a proper diagnostic; we just pick a coherent type here.
1119                    _ => {
1120                        visit_args(self, ctx);
1121                        InferType::Concrete(Type::UNIT)
1122                    }
1123                }
1124            }
1125
1126            // Type intrinsic (@size_of, @align_of, @type_name, @type_info, @ownership)
1127            InstData::TypeIntrinsic { name, type_arg: _ } => {
1128                let intrinsic_name = self.interner.resolve(name);
1129                match lookup_by_name(intrinsic_name).map(|d| d.id) {
1130                    Some(IntrinsicId::TypeName) => InferType::Concrete(Type::COMPTIME_STR),
1131                    Some(IntrinsicId::TypeInfo) => {
1132                        // @type_info returns a comptime struct — use a fresh var
1133                        // since the actual type is determined by the comptime evaluator.
1134                        InferType::Var(self.fresh_var())
1135                    }
1136                    Some(IntrinsicId::SizeOf) | Some(IntrinsicId::AlignOf) => {
1137                        // @size_of / @align_of return `usize` (ADR-0054).
1138                        InferType::Concrete(Type::USIZE)
1139                    }
1140                    Some(IntrinsicId::Ownership) => {
1141                        // @ownership returns the built-in `Ownership` enum.
1142                        if let Some(ownership_spur) = self.interner.get("Ownership") {
1143                            if let Some(&ownership_ty) = self.enums.get(&ownership_spur) {
1144                                InferType::Concrete(ownership_ty)
1145                            } else {
1146                                InferType::Concrete(Type::ERROR)
1147                            }
1148                        } else {
1149                            InferType::Concrete(Type::ERROR)
1150                        }
1151                    }
1152                    Some(IntrinsicId::ThreadSafety) => {
1153                        // ADR-0084: @thread_safety returns the prelude
1154                        // `ThreadSafety` enum. Same lookup shape as
1155                        // @ownership.
1156                        if let Some(spur) = self.interner.get("ThreadSafety") {
1157                            if let Some(&ty) = self.enums.get(&spur) {
1158                                InferType::Concrete(ty)
1159                            } else {
1160                                InferType::Concrete(Type::ERROR)
1161                            }
1162                        } else {
1163                            InferType::Concrete(Type::ERROR)
1164                        }
1165                    }
1166                    // ADR-0079 Phase 2b: `@uninit(T)` only appears in
1167                    // `let h = …` position, where sema captures it via the
1168                    // side-table. Use a fresh var so the alloc's
1169                    // surrounding context resolves coherently.
1170                    Some(IntrinsicId::Uninit) => InferType::Var(self.fresh_var()),
1171                    // Fallback for unknown names.
1172                    _ => InferType::Concrete(Type::I32),
1173                }
1174            }
1175
1176            // Type+interface intrinsic (@implements)
1177            InstData::TypeInterfaceIntrinsic { name, .. } => {
1178                let intrinsic_name = self.interner.resolve(name);
1179                match lookup_by_name(intrinsic_name).map(|d| d.id) {
1180                    Some(IntrinsicId::Implements) => InferType::Concrete(Type::BOOL),
1181                    _ => InferType::Concrete(Type::ERROR),
1182                }
1183            }
1184
1185            // Block
1186            InstData::Block { extra_start, len } => {
1187                ctx.push_scope();
1188                let mut last_ty = InferType::Concrete(Type::UNIT);
1189                let block_insts = self.rir.get_extra(*extra_start, *len);
1190                for &inst_raw in block_insts {
1191                    let block_inst_ref = InstRef::from_raw(inst_raw);
1192                    let info = self.generate(block_inst_ref, ctx);
1193                    last_ty = info.ty;
1194                }
1195                ctx.pop_scope();
1196                last_ty
1197            }
1198
1199            // Branch (if/else). ADR-0079 follow-up: HM walks both
1200            // branches even for `comptime if`, since we don't yet
1201            // know the comptime-cond value here. Branches in our
1202            // use case (struct vs enum derive bodies) are
1203            // HM-permissive (uninit/finalize/field_set return fresh
1204            // vars), so this doesn't constrain. Sema does the
1205            // actual branch elision when it has the resolved type.
1206            InstData::Branch {
1207                cond,
1208                then_block,
1209                else_block,
1210                is_comptime: _,
1211            } => {
1212                let cond_info = self.generate(*cond, ctx);
1213                self.add_constraint(Constraint::equal(
1214                    cond_info.ty,
1215                    InferType::Concrete(Type::BOOL),
1216                    cond_info.span,
1217                ));
1218
1219                let then_info = self.generate(*then_block, ctx);
1220
1221                if let Some(else_ref) = else_block {
1222                    let else_info = self.generate(*else_ref, ctx);
1223
1224                    // Handle Never type coercion:
1225                    // - If one branch is Never, the if-else takes the other branch's type
1226                    // - If both are Never, the result is Never
1227                    // - Otherwise, both must unify to the same type
1228                    let then_is_never = matches!(&then_info.ty, InferType::Concrete(Type::NEVER));
1229                    let else_is_never = matches!(&else_info.ty, InferType::Concrete(Type::NEVER));
1230
1231                    match (then_is_never, else_is_never) {
1232                        (true, true) => {
1233                            // Both diverge - result is Never
1234                            InferType::Concrete(Type::NEVER)
1235                        }
1236                        (true, false) => {
1237                            // Then diverges - result is else type
1238                            else_info.ty
1239                        }
1240                        (false, true) => {
1241                            // Else diverges - result is then type
1242                            then_info.ty
1243                        }
1244                        (false, false) => {
1245                            // Neither diverges - both must have the same type
1246                            let result_var = self.fresh_var();
1247                            let result_ty = InferType::Var(result_var);
1248                            self.add_constraint(Constraint::equal(
1249                                then_info.ty,
1250                                result_ty.clone(),
1251                                then_info.span,
1252                            ));
1253                            self.add_constraint(Constraint::equal(
1254                                else_info.ty,
1255                                result_ty.clone(),
1256                                else_info.span,
1257                            ));
1258                            result_ty
1259                        }
1260                    }
1261                } else {
1262                    // No else branch - the if expression has unit type
1263                    // (or the then branch type if it's unit-compatible)
1264                    InferType::Concrete(Type::UNIT)
1265                }
1266            }
1267
1268            // While loop
1269            InstData::Loop { cond, body } => {
1270                let cond_info = self.generate(*cond, ctx);
1271                self.add_constraint(Constraint::equal(
1272                    cond_info.ty,
1273                    InferType::Concrete(Type::BOOL),
1274                    cond_info.span,
1275                ));
1276
1277                ctx.loop_depth += 1;
1278                self.generate(*body, ctx);
1279                ctx.loop_depth -= 1;
1280
1281                // Loops produce unit
1282                InferType::Concrete(Type::UNIT)
1283            }
1284
1285            // For-in loop (desugared to while in sema, but inference still sees it)
1286            InstData::For {
1287                binding,
1288                is_mut,
1289                iterable,
1290                body,
1291            } => {
1292                // Generate constraints for the iterable to determine the element type
1293                let iterable_info = self.generate(*iterable, ctx);
1294
1295                // Determine the binding type from the iterable:
1296                // - For @range: the iterable returns the integer type directly
1297                // - For arrays: extract the element type from InferType::Array
1298                // - For slices (ADR-0064): extract the element from
1299                //   `Slice(T)` / `MutSlice(T)`
1300                let binding_ty = match &iterable_info.ty {
1301                    InferType::Array { element, .. } => *element.clone(),
1302                    InferType::Concrete(t) => match t.kind() {
1303                        TypeKind::Slice(id) => InferType::Concrete(self.type_pool.slice_def(id)),
1304                        TypeKind::MutSlice(id) => {
1305                            InferType::Concrete(self.type_pool.mut_slice_def(id))
1306                        }
1307                        TypeKind::Vec(id) => InferType::Concrete(self.type_pool.vec_def(id)),
1308                        _ => iterable_info.ty.clone(),
1309                    },
1310                    other => other.clone(),
1311                };
1312
1313                // Register the binding so the body can reference it
1314                ctx.insert_local(
1315                    *binding,
1316                    LocalVarInfo {
1317                        ty: binding_ty,
1318                        is_mut: *is_mut,
1319                        span,
1320                    },
1321                );
1322
1323                ctx.loop_depth += 1;
1324                self.generate(*body, ctx);
1325                ctx.loop_depth -= 1;
1326
1327                // For loops produce unit
1328                InferType::Concrete(Type::UNIT)
1329            }
1330
1331            // Infinite loop
1332            InstData::InfiniteLoop { body } => {
1333                ctx.loop_depth += 1;
1334                self.generate(*body, ctx);
1335                ctx.loop_depth -= 1;
1336
1337                // Infinite loop without break never returns
1338                InferType::Concrete(Type::NEVER)
1339            }
1340
1341            // Break/Continue
1342            InstData::Break | InstData::Continue => InferType::Concrete(Type::NEVER),
1343
1344            // Match expression
1345            InstData::Match {
1346                scrutinee,
1347                arms_start,
1348                arms_len,
1349            } => {
1350                let scrutinee_info = self.generate(*scrutinee, ctx);
1351                let arms = self.rir.get_match_arms(*arms_start, *arms_len);
1352
1353                // Collect arm types, handling Never coercion
1354                let mut arm_types: Vec<ExprInfo> = Vec::new();
1355                for (pattern, body) in arms.iter() {
1356                    // Patterns constrain the scrutinee type
1357                    let pattern_ty = self.pattern_type(pattern);
1358                    self.add_constraint(Constraint::equal(
1359                        scrutinee_info.ty.clone(),
1360                        pattern_ty,
1361                        pattern.span(),
1362                    ));
1363
1364                    // For DataVariant/StructVariant patterns, add bound variables to scope before
1365                    // generating body constraints, so VarRef lookups resolve correctly.
1366                    let bindings_to_remove = match pattern {
1367                        gruel_rir::RirPattern::DataVariant {
1368                            type_name,
1369                            variant,
1370                            bindings,
1371                            ..
1372                        } => {
1373                            let mut added_bindings = Vec::new();
1374                            if let Some(&enum_ty) = self.enums.get(type_name)
1375                                && let Some(enum_id) = enum_ty.as_enum()
1376                            {
1377                                let enum_def = self.type_pool.enum_def(enum_id);
1378                                let variant_name = self.interner.resolve(variant);
1379                                if let Some(variant_idx) = enum_def.find_variant(variant_name) {
1380                                    let field_types = &enum_def.variants[variant_idx].fields;
1381                                    for (i, binding) in bindings.iter().enumerate() {
1382                                        let field_ty = if i < field_types.len() {
1383                                            InferType::Concrete(field_types[i])
1384                                        } else {
1385                                            InferType::Concrete(Type::ERROR)
1386                                        };
1387                                        self.register_binding(
1388                                            binding,
1389                                            field_ty,
1390                                            pattern.span(),
1391                                            ctx,
1392                                            &mut added_bindings,
1393                                        );
1394                                    }
1395                                }
1396                            } else {
1397                                // Enum not found — likely a comptime type variable.
1398                                // Register bindings with fresh type variables so body
1399                                // constraint generation can still resolve variable references.
1400                                for binding in bindings.iter() {
1401                                    let var = self.fresh_var();
1402                                    let ty = InferType::Var(var);
1403                                    self.register_binding(
1404                                        binding,
1405                                        ty,
1406                                        pattern.span(),
1407                                        ctx,
1408                                        &mut added_bindings,
1409                                    );
1410                                }
1411                            }
1412                            added_bindings
1413                        }
1414                        gruel_rir::RirPattern::StructVariant {
1415                            type_name,
1416                            variant,
1417                            field_bindings,
1418                            ..
1419                        } => {
1420                            let mut added_bindings = Vec::new();
1421                            if let Some(&enum_ty) = self.enums.get(type_name)
1422                                && let Some(enum_id) = enum_ty.as_enum()
1423                            {
1424                                let enum_def = self.type_pool.enum_def(enum_id);
1425                                let variant_name = self.interner.resolve(variant);
1426                                if let Some(variant_idx) = enum_def.find_variant(variant_name) {
1427                                    let variant_def = &enum_def.variants[variant_idx];
1428                                    for fb in field_bindings {
1429                                        let field_name = self.interner.resolve(&fb.field_name);
1430                                        let field_ty =
1431                                            if let Some(idx) = variant_def.find_field(field_name) {
1432                                                InferType::Concrete(variant_def.fields[idx])
1433                                            } else {
1434                                                InferType::Concrete(Type::ERROR)
1435                                            };
1436                                        self.register_binding(
1437                                            &fb.binding,
1438                                            field_ty,
1439                                            pattern.span(),
1440                                            ctx,
1441                                            &mut added_bindings,
1442                                        );
1443                                    }
1444                                }
1445                            } else {
1446                                // Enum not found — likely a comptime type variable.
1447                                // Register bindings with fresh type variables.
1448                                for fb in field_bindings {
1449                                    let var = self.fresh_var();
1450                                    let ty = InferType::Var(var);
1451                                    self.register_binding(
1452                                        &fb.binding,
1453                                        ty,
1454                                        pattern.span(),
1455                                        ctx,
1456                                        &mut added_bindings,
1457                                    );
1458                                }
1459                            }
1460                            added_bindings
1461                        }
1462                        // ADR-0051 Phase 4c: register Ident-leaf bindings for
1463                        // Tuple / Struct / Ident arm roots. We walk the
1464                        // pattern tree recursively, pulling field types from
1465                        // the scrutinee's concrete struct definition when
1466                        // available; unknown types get fresh variables so
1467                        // body constraint generation still finds the binding.
1468                        gruel_rir::RirPattern::Ident { .. }
1469                        | gruel_rir::RirPattern::Tuple { .. }
1470                        | gruel_rir::RirPattern::Struct { .. } => {
1471                            let mut added_bindings = Vec::new();
1472                            self.collect_recursive_pattern_bindings(
1473                                pattern,
1474                                scrutinee_info.ty.clone(),
1475                                ctx,
1476                                &mut added_bindings,
1477                            );
1478                            added_bindings
1479                        }
1480                        _ => Vec::new(),
1481                    };
1482
1483                    // Generate body and collect its type
1484                    let body_info = self.generate(*body, ctx);
1485                    arm_types.push(body_info);
1486
1487                    // Remove DataVariant bindings from scope after body generation
1488                    for (name, old_val) in bindings_to_remove {
1489                        match old_val {
1490                            Some(prev) => {
1491                                ctx.locals.insert(name, prev);
1492                            }
1493                            None => {
1494                                ctx.locals.remove(&name);
1495                            }
1496                        }
1497                    }
1498                }
1499
1500                // Handle Never type coercion:
1501                // Filter out Never arms and use the remaining non-Never types
1502                let non_never_arms: Vec<_> = arm_types
1503                    .iter()
1504                    .filter(|info| !matches!(&info.ty, InferType::Concrete(Type::NEVER)))
1505                    .collect();
1506
1507                if non_never_arms.is_empty() {
1508                    // All arms diverge - result is Never
1509                    InferType::Concrete(Type::NEVER)
1510                } else {
1511                    // Create constraints for non-Never arms to have the same type
1512                    let result_var = self.fresh_var();
1513                    let result_ty = InferType::Var(result_var);
1514                    for arm_info in non_never_arms {
1515                        self.add_constraint(Constraint::equal(
1516                            arm_info.ty.clone(),
1517                            result_ty.clone(),
1518                            arm_info.span,
1519                        ));
1520                    }
1521                    result_ty
1522                }
1523            }
1524
1525            // Struct initialization
1526            InstData::StructInit {
1527                type_name,
1528                fields_start,
1529                fields_len,
1530                ..
1531            } => {
1532                // Check type_subst first (for Self and type parameters in method bodies)
1533                let struct_ty = self
1534                    .type_subst
1535                    .and_then(|subst| subst.get(type_name).copied())
1536                    .or_else(|| self.structs.get(type_name).copied());
1537
1538                if let Some(struct_ty) = struct_ty {
1539                    let fields = self.rir.get_field_inits(*fields_start, *fields_len);
1540                    // Generate constraints for each field
1541                    for (_, value_ref) in fields.iter() {
1542                        self.generate(*value_ref, ctx);
1543                    }
1544                    InferType::Concrete(struct_ty)
1545                } else {
1546                    InferType::Concrete(Type::ERROR)
1547                }
1548            }
1549
1550            // Field access
1551            InstData::FieldGet { base, field } => {
1552                // Generate constraints for the base expression (needed for nested field access)
1553                let base_info = self.generate(*base, ctx);
1554                // ADR-0082: when the base has a concrete struct type
1555                // (whether normal or after auto-deref through Ref/MutRef
1556                // — `self: Ref(Self)` is the load-bearing case for vec
1557                // methods like `is_empty` whose body is `self.len == 0`),
1558                // look up the field's declared type so the integer
1559                // literal on the other side of the comparison is pinned
1560                // to it. Without this, FieldGet returns a fresh var,
1561                // the literal defaults to i32, and codegen sees an
1562                // i64-vs-i32 mismatch when the field is `usize`.
1563                let mut field_ty: Option<crate::types::Type> = None;
1564                if let InferType::Concrete(base_ty) = &base_info.ty {
1565                    let resolved = match base_ty.kind() {
1566                        crate::types::TypeKind::Struct(_) => Some(*base_ty),
1567                        crate::types::TypeKind::Ref(id) => Some(self.type_pool.ref_def(id)),
1568                        crate::types::TypeKind::MutRef(id) => Some(self.type_pool.mut_ref_def(id)),
1569                        _ => None,
1570                    };
1571                    if let Some(struct_ty) = resolved
1572                        && let Some(struct_id) = struct_ty.as_struct()
1573                    {
1574                        let struct_def = self.type_pool.struct_def(struct_id);
1575                        let field_name = self.interner.resolve(field);
1576                        if let Some((_, sf)) = struct_def.find_field(field_name) {
1577                            field_ty = Some(sf.ty);
1578                        }
1579                    }
1580                }
1581                if let Some(ty) = field_ty {
1582                    InferType::Concrete(ty)
1583                } else {
1584                    // Fall back to fresh var if we don't yet know the
1585                    // base's type (sema fixes this up post-HM).
1586                    let result_var = self.fresh_var();
1587                    InferType::Var(result_var)
1588                }
1589            }
1590
1591            // Field assignment
1592            InstData::FieldSet {
1593                base,
1594                field: _,
1595                value,
1596            } => {
1597                self.generate(*base, ctx);
1598                self.generate(*value, ctx);
1599                InferType::Concrete(Type::UNIT)
1600            }
1601
1602            // Enum variant (unit or path)
1603            InstData::EnumVariant { type_name, .. } => {
1604                if let Some(&enum_ty) = self.enums.get(type_name) {
1605                    InferType::Concrete(enum_ty)
1606                } else {
1607                    // May be a comptime type variable — use fresh var
1608                    let var = self.fresh_var();
1609                    InferType::Var(var)
1610                }
1611            }
1612
1613            // Enum struct variant construction
1614            InstData::EnumStructVariant {
1615                type_name,
1616                fields_start,
1617                fields_len,
1618                ..
1619            } => {
1620                // Generate constraints for field value expressions
1621                let fields = self.rir.get_field_inits(*fields_start, *fields_len);
1622                for (_, value_ref) in fields.iter() {
1623                    self.generate(*value_ref, ctx);
1624                }
1625                if let Some(&enum_ty) = self.enums.get(type_name) {
1626                    InferType::Concrete(enum_ty)
1627                } else {
1628                    // May be a comptime type variable — use fresh var
1629                    let var = self.fresh_var();
1630                    InferType::Var(var)
1631                }
1632            }
1633
1634            // Array initialization
1635            InstData::ArrayInit {
1636                elems_start,
1637                elems_len,
1638            } => {
1639                let elements = self.rir.get_inst_refs(*elems_start, *elems_len);
1640                if elements.is_empty() {
1641                    // Empty array - need type annotation to know element type
1642                    // Use a fresh type variable for the element type
1643                    let elem_var = self.fresh_var();
1644                    InferType::Array {
1645                        element: Box::new(InferType::Var(elem_var)),
1646                        length: 0,
1647                    }
1648                } else {
1649                    // Get element type from first element, constrain rest to match
1650                    let first_info = self.generate(elements[0], ctx);
1651                    for elem_ref in elements.iter().skip(1) {
1652                        let elem_info = self.generate(*elem_ref, ctx);
1653                        self.add_constraint(Constraint::equal(
1654                            elem_info.ty,
1655                            first_info.ty.clone(),
1656                            elem_info.span,
1657                        ));
1658                    }
1659                    // Build the array type with the inferred element type
1660                    InferType::Array {
1661                        element: Box::new(first_info.ty),
1662                        length: elements.len() as u64,
1663                    }
1664                }
1665            }
1666
1667            // Array index
1668            InstData::IndexGet { base, index } => {
1669                let base_info = self.generate(*base, ctx);
1670                let index_info = self.generate(*index, ctx);
1671                // Index must be exactly `usize` (ADR-0054).
1672                self.add_constraint(Constraint::equal(
1673                    InferType::Concrete(Type::USIZE),
1674                    index_info.ty.clone(),
1675                    index_info.span,
1676                ));
1677
1678                // Extract element type from array type.
1679                // If base is InferType::Array, we can get the element type directly.
1680                // Otherwise, we need a fresh variable that will be resolved later.
1681                match &base_info.ty {
1682                    InferType::Array { element, .. } => (**element).clone(),
1683                    _ => {
1684                        // Base might be a type variable that will resolve to an array.
1685                        // Use a fresh variable for the element type.
1686                        let result_var = self.fresh_var();
1687                        InferType::Var(result_var)
1688                    }
1689                }
1690            }
1691
1692            // Array index assignment
1693            InstData::IndexSet { base, index, value } => {
1694                let base_info = self.generate(*base, ctx);
1695                let index_info = self.generate(*index, ctx);
1696                // Index must be exactly `usize` (ADR-0054).
1697                self.add_constraint(Constraint::equal(
1698                    InferType::Concrete(Type::USIZE),
1699                    index_info.ty.clone(),
1700                    index_info.span,
1701                ));
1702
1703                let value_info = self.generate(*value, ctx);
1704
1705                // Constrain value type to match array element type
1706                if let InferType::Array { element, .. } = &base_info.ty {
1707                    self.add_constraint(Constraint::equal(
1708                        value_info.ty,
1709                        (**element).clone(),
1710                        value_info.span,
1711                    ));
1712                }
1713
1714                InferType::Concrete(Type::UNIT)
1715            }
1716
1717            // Type declarations don't produce values
1718            InstData::FnDecl { .. }
1719            | InstData::StructDecl { .. }
1720            | InstData::EnumDecl { .. }
1721            | InstData::InterfaceDecl { .. }
1722            | InstData::InterfaceMethodSig { .. }
1723            | InstData::DeriveDecl { .. }
1724            | InstData::ConstDecl { .. } => InferType::Concrete(Type::UNIT),
1725
1726            // ADR-0057: anonymous interface type expressions yield a
1727            // comptime type value (parallel to AnonStructType /
1728            // AnonEnumType, which are typed COMPTIME_TYPE elsewhere via
1729            // their dedicated arms).
1730            InstData::AnonInterfaceType { .. } => InferType::Concrete(Type::COMPTIME_TYPE),
1731
1732            // Method call: receiver.method(args)
1733            InstData::MethodCall {
1734                receiver,
1735                method,
1736                args_start,
1737                args_len,
1738            } => {
1739                // Generate type for receiver
1740                let receiver_info = self.generate(*receiver, ctx);
1741                let args = self.rir.get_call_args(*args_start, *args_len);
1742
1743                // Get struct name from receiver type if it's a struct
1744                // If we can't determine the struct type, we still generate constraints
1745                // for the arguments and return a type variable (actual error is in sema)
1746
1747                if let InferType::Concrete(ty) = &receiver_info.ty {
1748                    // ADR-0063: methods on `Ptr(T)` / `MutPtr(T)` resolve via
1749                    // the POINTER_METHODS registry. Fan out per method here
1750                    // so HM produces the right constraint shape; sema does
1751                    // the real type-checking.
1752                    if matches!(ty.kind(), TypeKind::PtrConst(_) | TypeKind::PtrMut(_)) {
1753                        let method_str = self.interner.resolve(method);
1754                        let pointee = match ty.kind() {
1755                            TypeKind::PtrConst(id) => self.type_pool.ptr_const_def(id),
1756                            TypeKind::PtrMut(id) => self.type_pool.ptr_mut_def(id),
1757                            _ => unreachable!(),
1758                        };
1759                        // Generate inference for args (no constraints — sema
1760                        // will type-check).
1761                        for arg in args.iter() {
1762                            self.generate(arg.value, ctx);
1763                        }
1764                        return ExprInfo {
1765                            ty: match method_str {
1766                                "read" | "read_volatile" => InferType::Concrete(pointee),
1767                                "write" | "write_volatile" => InferType::Concrete(Type::UNIT),
1768                                "offset" => InferType::Concrete(*ty),
1769                                "is_null" => InferType::Concrete(Type::BOOL),
1770                                "to_int" => InferType::Concrete(Type::U64),
1771                                "copy_from" => InferType::Concrete(Type::UNIT),
1772                                _ => InferType::Concrete(Type::ERROR),
1773                            },
1774                            span,
1775                        };
1776                    }
1777
1778                    if let Some(struct_id) = ty.as_struct() {
1779                        // Use StructId directly for method lookup
1780                        let method_key = (struct_id, *method);
1781                        if let Some(method_sig) = self.methods.get(&method_key) {
1782                            // Generate constraints for arguments
1783                            for (arg, param_type) in args.iter().zip(method_sig.param_types.iter())
1784                            {
1785                                let arg_info = self.generate(arg.value, ctx);
1786                                self.add_constraint(Constraint::equal(
1787                                    arg_info.ty,
1788                                    param_type.clone(),
1789                                    arg_info.span,
1790                                ));
1791                            }
1792                            method_sig.return_type.clone()
1793                        } else {
1794                            // Method not found in pre-built context - may be an anonymous
1795                            // struct method registered during comptime evaluation.
1796                            // Use a fresh type variable so inference doesn't poison
1797                            // surrounding expressions with ERROR.
1798                            for arg in args.iter() {
1799                                self.generate(arg.value, ctx);
1800                            }
1801                            InferType::Var(self.fresh_var())
1802                        }
1803                    } else if let Some(enum_id) = ty.as_enum() {
1804                        // Enum receiver - check enum_methods
1805                        let method_key = (enum_id, *method);
1806                        if let Some(method_sig) = self.enum_methods.get(&method_key) {
1807                            for (arg, param_type) in args.iter().zip(method_sig.param_types.iter())
1808                            {
1809                                let arg_info = self.generate(arg.value, ctx);
1810                                self.add_constraint(Constraint::equal(
1811                                    arg_info.ty,
1812                                    param_type.clone(),
1813                                    arg_info.span,
1814                                ));
1815                            }
1816                            method_sig.return_type.clone()
1817                        } else {
1818                            // Enum method not found - use fresh var, sema reports error
1819                            for arg in args.iter() {
1820                                self.generate(arg.value, ctx);
1821                            }
1822                            InferType::Var(self.fresh_var())
1823                        }
1824                    } else {
1825                        // Non-struct/non-enum receiver - sema will report the error
1826                        for arg in args.iter() {
1827                            self.generate(arg.value, ctx);
1828                        }
1829                        InferType::Concrete(Type::ERROR)
1830                    }
1831                } else {
1832                    // Non-concrete receiver type - use fresh var, sema resolves it
1833                    for arg in args.iter() {
1834                        self.generate(arg.value, ctx);
1835                    }
1836                    InferType::Var(self.fresh_var())
1837                }
1838            }
1839
1840            // Associated function call: Type::function(args)
1841            InstData::AssocFnCall {
1842                type_name,
1843                function,
1844                args_start,
1845                args_len,
1846            } => {
1847                let args = self.rir.get_call_args(*args_start, *args_len);
1848                // Get struct ID from type name for method lookup
1849                let struct_id = self.structs.get(type_name).and_then(|ty| ty.as_struct());
1850
1851                if let Some(struct_id) = struct_id {
1852                    let method_key = (struct_id, *function);
1853                    if let Some(method_sig) = self.methods.get(&method_key) {
1854                        // Generate constraints for arguments
1855                        for (arg, param_type) in args.iter().zip(method_sig.param_types.iter()) {
1856                            let arg_info = self.generate(arg.value, ctx);
1857                            self.add_constraint(Constraint::equal(
1858                                arg_info.ty,
1859                                param_type.clone(),
1860                                arg_info.span,
1861                            ));
1862                        }
1863                        method_sig.return_type.clone()
1864                    } else {
1865                        // Method not found - may be an anonymous struct method
1866                        // registered during comptime. Use fresh var to avoid ERROR poison.
1867                        for arg in args.iter() {
1868                            self.generate(arg.value, ctx);
1869                        }
1870                        InferType::Var(self.fresh_var())
1871                    }
1872                } else {
1873                    // Type not found in pre-built context - may be a comptime type var.
1874                    // Use fresh var so inference doesn't poison surrounding expressions.
1875                    for arg in args.iter() {
1876                        self.generate(arg.value, ctx);
1877                    }
1878                    InferType::Var(self.fresh_var())
1879                }
1880            }
1881
1882            // Comptime block: the type depends on whether evaluation succeeds at compile time.
1883            // For type inference, we use a fresh type variable that can unify with
1884            // whatever type is expected from the context (e.g., a let binding's type annotation).
1885            // Similar to integer literals, comptime blocks can adapt to their context.
1886            InstData::Comptime { expr: _ } => {
1887                // Comptime blocks are fully evaluated by the comptime interpreter in sema,
1888                // which handles its own type checking (comptime_str, TypeInfo structs, etc.).
1889                // We don't generate constraints for the inner expression because comptime
1890                // has types (comptime_str, anonymous structs from @typeInfo) that don't
1891                // exist in the regular type system and would cause false unification errors.
1892                // Use a fresh variable so comptime can unify with whatever the context expects.
1893                let var = self.fresh_var();
1894                self.int_literal_vars.push(var);
1895                InferType::Var(var)
1896            }
1897
1898            // Comptime unroll for: the iterable is evaluated at comptime, the body is unrolled.
1899            // Like regular for loops, the result type is unit.
1900            // We must generate constraints for the body so that type inference resolves
1901            // types for runtime expressions inside the loop body (e.g., `total + 1`).
1902            // The binding variable holds a comptime value and is handled by sema, but
1903            // we register it as an integer type variable so VarRef lookups don't fail.
1904            InstData::ComptimeUnrollFor {
1905                binding,
1906                iterable,
1907                body,
1908            } => {
1909                // Generate constraints for the iterable (it's a comptime block)
1910                self.generate(*iterable, ctx);
1911
1912                // Register the binding as a fresh type variable.
1913                // The actual comptime value type is determined by sema, but HM
1914                // inference needs the binding in scope so VarRef doesn't fail.
1915                let binding_ty = {
1916                    let var = self.fresh_var();
1917                    InferType::Var(var)
1918                };
1919                ctx.insert_local(
1920                    *binding,
1921                    LocalVarInfo {
1922                        ty: binding_ty,
1923                        is_mut: false,
1924                        span,
1925                    },
1926                );
1927
1928                // Generate constraints for the body
1929                self.generate(*body, ctx);
1930
1931                InferType::Concrete(Type::UNIT)
1932            }
1933
1934            // Checked block: for type inference purposes, the type is the type of the inner expression
1935            // The actual checking of unchecked operations happens in sema
1936            InstData::Checked { expr } => {
1937                // Generate constraints for the inner expression
1938                let inner_info = self.generate(*expr, ctx);
1939                inner_info.ty
1940            }
1941
1942            // Type constant: a type used as a value (e.g., `i32` in `identity(i32, 42)`)
1943            // This has the special ComptimeType type which indicates it's a type value.
1944            InstData::TypeConst { .. } => InferType::Concrete(Type::COMPTIME_TYPE),
1945
1946            // Anonymous struct type: a struct type used as a comptime value
1947            // This also has the ComptimeType type.
1948            InstData::AnonStructType { .. } => InferType::Concrete(Type::COMPTIME_TYPE),
1949
1950            // Anonymous enum type: an enum type used as a comptime value
1951            // This also has the ComptimeType type.
1952            InstData::AnonEnumType { .. } => InferType::Concrete(Type::COMPTIME_TYPE),
1953
1954            // Tuple lowering (ADR-0048): defer to a fresh type variable. The sema
1955            // layer resolves tuples to anonymous structs during analysis; inference
1956            // does not need a concrete shape here.
1957            InstData::TupleInit {
1958                elems_start,
1959                elems_len,
1960            } => {
1961                let elems = self.rir.get_inst_refs(*elems_start, *elems_len);
1962                for elem in elems {
1963                    self.generate(elem, ctx);
1964                }
1965                let result_var = self.fresh_var();
1966                InferType::Var(result_var)
1967            }
1968
1969            // Anonymous function value (ADR-0055): sema lowers to a fresh anon
1970            // struct with a `__call` method, then emits a StructInit against
1971            // that struct. Inference defers to a fresh type variable here —
1972            // analysis.rs supplies the concrete struct type.
1973            InstData::AnonFnValue { .. } => {
1974                let result_var = self.fresh_var();
1975                InferType::Var(result_var)
1976            }
1977        };
1978
1979        // Record the type for this expression
1980        self.record_type(inst_ref, ty.clone());
1981        ExprInfo::new(ty, span)
1982    }
1983
1984    /// Generate constraints for a binary arithmetic operation (+, -, *, /, %).
1985    ///
1986    /// Operands must be numeric (integer or float).
1987    fn generate_binary_arith(
1988        &mut self,
1989        lhs: InstRef,
1990        rhs: InstRef,
1991        ctx: &mut ConstraintContext,
1992    ) -> InferType {
1993        let lhs_info = self.generate(lhs, ctx);
1994        let rhs_info = self.generate(rhs, ctx);
1995
1996        let result_var = self.fresh_var();
1997        let result_ty = InferType::Var(result_var);
1998
1999        self.add_constraint(Constraint::equal(
2000            self.auto_deref(lhs_info.ty),
2001            result_ty.clone(),
2002            lhs_info.span,
2003        ));
2004        self.add_constraint(Constraint::equal(
2005            self.auto_deref(rhs_info.ty),
2006            result_ty.clone(),
2007            rhs_info.span,
2008        ));
2009
2010        // Result must be numeric (integer or float)
2011        self.add_constraint(Constraint::is_numeric(result_ty.clone(), lhs_info.span));
2012
2013        result_ty
2014    }
2015
2016    /// Generate constraints for a binary bitwise operation (&, |, ^, <<, >>).
2017    ///
2018    /// Operands must be integers (floats are not allowed).
2019    fn generate_binary_bitwise(
2020        &mut self,
2021        lhs: InstRef,
2022        rhs: InstRef,
2023        ctx: &mut ConstraintContext,
2024    ) -> InferType {
2025        let lhs_info = self.generate(lhs, ctx);
2026        let rhs_info = self.generate(rhs, ctx);
2027
2028        let result_var = self.fresh_var();
2029        let result_ty = InferType::Var(result_var);
2030
2031        self.add_constraint(Constraint::equal(
2032            lhs_info.ty,
2033            result_ty.clone(),
2034            lhs_info.span,
2035        ));
2036        self.add_constraint(Constraint::equal(
2037            rhs_info.ty,
2038            result_ty.clone(),
2039            rhs_info.span,
2040        ));
2041
2042        // Result must be an integer type (no floats)
2043        self.add_constraint(Constraint::is_integer(result_ty.clone(), lhs_info.span));
2044
2045        result_ty
2046    }
2047
2048    /// Get the inferred type for a pattern.
2049    /// ADR-0051 Phase 4 part 2: register a single data/struct-variant
2050    /// field binding. If the binding is a flat named binding, insert it
2051    /// directly. If it carries a nested `sub_pattern`, walk into it via
2052    /// `collect_recursive_pattern_bindings` so nested Ident leaves
2053    /// become scope entries.
2054    fn register_binding(
2055        &mut self,
2056        binding: &gruel_rir::RirPatternBinding,
2057        field_ty: InferType,
2058        pattern_span: gruel_util::Span,
2059        ctx: &mut ConstraintContext,
2060        added_bindings: &mut Vec<(lasso::Spur, Option<LocalVarInfo>)>,
2061    ) {
2062        if let Some(sub) = &binding.sub_pattern {
2063            self.collect_recursive_pattern_bindings(sub, field_ty, ctx, added_bindings);
2064            return;
2065        }
2066        if binding.is_wildcard {
2067            return;
2068        }
2069        if let Some(name) = binding.name {
2070            let old = ctx.locals.insert(
2071                name,
2072                LocalVarInfo {
2073                    ty: field_ty,
2074                    is_mut: binding.is_mut,
2075                    span: pattern_span,
2076                },
2077            );
2078            added_bindings.push((name, old));
2079        }
2080    }
2081
2082    /// ADR-0051 Phase 4c: walk a Tuple / Struct / Ident match-arm
2083    /// pattern, registering each Ident leaf in `ctx.locals` so body
2084    /// constraint generation resolves the variable. Field types are
2085    /// pulled from the concrete struct when `scr_ty` is known; fresh
2086    /// type variables take over otherwise.
2087    fn collect_recursive_pattern_bindings(
2088        &mut self,
2089        pattern: &gruel_rir::RirPattern,
2090        scr_ty: InferType,
2091        ctx: &mut ConstraintContext,
2092        added_bindings: &mut Vec<(lasso::Spur, Option<LocalVarInfo>)>,
2093    ) {
2094        match pattern {
2095            gruel_rir::RirPattern::Ident { name, is_mut, span } => {
2096                let old = ctx.locals.insert(
2097                    *name,
2098                    LocalVarInfo {
2099                        ty: scr_ty,
2100                        is_mut: *is_mut,
2101                        span: *span,
2102                    },
2103                );
2104                added_bindings.push((*name, old));
2105            }
2106            gruel_rir::RirPattern::Tuple { elems, .. } => {
2107                // Try to resolve scrutinee's struct type to extract field
2108                // types; fall back to fresh vars when unknown.
2109                let field_tys: Vec<InferType> = match &scr_ty {
2110                    InferType::Concrete(ty) => {
2111                        if let Some(struct_id) = ty.as_struct() {
2112                            let def = self.type_pool.struct_def(struct_id);
2113                            def.fields
2114                                .iter()
2115                                .map(|f| InferType::Concrete(f.ty))
2116                                .collect()
2117                        } else {
2118                            elems
2119                                .iter()
2120                                .map(|_| InferType::Var(self.fresh_var()))
2121                                .collect()
2122                        }
2123                    }
2124                    _ => elems
2125                        .iter()
2126                        .map(|_| InferType::Var(self.fresh_var()))
2127                        .collect(),
2128                };
2129                for (i, elem) in elems.iter().enumerate() {
2130                    let elem_ty = field_tys
2131                        .get(i)
2132                        .cloned()
2133                        .unwrap_or_else(|| InferType::Var(self.fresh_var()));
2134                    self.collect_recursive_pattern_bindings(elem, elem_ty, ctx, added_bindings);
2135                }
2136            }
2137            gruel_rir::RirPattern::Struct { fields, .. } => {
2138                // For named-struct roots, we need field types by name.
2139                // If the scrutinee type is concrete, walk its field list.
2140                let struct_id_opt = match &scr_ty {
2141                    InferType::Concrete(ty) => ty.as_struct(),
2142                    _ => None,
2143                };
2144                for rf in fields {
2145                    let field_ty = if let Some(sid) = struct_id_opt {
2146                        let def = self.type_pool.struct_def(sid);
2147                        let name = self.interner.resolve(&rf.field_name);
2148                        def.fields
2149                            .iter()
2150                            .find(|sf| sf.name == name)
2151                            .map(|sf| InferType::Concrete(sf.ty))
2152                            .unwrap_or_else(|| InferType::Var(self.fresh_var()))
2153                    } else {
2154                        InferType::Var(self.fresh_var())
2155                    };
2156                    self.collect_recursive_pattern_bindings(
2157                        &rf.pattern,
2158                        field_ty,
2159                        ctx,
2160                        added_bindings,
2161                    );
2162                }
2163            }
2164            gruel_rir::RirPattern::DataVariant {
2165                type_name,
2166                bindings,
2167                ..
2168            } => {
2169                // ADR-0052: nested refutable variant sub-pattern.
2170                // Resolve the enum (if possible) to thread field types
2171                // through each inner binding.
2172                let enum_id = self.enums.get(type_name).and_then(|ty| ty.as_enum());
2173                let field_tys: Vec<InferType> = if let Some(eid) = enum_id
2174                    && let Some(variant_idx) = self.resolve_variant_index(pattern)
2175                {
2176                    let def = self.type_pool.enum_def(eid);
2177                    def.variants[variant_idx]
2178                        .fields
2179                        .iter()
2180                        .map(|t| InferType::Concrete(*t))
2181                        .collect()
2182                } else {
2183                    bindings
2184                        .iter()
2185                        .map(|_| InferType::Var(self.fresh_var()))
2186                        .collect()
2187                };
2188                for (i, binding) in bindings.iter().enumerate() {
2189                    let ty = field_tys
2190                        .get(i)
2191                        .cloned()
2192                        .unwrap_or_else(|| InferType::Var(self.fresh_var()));
2193                    self.register_binding(binding, ty, pattern.span(), ctx, added_bindings);
2194                }
2195            }
2196            gruel_rir::RirPattern::StructVariant {
2197                type_name,
2198                field_bindings,
2199                ..
2200            } => {
2201                let enum_id = self.enums.get(type_name).and_then(|ty| ty.as_enum());
2202                for fb in field_bindings {
2203                    let ty = if let Some(eid) = enum_id
2204                        && let Some(variant_idx) = self.resolve_variant_index(pattern)
2205                    {
2206                        let def = self.type_pool.enum_def(eid);
2207                        let variant_def = &def.variants[variant_idx];
2208                        let name = self.interner.resolve(&fb.field_name);
2209                        variant_def
2210                            .find_field(name)
2211                            .map(|idx| InferType::Concrete(variant_def.fields[idx]))
2212                            .unwrap_or_else(|| InferType::Var(self.fresh_var()))
2213                    } else {
2214                        InferType::Var(self.fresh_var())
2215                    };
2216                    self.register_binding(&fb.binding, ty, pattern.span(), ctx, added_bindings);
2217                }
2218            }
2219            // Leaf literals and Path (unit variant) introduce no
2220            // additional bindings at this level.
2221            _ => {}
2222        }
2223    }
2224
2225    /// Helper for `collect_recursive_pattern_bindings`: resolve a
2226    /// DataVariant / StructVariant's variant index from the RIR shape.
2227    fn resolve_variant_index(&self, pattern: &gruel_rir::RirPattern) -> Option<usize> {
2228        let (type_name, variant) = match pattern {
2229            gruel_rir::RirPattern::DataVariant {
2230                type_name, variant, ..
2231            }
2232            | gruel_rir::RirPattern::StructVariant {
2233                type_name, variant, ..
2234            }
2235            | gruel_rir::RirPattern::Path {
2236                type_name, variant, ..
2237            } => (*type_name, *variant),
2238            _ => return None,
2239        };
2240        let enum_id = self.enums.get(&type_name)?.as_enum()?;
2241        let def = self.type_pool.enum_def(enum_id);
2242        def.find_variant(self.interner.resolve(&variant))
2243    }
2244
2245    fn pattern_type(&mut self, pattern: &gruel_rir::RirPattern) -> InferType {
2246        match pattern {
2247            gruel_rir::RirPattern::Wildcard(_) => {
2248                // Wildcard matches anything - use a fresh type variable
2249                let var = self.fresh_var();
2250                InferType::Var(var)
2251            }
2252            gruel_rir::RirPattern::Int(_, _) => InferType::IntLiteral,
2253            gruel_rir::RirPattern::Bool(_, _) => InferType::Concrete(Type::BOOL),
2254            gruel_rir::RirPattern::Path { type_name, .. }
2255            | gruel_rir::RirPattern::DataVariant { type_name, .. }
2256            | gruel_rir::RirPattern::StructVariant { type_name, .. } => {
2257                if let Some(&enum_ty) = self.enums.get(type_name) {
2258                    InferType::Concrete(enum_ty)
2259                } else {
2260                    // Enum type not found — may be a comptime type variable
2261                    // (e.g., `let Opt = Option(i32); match x { Opt::Some(v) => ... }`).
2262                    // Use a fresh type variable so arm bodies can still infer types.
2263                    let var = self.fresh_var();
2264                    InferType::Var(var)
2265                }
2266            }
2267            // ADR-0051 Phase 4b: top-level Ident / Tuple / Struct arms
2268            // don't constrain the scrutinee's type on their own — inference
2269            // ties them to the scrutinee through a fresh type variable. For
2270            // Struct arms whose `type_name` resolves to a known struct we
2271            // could tighten this, but the arm's internal field patterns
2272            // unify against the scrutinee's struct type elsewhere.
2273            gruel_rir::RirPattern::Ident { .. }
2274            | gruel_rir::RirPattern::Tuple { .. }
2275            | gruel_rir::RirPattern::Struct { .. } => {
2276                let var = self.fresh_var();
2277                InferType::Var(var)
2278            }
2279            // ADR-0079 Phase 3: an unroll-arm template constrains
2280            // the scrutinee to whatever type the post-expansion
2281            // arms imply; inference treats it as a fresh var here.
2282            gruel_rir::RirPattern::ComptimeUnrollArm { .. } => {
2283                let var = self.fresh_var();
2284                InferType::Var(var)
2285            }
2286        }
2287    }
2288
2289    /// Resolve a type name to an InferType, consulting `local_subst`
2290    /// before falling back to the body-level `self.type_subst`.
2291    ///
2292    /// Used at generic-call sites: the callee's `return_type_sym` may be
2293    /// a compound name like `Ptr(T)` where `T` is bound to a concrete
2294    /// type only by the per-call substitution map we just built. The
2295    /// regular `resolve_type_name` only sees `self.type_subst` (the
2296    /// outer body's subst), so it would resolve `T` to `Type::ERROR`
2297    /// and the call's return type would never become `Ptr(<concrete>)`.
2298    fn resolve_type_with_local_subst(
2299        &self,
2300        name: &str,
2301        local_subst: &rustc_hash::FxHashMap<lasso::Spur, Type>,
2302    ) -> Option<InferType> {
2303        // Local subst takes precedence — its bindings are call-site-specific.
2304        if let Some(name_spur) = self.interner.get(name)
2305            && let Some(&ty) = local_subst.get(&name_spur)
2306        {
2307            return Some(InferType::Concrete(ty));
2308        }
2309        // For compound names (`Ptr(T)`, `MutRef(T)`, `[T; N]`, …) recurse so
2310        // the inner type-arg goes through the same local-first lookup.
2311        if let Some((element_type_str, length)) = parse_array_type_syntax(name) {
2312            let element_ty = self.resolve_type_with_local_subst(&element_type_str, local_subst)?;
2313            return Some(InferType::Array {
2314                element: Box::new(element_ty),
2315                length,
2316            });
2317        }
2318        if let Some((callee_name, arg_strs)) = parse_type_call_syntax(name)
2319            && let Some(constructor) = gruel_builtins::get_builtin_type_constructor(&callee_name)
2320            && arg_strs.len() == constructor.arity
2321        {
2322            let arg_infer = self.resolve_type_with_local_subst(&arg_strs[0], local_subst)?;
2323            let arg_ty = match arg_infer {
2324                InferType::Concrete(ty) => ty,
2325                _ => return None,
2326            };
2327            let ptr_ty = match constructor.kind {
2328                BuiltinTypeConstructorKind::Ptr => {
2329                    let id = self.type_pool.intern_ptr_const_from_type(arg_ty);
2330                    Type::new_ptr_const(id)
2331                }
2332                BuiltinTypeConstructorKind::MutPtr => {
2333                    let id = self.type_pool.intern_ptr_mut_from_type(arg_ty);
2334                    Type::new_ptr_mut(id)
2335                }
2336                BuiltinTypeConstructorKind::Ref => {
2337                    let id = self.type_pool.intern_ref_from_type(arg_ty);
2338                    Type::new_ref(id)
2339                }
2340                BuiltinTypeConstructorKind::MutRef => {
2341                    let id = self.type_pool.intern_mut_ref_from_type(arg_ty);
2342                    Type::new_mut_ref(id)
2343                }
2344                BuiltinTypeConstructorKind::Slice => {
2345                    let id = self.type_pool.intern_slice_from_type(arg_ty);
2346                    Type::new_slice(id)
2347                }
2348                BuiltinTypeConstructorKind::MutSlice => {
2349                    let id = self.type_pool.intern_mut_slice_from_type(arg_ty);
2350                    Type::new_mut_slice(id)
2351                }
2352                BuiltinTypeConstructorKind::Vec => {
2353                    let id = self.type_pool.intern_vec_from_type(arg_ty);
2354                    Type::new_vec(id)
2355                }
2356            };
2357            return Some(InferType::Concrete(ptr_ty));
2358        }
2359        // Fall through to the regular resolver for primitives / structs / etc.
2360        self.resolve_type_name(name)
2361    }
2362
2363    /// Resolve a type name to an InferType.
2364    ///
2365    /// Handles primitive types, array syntax `[T; N]`, pointer syntax `ptr mut T` / `ptr const T`,
2366    /// and struct/enum types.
2367    fn resolve_type_name(&self, name: &str) -> Option<InferType> {
2368        // ADR-0082: type_subst lookup. When resolving a method body
2369        // for a parameterized struct instance (e.g. `Vec(I32)`), the
2370        // outer fn's comptime type bindings (`T → I32`, `Self →
2371        // <struct_type>`) are stored in `self.type_subst`. Consult it
2372        // before everything else so a bare `T` annotation in
2373        // `let p: MutPtr(T) = ...` (which decomposes to a recursive
2374        // resolve_type_name("T")) resolves to the concrete type.
2375        if let Some(subst) = self.type_subst
2376            && let Some(name_spur) = self.interner.get(name)
2377            && let Some(&ty) = subst.get(&name_spur)
2378        {
2379            return Some(InferType::Concrete(ty));
2380        }
2381
2382        // Check for array syntax first: [T; N]
2383        if let Some((element_type_str, length)) = parse_array_type_syntax(name) {
2384            // Recursively resolve the element type
2385            let element_ty = self.resolve_type_name(&element_type_str)?;
2386            return Some(InferType::Array {
2387                element: Box::new(element_ty),
2388                length,
2389            });
2390        }
2391
2392        // ADR-0061: built-in parameterized types (`Ptr(T)`, `MutPtr(T)`).
2393        if let Some((callee_name, arg_strs)) = parse_type_call_syntax(name)
2394            && let Some(constructor) = gruel_builtins::get_builtin_type_constructor(&callee_name)
2395            && arg_strs.len() == constructor.arity
2396        {
2397            let arg_infer = self.resolve_type_name(&arg_strs[0])?;
2398            let arg_ty = match arg_infer {
2399                InferType::Concrete(ty) => ty,
2400                _ => return None,
2401            };
2402            let ptr_ty = match constructor.kind {
2403                BuiltinTypeConstructorKind::Ptr => {
2404                    let id = self.type_pool.intern_ptr_const_from_type(arg_ty);
2405                    Type::new_ptr_const(id)
2406                }
2407                BuiltinTypeConstructorKind::MutPtr => {
2408                    let id = self.type_pool.intern_ptr_mut_from_type(arg_ty);
2409                    Type::new_ptr_mut(id)
2410                }
2411                BuiltinTypeConstructorKind::Ref => {
2412                    let id = self.type_pool.intern_ref_from_type(arg_ty);
2413                    Type::new_ref(id)
2414                }
2415                BuiltinTypeConstructorKind::MutRef => {
2416                    let id = self.type_pool.intern_mut_ref_from_type(arg_ty);
2417                    Type::new_mut_ref(id)
2418                }
2419                BuiltinTypeConstructorKind::Slice => {
2420                    let id = self.type_pool.intern_slice_from_type(arg_ty);
2421                    Type::new_slice(id)
2422                }
2423                BuiltinTypeConstructorKind::MutSlice => {
2424                    let id = self.type_pool.intern_mut_slice_from_type(arg_ty);
2425                    Type::new_mut_slice(id)
2426                }
2427                BuiltinTypeConstructorKind::Vec => {
2428                    let id = self.type_pool.intern_vec_from_type(arg_ty);
2429                    Type::new_vec(id)
2430                }
2431            };
2432            return Some(InferType::Concrete(ptr_ty));
2433        }
2434
2435        // Check for pointer syntax: ptr mut T / ptr const T
2436        if let Some((pointee_type_str, mutability)) = parse_pointer_type_syntax(name) {
2437            // Recursively resolve the pointee type
2438            let pointee_infer_ty = self.resolve_type_name(&pointee_type_str)?;
2439
2440            // Convert InferType to Type so we can intern the pointer
2441            let pointee_ty = match pointee_infer_ty {
2442                InferType::Concrete(ty) => ty,
2443                // Can't handle non-concrete types in pointer positions during constraint generation
2444                _ => return None,
2445            };
2446
2447            // Intern the pointer type
2448            let ptr_ty = match mutability {
2449                PtrMutability::Mut => {
2450                    let ptr_id = self.type_pool.intern_ptr_mut_from_type(pointee_ty);
2451                    Type::new_ptr_mut(ptr_id)
2452                }
2453                PtrMutability::Const => {
2454                    let ptr_id = self.type_pool.intern_ptr_const_from_type(pointee_ty);
2455                    Type::new_ptr_const(ptr_id)
2456                }
2457            };
2458            return Some(InferType::Concrete(ptr_ty));
2459        }
2460
2461        // Check primitives
2462        let ty = match name {
2463            "i8" => Type::I8,
2464            "i16" => Type::I16,
2465            "i32" => Type::I32,
2466            "i64" => Type::I64,
2467            "u8" => Type::U8,
2468            "u16" => Type::U16,
2469            "u32" => Type::U32,
2470            "u64" => Type::U64,
2471            "isize" => Type::ISIZE,
2472            "usize" => Type::USIZE,
2473            "f16" => Type::F16,
2474            "f32" => Type::F32,
2475            "f64" => Type::F64,
2476            "bool" => Type::BOOL,
2477            "()" => Type::UNIT,
2478            // ADR-0086: C named arithmetic primitive types. Resolved
2479            // identically to native primitives at the inference layer —
2480            // sema's resolve_type fires the preview gate before
2481            // inference runs. `c_void` is also recognised here (so the
2482            // resulting constraint can flag a mismatch) but downstream
2483            // sema rejects c_void in any value-bearing position.
2484            "c_schar" => Type::C_SCHAR,
2485            "c_short" => Type::C_SHORT,
2486            "c_int" => Type::C_INT,
2487            "c_long" => Type::C_LONG,
2488            "c_longlong" => Type::C_LONGLONG,
2489            "c_uchar" => Type::C_UCHAR,
2490            "c_ushort" => Type::C_USHORT,
2491            "c_uint" => Type::C_UINT,
2492            "c_ulong" => Type::C_ULONG,
2493            "c_ulonglong" => Type::C_ULONGLONG,
2494            "c_float" => Type::C_FLOAT,
2495            "c_double" => Type::C_DOUBLE,
2496            "c_void" => Type::C_VOID,
2497            _ => {
2498                // Check for struct types (including builtin String)
2499                if let Some(name_spur) = self.interner.get(name) {
2500                    if let Some(&struct_ty) = self.structs.get(&name_spur) {
2501                        return Some(InferType::Concrete(struct_ty));
2502                    }
2503                    if let Some(&enum_ty) = self.enums.get(&name_spur) {
2504                        return Some(InferType::Concrete(enum_ty));
2505                    }
2506                }
2507                return None;
2508            }
2509        };
2510        Some(InferType::Concrete(ty))
2511    }
2512}
2513
2514#[cfg(test)]
2515mod tests {
2516    use super::*;
2517    use lasso::ThreadedRodeo;
2518
2519    /// Helper to create a minimal RIR, interner, and type pool for testing.
2520    fn make_test_rir_interner_and_type_pool() -> (Rir, ThreadedRodeo, TypeInternPool) {
2521        let rir = Rir::new();
2522        let interner = ThreadedRodeo::new();
2523        let type_pool = TypeInternPool::new();
2524        (rir, interner, type_pool)
2525    }
2526
2527    #[test]
2528    fn test_constraint_generator_int_literal() {
2529        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2530        let functions = HashMap::default();
2531        let structs = HashMap::default();
2532        let enums = HashMap::default();
2533        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
2534        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
2535
2536        // Add an integer constant to RIR
2537        let inst_ref = rir.add_inst(gruel_rir::Inst {
2538            data: InstData::IntConst(42),
2539            span: Span::new(0, 2),
2540        });
2541
2542        let infer_ctx = InferenceContext {
2543            func_sigs: functions.clone(),
2544            struct_types: structs.clone(),
2545            enum_types: enums.clone(),
2546            method_sigs: methods.clone(),
2547            enum_method_sigs: enum_methods.clone(),
2548        };
2549        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
2550        let params = HashMap::default();
2551        let mut ctx = ConstraintContext::new(&params, Type::I32);
2552
2553        let info = cgen.generate(inst_ref, &mut ctx);
2554
2555        // Integer literals now get a type variable (tracked as int literal var)
2556        assert!(matches!(info.ty, InferType::Var(_)));
2557        // The type variable should be tracked in int_literal_vars
2558        assert_eq!(cgen.int_literal_vars().len(), 1);
2559        // No constraints should be generated for a simple literal
2560        assert_eq!(cgen.constraints().len(), 0);
2561    }
2562
2563    #[test]
2564    fn test_constraint_generator_bool_literal() {
2565        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2566        let functions = HashMap::default();
2567        let structs = HashMap::default();
2568        let enums = HashMap::default();
2569        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
2570        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
2571
2572        let inst_ref = rir.add_inst(gruel_rir::Inst {
2573            data: InstData::BoolConst(true),
2574            span: Span::new(0, 4),
2575        });
2576
2577        let infer_ctx = InferenceContext {
2578            func_sigs: functions.clone(),
2579            struct_types: structs.clone(),
2580            enum_types: enums.clone(),
2581            method_sigs: methods.clone(),
2582            enum_method_sigs: enum_methods.clone(),
2583        };
2584        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
2585        let params = HashMap::default();
2586        let mut ctx = ConstraintContext::new(&params, Type::BOOL);
2587
2588        let info = cgen.generate(inst_ref, &mut ctx);
2589
2590        assert_eq!(info.ty, InferType::Concrete(Type::BOOL));
2591        assert_eq!(cgen.constraints().len(), 0);
2592    }
2593
2594    #[test]
2595    fn test_constraint_generator_binary_add() {
2596        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2597        let functions = HashMap::default();
2598        let structs = HashMap::default();
2599        let enums = HashMap::default();
2600        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
2601        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
2602
2603        // Create: 1 + 2
2604        let lhs = rir.add_inst(gruel_rir::Inst {
2605            data: InstData::IntConst(1),
2606            span: Span::new(0, 1),
2607        });
2608        let rhs = rir.add_inst(gruel_rir::Inst {
2609            data: InstData::IntConst(2),
2610            span: Span::new(4, 5),
2611        });
2612        let add = rir.add_inst(gruel_rir::Inst {
2613            data: InstData::Bin {
2614                op: BinOp::Add,
2615                lhs,
2616                rhs,
2617            },
2618            span: Span::new(0, 5),
2619        });
2620
2621        let infer_ctx = InferenceContext {
2622            func_sigs: functions.clone(),
2623            struct_types: structs.clone(),
2624            enum_types: enums.clone(),
2625            method_sigs: methods.clone(),
2626            enum_method_sigs: enum_methods.clone(),
2627        };
2628        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
2629        let params = HashMap::default();
2630        let mut ctx = ConstraintContext::new(&params, Type::I32);
2631
2632        let info = cgen.generate(add, &mut ctx);
2633
2634        // Result should be a type variable
2635        assert!(info.ty.is_var());
2636        // Should generate 3 constraints: lhs = result, rhs = result, IsNumeric(result)
2637        assert_eq!(cgen.constraints().len(), 3);
2638        // Verify the third constraint is IsNumeric
2639        match &cgen.constraints()[2] {
2640            Constraint::IsNumeric(_, _) => {}
2641            _ => panic!("Expected IsNumeric constraint for arithmetic result"),
2642        }
2643    }
2644
2645    #[test]
2646    fn test_constraint_generator_comparison() {
2647        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2648        let functions = HashMap::default();
2649        let structs = HashMap::default();
2650        let enums = HashMap::default();
2651        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
2652        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
2653
2654        // Create: 1 < 2
2655        let lhs = rir.add_inst(gruel_rir::Inst {
2656            data: InstData::IntConst(1),
2657            span: Span::new(0, 1),
2658        });
2659        let rhs = rir.add_inst(gruel_rir::Inst {
2660            data: InstData::IntConst(2),
2661            span: Span::new(4, 5),
2662        });
2663        let lt = rir.add_inst(gruel_rir::Inst {
2664            data: InstData::Bin {
2665                op: BinOp::Lt,
2666                lhs,
2667                rhs,
2668            },
2669            span: Span::new(0, 5),
2670        });
2671
2672        let infer_ctx = InferenceContext {
2673            func_sigs: functions.clone(),
2674            struct_types: structs.clone(),
2675            enum_types: enums.clone(),
2676            method_sigs: methods.clone(),
2677            enum_method_sigs: enum_methods.clone(),
2678        };
2679        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
2680        let params = HashMap::default();
2681        let mut ctx = ConstraintContext::new(&params, Type::BOOL);
2682
2683        let info = cgen.generate(lt, &mut ctx);
2684
2685        // Comparisons always return Bool
2686        assert_eq!(info.ty, InferType::Concrete(Type::BOOL));
2687        // Should generate 1 constraint: lhs type = rhs type
2688        assert_eq!(cgen.constraints().len(), 1);
2689    }
2690
2691    #[test]
2692    fn test_constraint_generator_logical_and() {
2693        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2694        let functions = HashMap::default();
2695        let structs = HashMap::default();
2696        let enums = HashMap::default();
2697        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
2698        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
2699
2700        // Create: true && false
2701        let lhs = rir.add_inst(gruel_rir::Inst {
2702            data: InstData::BoolConst(true),
2703            span: Span::new(0, 4),
2704        });
2705        let rhs = rir.add_inst(gruel_rir::Inst {
2706            data: InstData::BoolConst(false),
2707            span: Span::new(8, 13),
2708        });
2709        let and = rir.add_inst(gruel_rir::Inst {
2710            data: InstData::Bin {
2711                op: BinOp::And,
2712                lhs,
2713                rhs,
2714            },
2715            span: Span::new(0, 13),
2716        });
2717
2718        let infer_ctx = InferenceContext {
2719            func_sigs: functions.clone(),
2720            struct_types: structs.clone(),
2721            enum_types: enums.clone(),
2722            method_sigs: methods.clone(),
2723            enum_method_sigs: enum_methods.clone(),
2724        };
2725        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
2726        let params = HashMap::default();
2727        let mut ctx = ConstraintContext::new(&params, Type::BOOL);
2728
2729        let info = cgen.generate(and, &mut ctx);
2730
2731        // Logical operators return Bool
2732        assert_eq!(info.ty, InferType::Concrete(Type::BOOL));
2733        // Should generate 2 constraints: lhs = bool, rhs = bool
2734        assert_eq!(cgen.constraints().len(), 2);
2735    }
2736
2737    #[test]
2738    fn test_constraint_generator_negation() {
2739        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2740        let functions = HashMap::default();
2741        let structs = HashMap::default();
2742        let enums = HashMap::default();
2743        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
2744        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
2745
2746        // Create: -42
2747        let operand = rir.add_inst(gruel_rir::Inst {
2748            data: InstData::IntConst(42),
2749            span: Span::new(1, 3),
2750        });
2751        let neg = rir.add_inst(gruel_rir::Inst {
2752            data: InstData::Unary {
2753                op: UnaryOp::Neg,
2754                operand,
2755            },
2756            span: Span::new(0, 3),
2757        });
2758
2759        let infer_ctx = InferenceContext {
2760            func_sigs: functions.clone(),
2761            struct_types: structs.clone(),
2762            enum_types: enums.clone(),
2763            method_sigs: methods.clone(),
2764            enum_method_sigs: enum_methods.clone(),
2765        };
2766        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
2767        let params = HashMap::default();
2768        let mut ctx = ConstraintContext::new(&params, Type::I32);
2769
2770        let info = cgen.generate(neg, &mut ctx);
2771
2772        // Negation preserves the operand type (now a type variable for the int literal)
2773        assert!(matches!(info.ty, InferType::Var(_)));
2774        // Should generate 1 constraint: IsSigned for the result
2775        assert_eq!(cgen.constraints().len(), 1);
2776        // Verify it's an IsSigned constraint
2777        match &cgen.constraints()[0] {
2778            Constraint::IsSigned(_, _) => {}
2779            _ => panic!("Expected IsSigned constraint"),
2780        }
2781    }
2782
2783    #[test]
2784    fn test_constraint_generator_return() {
2785        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2786        let functions = HashMap::default();
2787        let structs = HashMap::default();
2788        let enums = HashMap::default();
2789        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
2790        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
2791
2792        // Create: return 42
2793        let value = rir.add_inst(gruel_rir::Inst {
2794            data: InstData::IntConst(42),
2795            span: Span::new(7, 9),
2796        });
2797        let ret = rir.add_inst(gruel_rir::Inst {
2798            data: InstData::Ret(Some(value)),
2799            span: Span::new(0, 9),
2800        });
2801
2802        let infer_ctx = InferenceContext {
2803            func_sigs: functions.clone(),
2804            struct_types: structs.clone(),
2805            enum_types: enums.clone(),
2806            method_sigs: methods.clone(),
2807            enum_method_sigs: enum_methods.clone(),
2808        };
2809        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
2810        let params = HashMap::default();
2811        let mut ctx = ConstraintContext::new(&params, Type::I32);
2812
2813        let info = cgen.generate(ret, &mut ctx);
2814
2815        // Return is divergent (Never type)
2816        assert_eq!(info.ty, InferType::Concrete(Type::NEVER));
2817        // Should generate 1 constraint: return value = return type
2818        assert_eq!(cgen.constraints().len(), 1);
2819    }
2820
2821    #[test]
2822    fn test_constraint_generator_if_else() {
2823        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2824        let functions = HashMap::default();
2825        let structs = HashMap::default();
2826        let enums = HashMap::default();
2827        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
2828        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
2829
2830        // Create: if true { 1 } else { 2 }
2831        let cond = rir.add_inst(gruel_rir::Inst {
2832            data: InstData::BoolConst(true),
2833            span: Span::new(3, 7),
2834        });
2835        let then_val = rir.add_inst(gruel_rir::Inst {
2836            data: InstData::IntConst(1),
2837            span: Span::new(10, 11),
2838        });
2839        let else_val = rir.add_inst(gruel_rir::Inst {
2840            data: InstData::IntConst(2),
2841            span: Span::new(20, 21),
2842        });
2843        let branch = rir.add_inst(gruel_rir::Inst {
2844            data: InstData::Branch {
2845                cond,
2846                then_block: then_val,
2847                else_block: Some(else_val),
2848                is_comptime: false,
2849            },
2850            span: Span::new(0, 25),
2851        });
2852
2853        let infer_ctx = InferenceContext {
2854            func_sigs: functions.clone(),
2855            struct_types: structs.clone(),
2856            enum_types: enums.clone(),
2857            method_sigs: methods.clone(),
2858            enum_method_sigs: enum_methods.clone(),
2859        };
2860        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
2861        let params = HashMap::default();
2862        let mut ctx = ConstraintContext::new(&params, Type::I32);
2863
2864        let info = cgen.generate(branch, &mut ctx);
2865
2866        // Result should be a type variable (unified from both branches)
2867        assert!(info.ty.is_var());
2868        // Should generate 3 constraints: cond = bool, then = result, else = result
2869        assert_eq!(cgen.constraints().len(), 3);
2870    }
2871
2872    #[test]
2873    fn test_constraint_generator_while_loop() {
2874        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2875        let functions = HashMap::default();
2876        let structs = HashMap::default();
2877        let enums = HashMap::default();
2878        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
2879        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
2880
2881        // Create: while true { 0 }
2882        let cond = rir.add_inst(gruel_rir::Inst {
2883            data: InstData::BoolConst(true),
2884            span: Span::new(6, 10),
2885        });
2886        let body = rir.add_inst(gruel_rir::Inst {
2887            data: InstData::IntConst(0),
2888            span: Span::new(13, 14),
2889        });
2890        let loop_inst = rir.add_inst(gruel_rir::Inst {
2891            data: InstData::Loop { cond, body },
2892            span: Span::new(0, 15),
2893        });
2894
2895        let infer_ctx = InferenceContext {
2896            func_sigs: functions.clone(),
2897            struct_types: structs.clone(),
2898            enum_types: enums.clone(),
2899            method_sigs: methods.clone(),
2900            enum_method_sigs: enum_methods.clone(),
2901        };
2902        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
2903        let params = HashMap::default();
2904        let mut ctx = ConstraintContext::new(&params, Type::UNIT);
2905
2906        let info = cgen.generate(loop_inst, &mut ctx);
2907
2908        // While loops produce Unit
2909        assert_eq!(info.ty, InferType::Concrete(Type::UNIT));
2910        // Should generate 1 constraint: cond = bool
2911        assert_eq!(cgen.constraints().len(), 1);
2912    }
2913
2914    #[test]
2915    fn test_constraint_context_scope() {
2916        let params = HashMap::default();
2917        let mut ctx = ConstraintContext::new(&params, Type::I32);
2918
2919        // Use an interner to create a symbol
2920        let interner = ThreadedRodeo::new();
2921        let sym = interner.get_or_intern("x");
2922        ctx.insert_local(
2923            sym,
2924            LocalVarInfo {
2925                ty: InferType::Concrete(Type::I32),
2926                is_mut: false,
2927                span: Span::new(0, 1),
2928            },
2929        );
2930
2931        assert!(ctx.locals.contains_key(&sym));
2932
2933        // Push a scope and shadow the variable
2934        ctx.push_scope();
2935        ctx.insert_local(
2936            sym,
2937            LocalVarInfo {
2938                ty: InferType::Concrete(Type::I64),
2939                is_mut: true,
2940                span: Span::new(10, 15),
2941            },
2942        );
2943
2944        // Should see the shadowed version
2945        let local = ctx.locals.get(&sym).unwrap();
2946        assert_eq!(local.ty, InferType::Concrete(Type::I64));
2947        assert!(local.is_mut);
2948
2949        // Pop scope - should restore original
2950        ctx.pop_scope();
2951        let local = ctx.locals.get(&sym).unwrap();
2952        assert_eq!(local.ty, InferType::Concrete(Type::I32));
2953        assert!(!local.is_mut);
2954    }
2955
2956    #[test]
2957    fn test_expr_info_creation() {
2958        let info = ExprInfo::new(InferType::IntLiteral, Span::new(5, 10));
2959        assert!(info.ty.is_int_literal());
2960        assert_eq!(info.span, Span::new(5, 10));
2961    }
2962
2963    /// Helper to create a non-generic FunctionSig for tests
2964    fn make_test_func_sig(param_types: Vec<InferType>, return_type: InferType) -> FunctionSig {
2965        let num_params = param_types.len();
2966        FunctionSig {
2967            param_types,
2968            return_type,
2969            is_generic: false,
2970            param_modes: vec![gruel_rir::RirParamMode::Normal; num_params],
2971            param_comptime: vec![false; num_params],
2972            param_names: vec![],
2973            return_type_sym: lasso::Spur::default(),
2974        }
2975    }
2976
2977    #[test]
2978    fn test_function_sig() {
2979        let sig = make_test_func_sig(
2980            vec![
2981                InferType::Concrete(Type::I32),
2982                InferType::Concrete(Type::BOOL),
2983            ],
2984            InferType::Concrete(Type::I64),
2985        );
2986        assert_eq!(sig.param_types.len(), 2);
2987        assert_eq!(sig.return_type, InferType::Concrete(Type::I64));
2988    }
2989
2990    #[test]
2991    fn test_constraint_generator_infinite_loop() {
2992        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2993        let functions = HashMap::default();
2994        let structs = HashMap::default();
2995        let enums = HashMap::default();
2996        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
2997        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
2998
2999        // Create: loop { 0 }
3000        let body = rir.add_inst(gruel_rir::Inst {
3001            data: InstData::IntConst(0),
3002            span: Span::new(6, 7),
3003        });
3004        let loop_inst = rir.add_inst(gruel_rir::Inst {
3005            data: InstData::InfiniteLoop { body },
3006            span: Span::new(0, 10),
3007        });
3008
3009        let infer_ctx = InferenceContext {
3010            func_sigs: functions.clone(),
3011            struct_types: structs.clone(),
3012            enum_types: enums.clone(),
3013            method_sigs: methods.clone(),
3014            enum_method_sigs: enum_methods.clone(),
3015        };
3016        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
3017        let params = HashMap::default();
3018        let mut ctx = ConstraintContext::new(&params, Type::UNIT);
3019
3020        let info = cgen.generate(loop_inst, &mut ctx);
3021
3022        // Infinite loop produces Never (diverges)
3023        assert_eq!(info.ty, InferType::Concrete(Type::NEVER));
3024        // No constraints for infinite loop itself
3025        assert_eq!(cgen.constraints().len(), 0);
3026    }
3027
3028    #[test]
3029    fn test_constraint_generator_break_continue() {
3030        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
3031        let functions = HashMap::default();
3032        let structs = HashMap::default();
3033        let enums = HashMap::default();
3034        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
3035        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
3036
3037        let break_inst = rir.add_inst(gruel_rir::Inst {
3038            data: InstData::Break,
3039            span: Span::new(0, 5),
3040        });
3041
3042        let infer_ctx = InferenceContext {
3043            func_sigs: functions.clone(),
3044            struct_types: structs.clone(),
3045            enum_types: enums.clone(),
3046            method_sigs: methods.clone(),
3047            enum_method_sigs: enum_methods.clone(),
3048        };
3049        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
3050        let params = HashMap::default();
3051        let mut ctx = ConstraintContext::new(&params, Type::UNIT);
3052
3053        let info = cgen.generate(break_inst, &mut ctx);
3054
3055        // Break diverges
3056        assert_eq!(info.ty, InferType::Concrete(Type::NEVER));
3057        assert_eq!(cgen.constraints().len(), 0);
3058    }
3059
3060    #[test]
3061    fn test_constraint_generator_index_get() {
3062        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
3063        let functions = HashMap::default();
3064        let structs = HashMap::default();
3065        let enums = HashMap::default();
3066        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
3067        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
3068
3069        // Create: arr[0]
3070        let base = rir.add_inst(gruel_rir::Inst {
3071            data: InstData::IntConst(0), // Placeholder for array
3072            span: Span::new(0, 3),
3073        });
3074        let index = rir.add_inst(gruel_rir::Inst {
3075            data: InstData::IntConst(0),
3076            span: Span::new(4, 5),
3077        });
3078        let index_get = rir.add_inst(gruel_rir::Inst {
3079            data: InstData::IndexGet { base, index },
3080            span: Span::new(0, 6),
3081        });
3082
3083        let infer_ctx = InferenceContext {
3084            func_sigs: functions.clone(),
3085            struct_types: structs.clone(),
3086            enum_types: enums.clone(),
3087            method_sigs: methods.clone(),
3088            enum_method_sigs: enum_methods.clone(),
3089        };
3090        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
3091        let params = HashMap::default();
3092        let mut ctx = ConstraintContext::new(&params, Type::I32);
3093
3094        let info = cgen.generate(index_get, &mut ctx);
3095
3096        // Result is a type variable (element type unknown)
3097        assert!(info.ty.is_var());
3098        // Should generate 1 constraint: index must be `usize`
3099        assert_eq!(cgen.constraints().len(), 1);
3100        match &cgen.constraints()[0] {
3101            Constraint::Equal(lhs, _rhs, _) => {
3102                assert_eq!(*lhs, InferType::Concrete(Type::USIZE));
3103            }
3104            _ => panic!("Expected Equal(USIZE, _) constraint for index"),
3105        }
3106    }
3107
3108    #[test]
3109    fn test_constraint_generator_index_set() {
3110        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
3111        let functions = HashMap::default();
3112        let structs = HashMap::default();
3113        let enums = HashMap::default();
3114        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
3115        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
3116
3117        // Create: arr[0] = 42
3118        let base = rir.add_inst(gruel_rir::Inst {
3119            data: InstData::IntConst(0), // Placeholder for array
3120            span: Span::new(0, 3),
3121        });
3122        let index = rir.add_inst(gruel_rir::Inst {
3123            data: InstData::IntConst(0),
3124            span: Span::new(4, 5),
3125        });
3126        let value = rir.add_inst(gruel_rir::Inst {
3127            data: InstData::IntConst(42),
3128            span: Span::new(9, 11),
3129        });
3130        let index_set = rir.add_inst(gruel_rir::Inst {
3131            data: InstData::IndexSet { base, index, value },
3132            span: Span::new(0, 11),
3133        });
3134
3135        let infer_ctx = InferenceContext {
3136            func_sigs: functions.clone(),
3137            struct_types: structs.clone(),
3138            enum_types: enums.clone(),
3139            method_sigs: methods.clone(),
3140            enum_method_sigs: enum_methods.clone(),
3141        };
3142        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
3143        let params = HashMap::default();
3144        let mut ctx = ConstraintContext::new(&params, Type::UNIT);
3145
3146        let info = cgen.generate(index_set, &mut ctx);
3147
3148        // Index assignment produces Unit
3149        assert_eq!(info.ty, InferType::Concrete(Type::UNIT));
3150        // Should generate 1 constraint: index must be `usize`
3151        assert_eq!(cgen.constraints().len(), 1);
3152        match &cgen.constraints()[0] {
3153            Constraint::Equal(lhs, _rhs, _) => {
3154                assert_eq!(*lhs, InferType::Concrete(Type::USIZE));
3155            }
3156            _ => panic!("Expected Equal(USIZE, _) constraint for index"),
3157        }
3158    }
3159
3160    #[test]
3161    fn test_constraint_generator_empty_block() {
3162        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
3163        let functions = HashMap::default();
3164        let structs = HashMap::default();
3165        let enums = HashMap::default();
3166        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
3167        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
3168
3169        // Create: { } (empty block)
3170        let block = rir.add_inst(gruel_rir::Inst {
3171            data: InstData::Block {
3172                extra_start: 0,
3173                len: 0,
3174            },
3175            span: Span::new(0, 2),
3176        });
3177
3178        let infer_ctx = InferenceContext {
3179            func_sigs: functions.clone(),
3180            struct_types: structs.clone(),
3181            enum_types: enums.clone(),
3182            method_sigs: methods.clone(),
3183            enum_method_sigs: enum_methods.clone(),
3184        };
3185        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
3186        let params = HashMap::default();
3187        let mut ctx = ConstraintContext::new(&params, Type::UNIT);
3188
3189        let info = cgen.generate(block, &mut ctx);
3190
3191        // Empty block produces Unit
3192        assert_eq!(info.ty, InferType::Concrete(Type::UNIT));
3193        assert_eq!(cgen.constraints().len(), 0);
3194    }
3195
3196    #[test]
3197    fn test_constraint_generator_bitwise_not() {
3198        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
3199        let functions = HashMap::default();
3200        let structs = HashMap::default();
3201        let enums = HashMap::default();
3202        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
3203        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
3204
3205        // Create: !42 (bitwise NOT)
3206        let operand = rir.add_inst(gruel_rir::Inst {
3207            data: InstData::IntConst(42),
3208            span: Span::new(1, 3),
3209        });
3210        let bitnot = rir.add_inst(gruel_rir::Inst {
3211            data: InstData::Unary {
3212                op: UnaryOp::BitNot,
3213                operand,
3214            },
3215            span: Span::new(0, 3),
3216        });
3217
3218        let infer_ctx = InferenceContext {
3219            func_sigs: functions.clone(),
3220            struct_types: structs.clone(),
3221            enum_types: enums.clone(),
3222            method_sigs: methods.clone(),
3223            enum_method_sigs: enum_methods.clone(),
3224        };
3225        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
3226        let params = HashMap::default();
3227        let mut ctx = ConstraintContext::new(&params, Type::I32);
3228
3229        let info = cgen.generate(bitnot, &mut ctx);
3230
3231        // Bitwise NOT preserves the operand type (now a type variable for int literal)
3232        assert!(matches!(info.ty, InferType::Var(_)));
3233        // Should generate 1 constraint: IsInteger for the result
3234        assert_eq!(cgen.constraints().len(), 1);
3235        match &cgen.constraints()[0] {
3236            Constraint::IsInteger(_, _) => {}
3237            _ => panic!("Expected IsInteger constraint"),
3238        }
3239    }
3240
3241    #[test]
3242    fn test_constraint_generator_function_call_arg_count_mismatch() {
3243        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
3244        let mut functions = HashMap::default();
3245        let structs = HashMap::default();
3246        let enums = HashMap::default();
3247        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
3248        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
3249
3250        // Register a function that takes 2 parameters
3251        let func_name = interner.get_or_intern("foo");
3252        functions.insert(
3253            func_name,
3254            make_test_func_sig(
3255                vec![
3256                    InferType::Concrete(Type::I32),
3257                    InferType::Concrete(Type::I32),
3258                ],
3259                InferType::Concrete(Type::BOOL),
3260            ),
3261        );
3262
3263        // Create a call with only 1 argument (mismatch)
3264        let arg = rir.add_inst(gruel_rir::Inst {
3265            data: InstData::IntConst(42),
3266            span: Span::new(4, 6),
3267        });
3268        let (args_start, args_len) = rir.add_call_args(&[gruel_rir::RirCallArg {
3269            value: arg,
3270            mode: gruel_rir::RirArgMode::Normal,
3271        }]);
3272        let call = rir.add_inst(gruel_rir::Inst {
3273            data: InstData::Call {
3274                name: func_name,
3275                args_start,
3276                args_len,
3277            },
3278            span: Span::new(0, 7),
3279        });
3280
3281        let infer_ctx = InferenceContext {
3282            func_sigs: functions.clone(),
3283            struct_types: structs.clone(),
3284            enum_types: enums.clone(),
3285            method_sigs: methods.clone(),
3286            enum_method_sigs: enum_methods.clone(),
3287        };
3288        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
3289        let params = HashMap::default();
3290        let mut ctx = ConstraintContext::new(&params, Type::BOOL);
3291
3292        let info = cgen.generate(call, &mut ctx);
3293
3294        // Should still return the declared return type
3295        assert_eq!(info.ty, InferType::Concrete(Type::BOOL));
3296        // No constraints generated when arg count mismatches (error will be in sema)
3297        assert_eq!(cgen.constraints().len(), 0);
3298    }
3299
3300    #[test]
3301    fn test_constraint_generator_unknown_function() {
3302        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
3303        let functions = HashMap::default(); // Empty - no functions registered
3304        let structs = HashMap::default();
3305        let enums = HashMap::default();
3306        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
3307        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
3308
3309        // Create a call to an unknown function
3310        let unknown_func = interner.get_or_intern("unknown");
3311        let arg = rir.add_inst(gruel_rir::Inst {
3312            data: InstData::IntConst(42),
3313            span: Span::new(8, 10),
3314        });
3315        let (args_start, args_len) = rir.add_call_args(&[gruel_rir::RirCallArg {
3316            value: arg,
3317            mode: gruel_rir::RirArgMode::Normal,
3318        }]);
3319        let call = rir.add_inst(gruel_rir::Inst {
3320            data: InstData::Call {
3321                name: unknown_func,
3322                args_start,
3323                args_len,
3324            },
3325            span: Span::new(0, 11),
3326        });
3327
3328        let infer_ctx = InferenceContext {
3329            func_sigs: functions.clone(),
3330            struct_types: structs.clone(),
3331            enum_types: enums.clone(),
3332            method_sigs: methods.clone(),
3333            enum_method_sigs: enum_methods.clone(),
3334        };
3335        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
3336        let params = HashMap::default();
3337        let mut ctx = ConstraintContext::new(&params, Type::I32);
3338
3339        let info = cgen.generate(call, &mut ctx);
3340
3341        // Unknown function returns Error type
3342        assert_eq!(info.ty, InferType::Concrete(Type::ERROR));
3343        // Arguments should still be processed (but no constraints generated for them)
3344        assert_eq!(cgen.constraints().len(), 0);
3345    }
3346
3347    #[test]
3348    fn test_constraint_generator_match_multiple_arms() {
3349        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
3350        let functions = HashMap::default();
3351        let structs = HashMap::default();
3352        let enums = HashMap::default();
3353        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::default();
3354        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::default();
3355
3356        // Create: match x { 1 => 10, 2 => 20, _ => 30 }
3357        let scrutinee = rir.add_inst(gruel_rir::Inst {
3358            data: InstData::IntConst(5),
3359            span: Span::new(6, 7),
3360        });
3361
3362        // Arm 1: 1 => 10
3363        let body1 = rir.add_inst(gruel_rir::Inst {
3364            data: InstData::IntConst(10),
3365            span: Span::new(15, 17),
3366        });
3367        let pattern1 = gruel_rir::RirPattern::Int(1, Span::new(10, 11));
3368
3369        // Arm 2: 2 => 20
3370        let body2 = rir.add_inst(gruel_rir::Inst {
3371            data: InstData::IntConst(20),
3372            span: Span::new(25, 27),
3373        });
3374        let pattern2 = gruel_rir::RirPattern::Int(2, Span::new(20, 21));
3375
3376        // Arm 3: _ => 30
3377        let body3 = rir.add_inst(gruel_rir::Inst {
3378            data: InstData::IntConst(30),
3379            span: Span::new(35, 37),
3380        });
3381        let pattern3 = gruel_rir::RirPattern::Wildcard(Span::new(30, 31));
3382
3383        let arms = vec![(pattern1, body1), (pattern2, body2), (pattern3, body3)];
3384        let (arms_start, arms_len) = rir.add_match_arms(&arms);
3385        let match_inst = rir.add_inst(gruel_rir::Inst {
3386            data: InstData::Match {
3387                scrutinee,
3388                arms_start,
3389                arms_len,
3390            },
3391            span: Span::new(0, 40),
3392        });
3393
3394        let infer_ctx = InferenceContext {
3395            func_sigs: functions.clone(),
3396            struct_types: structs.clone(),
3397            enum_types: enums.clone(),
3398            method_sigs: methods.clone(),
3399            enum_method_sigs: enum_methods.clone(),
3400        };
3401        let mut cgen = ConstraintGenerator::new(&rir, &interner, &infer_ctx, &type_pool);
3402        let params = HashMap::default();
3403        let mut ctx = ConstraintContext::new(&params, Type::I32);
3404
3405        let info = cgen.generate(match_inst, &mut ctx);
3406
3407        // Result should be a type variable (unified from all arm bodies)
3408        assert!(info.ty.is_var());
3409
3410        // Should generate 6 constraints:
3411        // - 3 for pattern types matching scrutinee type (each arm)
3412        // - 3 for body types matching result type (each arm)
3413        assert_eq!(cgen.constraints().len(), 6);
3414
3415        // Verify all constraints are Equal constraints
3416        for constraint in cgen.constraints() {
3417            match constraint {
3418                Constraint::Equal(_, _, _) => {}
3419                _ => panic!("Expected Equal constraint in match"),
3420            }
3421        }
3422    }
3423}