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::types::{
15    EnumId, PtrMutability, StructId, parse_array_type_syntax, parse_pointer_type_syntax,
16};
17use gruel_rir::{InstData, InstRef, Rir};
18use gruel_span::Span;
19use lasso::{Spur, ThreadedRodeo};
20use std::collections::HashMap;
21
22/// Information about a local variable during constraint generation.
23#[derive(Debug, Clone)]
24pub struct LocalVarInfo {
25    /// The inferred type of this variable.
26    pub ty: InferType,
27    /// Whether the variable is mutable.
28    pub is_mut: bool,
29    /// Span of the variable declaration.
30    pub span: Span,
31}
32
33/// Information about a function parameter during constraint generation.
34#[derive(Debug, Clone)]
35pub struct ParamVarInfo {
36    /// The type of this parameter, as InferType for uniform handling.
37    pub ty: InferType,
38}
39
40/// Information about a function during constraint generation.
41///
42/// Uses `InferType` rather than `Type` so that array types are represented
43/// structurally (as `InferType::Array { element, length }`) rather than by
44/// opaque IDs. This allows uniform handling during inference.
45#[derive(Debug, Clone)]
46pub struct FunctionSig {
47    /// Parameter types (in order), as InferTypes for uniform handling.
48    pub param_types: Vec<InferType>,
49    /// Return type, as InferType for uniform handling.
50    pub return_type: InferType,
51    /// Whether this is a generic function (has comptime type parameters).
52    /// Generic functions skip type checking during constraint generation -
53    /// they'll be checked during specialization.
54    pub is_generic: bool,
55    /// Parameter modes (Normal, Inout, Borrow, Comptime).
56    pub param_modes: Vec<gruel_rir::RirParamMode>,
57    /// Which parameters are comptime (declared with `comptime` keyword).
58    /// This is separate from param_modes because `comptime T: type` sets
59    /// is_comptime=true but mode=Normal.
60    pub param_comptime: Vec<bool>,
61    /// Parameter names, needed for type substitution in generic returns.
62    pub param_names: Vec<lasso::Spur>,
63    /// The return type as a symbol (used for substitution lookup).
64    pub return_type_sym: lasso::Spur,
65}
66
67/// Information about a method during constraint generation.
68///
69/// Used for method calls (receiver.method()) and associated function calls (Type::function()).
70#[derive(Debug, Clone)]
71pub struct MethodSig {
72    /// The struct type this method belongs to (as concrete Type::Struct)
73    pub struct_type: Type,
74    /// Whether this is a method (has self) or associated function (no self)
75    pub has_self: bool,
76    /// Parameter types (excluding self), as InferTypes for uniform handling.
77    pub param_types: Vec<InferType>,
78    /// Return type, as InferType for uniform handling.
79    pub return_type: InferType,
80}
81
82/// Context for constraint generation within a single function.
83pub struct ConstraintContext<'a> {
84    /// Local variables in scope.
85    pub locals: HashMap<Spur, LocalVarInfo>,
86    /// Function parameters.
87    pub params: &'a HashMap<Spur, ParamVarInfo>,
88    /// Return type of the current function.
89    pub return_type: Type,
90    /// How many loops we're nested inside (for break/continue validation).
91    pub loop_depth: u32,
92    /// Scope stack for efficient scope management.
93    scope_stack: Vec<Vec<(Spur, Option<LocalVarInfo>)>>,
94}
95
96impl<'a> ConstraintContext<'a> {
97    /// Create a new context for a function.
98    pub fn new(params: &'a HashMap<Spur, ParamVarInfo>, return_type: Type) -> Self {
99        Self {
100            locals: HashMap::new(),
101            params,
102            return_type,
103            loop_depth: 0,
104            scope_stack: Vec::new(),
105        }
106    }
107}
108
109impl ScopedContext for ConstraintContext<'_> {
110    type VarInfo = LocalVarInfo;
111
112    fn locals_mut(&mut self) -> &mut HashMap<Spur, Self::VarInfo> {
113        &mut self.locals
114    }
115
116    fn scope_stack_mut(&mut self) -> &mut Vec<Vec<(Spur, Option<Self::VarInfo>)>> {
117        &mut self.scope_stack
118    }
119}
120
121/// Result of constraint generation for an expression.
122#[derive(Debug, Clone)]
123pub struct ExprInfo {
124    /// The inferred type of this expression.
125    pub ty: InferType,
126    /// The span of this expression (for error reporting).
127    pub span: Span,
128}
129
130impl ExprInfo {
131    /// Create a new expression info.
132    pub fn new(ty: InferType, span: Span) -> Self {
133        Self { ty, span }
134    }
135}
136
137/// Constraint generator that walks RIR and generates type constraints.
138///
139/// This is Phase 1 of HM inference: constraint generation. The constraints
140/// are later solved by the `Unifier` to determine concrete types.
141pub struct ConstraintGenerator<'a> {
142    /// The RIR being analyzed.
143    rir: &'a Rir,
144    /// String interner for resolving symbols.
145    interner: &'a ThreadedRodeo,
146    /// Type variable allocator.
147    type_vars: TypeVarAllocator,
148    /// Collected constraints.
149    constraints: Vec<Constraint>,
150    /// Mapping from RIR instruction to its inferred type.
151    expr_types: HashMap<InstRef, InferType>,
152    /// Function signatures (for call type checking).
153    functions: &'a HashMap<Spur, FunctionSig>,
154    /// Struct types (name -> Type::new_struct(id)).
155    structs: &'a HashMap<Spur, Type>,
156    /// Enum types (name -> Type::new_enum(id)).
157    enums: &'a HashMap<Spur, Type>,
158    /// Method signatures: (struct_id, method_name) -> MethodSig
159    methods: &'a HashMap<(StructId, Spur), MethodSig>,
160    /// Enum method signatures: (enum_id, method_name) -> MethodSig
161    enum_methods: &'a HashMap<(EnumId, Spur), MethodSig>,
162    /// Type variables allocated for integer literals.
163    /// These start as unbound and need to be defaulted to i32 if unconstrained.
164    int_literal_vars: Vec<TypeVarId>,
165    /// Type variables allocated for float literals.
166    /// These start as unbound and need to be defaulted to f64 if unconstrained.
167    float_literal_vars: Vec<TypeVarId>,
168    /// Type substitutions for Self and type parameters (used in method bodies).
169    /// Maps type names (like "Self") to their concrete types.
170    type_subst: Option<&'a HashMap<Spur, Type>>,
171    /// Type intern pool for creating pointer and array types during constraint generation.
172    type_pool: &'a TypeInternPool,
173}
174
175impl<'a> ConstraintGenerator<'a> {
176    /// Create a new constraint generator.
177    #[allow(clippy::too_many_arguments)]
178    pub fn new(
179        rir: &'a Rir,
180        interner: &'a ThreadedRodeo,
181        functions: &'a HashMap<Spur, FunctionSig>,
182        structs: &'a HashMap<Spur, Type>,
183        enums: &'a HashMap<Spur, Type>,
184        methods: &'a HashMap<(StructId, Spur), MethodSig>,
185        enum_methods: &'a HashMap<(EnumId, Spur), MethodSig>,
186        type_pool: &'a TypeInternPool,
187    ) -> Self {
188        Self {
189            rir,
190            interner,
191            type_vars: TypeVarAllocator::new(),
192            constraints: Vec::new(),
193            expr_types: HashMap::new(),
194            functions,
195            structs,
196            enums,
197            methods,
198            enum_methods,
199            int_literal_vars: Vec::new(),
200            float_literal_vars: Vec::new(),
201            type_subst: None,
202            type_pool,
203        }
204    }
205
206    /// Set type substitutions for `Self` and type parameters (builder pattern).
207    ///
208    /// The `type_subst` map provides type substitutions for names like "Self"
209    /// that should be resolved to concrete types during constraint generation.
210    /// This is used for method bodies where `Self { ... }` struct literals
211    /// need to know the concrete struct type.
212    pub fn with_type_subst(mut self, type_subst: Option<&'a HashMap<Spur, Type>>) -> Self {
213        self.type_subst = type_subst;
214        self
215    }
216
217    /// Get the type variables allocated for integer literals.
218    pub fn int_literal_vars(&self) -> &[TypeVarId] {
219        &self.int_literal_vars
220    }
221
222    /// Allocate a fresh type variable.
223    pub fn fresh_var(&mut self) -> TypeVarId {
224        self.type_vars.fresh()
225    }
226
227    /// Add a constraint.
228    pub fn add_constraint(&mut self, constraint: Constraint) {
229        self.constraints.push(constraint);
230    }
231
232    /// Record the type of an expression.
233    pub fn record_type(&mut self, inst_ref: InstRef, ty: InferType) {
234        self.expr_types.insert(inst_ref, ty);
235    }
236
237    /// Get the recorded type of an expression.
238    pub fn get_type(&self, inst_ref: InstRef) -> Option<&InferType> {
239        self.expr_types.get(&inst_ref)
240    }
241
242    /// Get all collected constraints.
243    pub fn constraints(&self) -> &[Constraint] {
244        &self.constraints
245    }
246
247    /// Take ownership of the collected constraints.
248    pub fn take_constraints(self) -> Vec<Constraint> {
249        self.constraints
250    }
251
252    /// Get the expression type mapping.
253    pub fn expr_types(&self) -> &HashMap<InstRef, InferType> {
254        &self.expr_types
255    }
256
257    /// Consume the constraint generator and return (constraints, int_literal_vars, float_literal_vars, expr_types, type_var_count).
258    ///
259    /// This is useful when you need ownership of the expression types map.
260    /// The `type_var_count` can be used to pre-size the unifier's substitution for better performance.
261    pub fn into_parts(
262        self,
263    ) -> (
264        Vec<Constraint>,
265        Vec<TypeVarId>,
266        Vec<TypeVarId>,
267        HashMap<InstRef, InferType>,
268        u32,
269    ) {
270        (
271            self.constraints,
272            self.int_literal_vars,
273            self.float_literal_vars,
274            self.expr_types,
275            self.type_vars.count(),
276        )
277    }
278
279    /// Generate constraints for an expression.
280    ///
281    /// Returns the inferred type of the expression. Records the type in
282    /// `expr_types` and adds constraints to `constraints`.
283    pub fn generate(&mut self, inst_ref: InstRef, ctx: &mut ConstraintContext) -> ExprInfo {
284        let inst = self.rir.get(inst_ref);
285        let span = inst.span;
286
287        let ty = match &inst.data {
288            InstData::IntConst(_) => {
289                // Integer literals get a fresh type variable that we immediately
290                // bind to IntLiteral. This allows unification to track when the
291                // literal is constrained to a specific integer type.
292                //
293                // Example: `let x: i64 = 42` generates:
294                //   - type_var(?0) for the literal 42
295                //   - substitution: ?0 -> IntLiteral
296                //   - constraint: Equal(Var(?0), Concrete(i64))
297                //
298                // During unification, Equal(IntLiteral, Concrete(i64)) succeeds
299                // and rebinds ?0 -> Concrete(i64) via rebind_int_literal_to_concrete.
300                let var = self.fresh_var();
301                self.int_literal_vars.push(var);
302                InferType::Var(var)
303            }
304
305            InstData::FloatConst(_) => {
306                // Float literals work like int literals but default to f64.
307                let var = self.fresh_var();
308                self.float_literal_vars.push(var);
309                InferType::Var(var)
310            }
311
312            InstData::BoolConst(_) => InferType::Concrete(Type::BOOL),
313
314            // String constants use the builtin String struct type.
315            InstData::StringConst(_) => {
316                // Look up the String type from the structs map
317                if let Some(string_spur) = self.interner.get("String") {
318                    if let Some(&string_ty) = self.structs.get(&string_spur) {
319                        InferType::Concrete(string_ty)
320                    } else {
321                        // Fallback if String struct not found (shouldn't happen after builtin injection)
322                        InferType::Concrete(Type::ERROR)
323                    }
324                } else {
325                    InferType::Concrete(Type::ERROR)
326                }
327            }
328
329            InstData::UnitConst => InferType::Concrete(Type::UNIT),
330
331            // Binary arithmetic: both operands must have the same numeric type
332            InstData::Add { lhs, rhs }
333            | InstData::Sub { lhs, rhs }
334            | InstData::Mul { lhs, rhs }
335            | InstData::Div { lhs, rhs }
336            | InstData::Mod { lhs, rhs } => self.generate_binary_arith(*lhs, *rhs, ctx),
337
338            // Bitwise operations: operands must be integers (no floats)
339            InstData::BitAnd { lhs, rhs }
340            | InstData::BitOr { lhs, rhs }
341            | InstData::BitXor { lhs, rhs }
342            | InstData::Shl { lhs, rhs }
343            | InstData::Shr { lhs, rhs } => self.generate_binary_bitwise(*lhs, *rhs, ctx),
344
345            // Comparison operators: operands must match, result is bool
346            InstData::Eq { lhs, rhs }
347            | InstData::Ne { lhs, rhs }
348            | InstData::Lt { lhs, rhs }
349            | InstData::Gt { lhs, rhs }
350            | InstData::Le { lhs, rhs }
351            | InstData::Ge { lhs, rhs } => {
352                let lhs_info = self.generate(*lhs, ctx);
353                let rhs_info = self.generate(*rhs, ctx);
354                // Operands must have the same type
355                self.add_constraint(Constraint::equal(lhs_info.ty, rhs_info.ty, span));
356                InferType::Concrete(Type::BOOL)
357            }
358
359            // Logical operators: operands must be bool, result is bool
360            InstData::And { lhs, rhs } | InstData::Or { lhs, rhs } => {
361                let lhs_info = self.generate(*lhs, ctx);
362                let rhs_info = self.generate(*rhs, ctx);
363                self.add_constraint(Constraint::equal(
364                    lhs_info.ty,
365                    InferType::Concrete(Type::BOOL),
366                    lhs_info.span,
367                ));
368                self.add_constraint(Constraint::equal(
369                    rhs_info.ty,
370                    InferType::Concrete(Type::BOOL),
371                    rhs_info.span,
372                ));
373                InferType::Concrete(Type::BOOL)
374            }
375
376            // Unary negation: operand must be signed integer
377            InstData::Neg { operand } => {
378                let operand_info = self.generate(*operand, ctx);
379                // Result type is the same as operand type
380                let result_ty = operand_info.ty.clone();
381                // Must be a signed integer
382                self.add_constraint(Constraint::is_signed(result_ty.clone(), span));
383                result_ty
384            }
385
386            // Logical NOT: operand must be bool
387            InstData::Not { operand } => {
388                let operand_info = self.generate(*operand, ctx);
389                self.add_constraint(Constraint::equal(
390                    operand_info.ty,
391                    InferType::Concrete(Type::BOOL),
392                    operand_info.span,
393                ));
394                InferType::Concrete(Type::BOOL)
395            }
396
397            // Bitwise NOT: operand must be integer
398            InstData::BitNot { operand } => {
399                let operand_info = self.generate(*operand, ctx);
400                let result_ty = operand_info.ty.clone();
401                // Must be an integer type (signed or unsigned)
402                self.add_constraint(Constraint::is_integer(result_ty.clone(), span));
403                result_ty
404            }
405
406            // Variable reference
407            InstData::VarRef { name } => {
408                if let Some(local) = ctx.locals.get(name) {
409                    local.ty.clone()
410                } else if let Some(param) = ctx.params.get(name) {
411                    param.ty.clone()
412                } else {
413                    // Unknown variable - will be caught during semantic analysis
414                    InferType::Concrete(Type::ERROR)
415                }
416            }
417
418            // Parameter reference
419            InstData::ParamRef { name, .. } => {
420                if let Some(param) = ctx.params.get(name) {
421                    param.ty.clone()
422                } else {
423                    InferType::Concrete(Type::ERROR)
424                }
425            }
426
427            // Local variable allocation
428            InstData::Alloc {
429                directives_start: _,
430                directives_len: _,
431                name,
432                is_mut,
433                ty: type_annotation,
434                init,
435            } => {
436                let init_info = self.generate(*init, ctx);
437
438                let var_ty = if let Some(ty_sym) = type_annotation {
439                    // Explicit type annotation - use it and constrain init to match
440                    let ty_name = self.interner.resolve(ty_sym);
441                    if let Some(annotated_ty) = self.resolve_type_name(ty_name) {
442                        self.add_constraint(Constraint::equal(
443                            init_info.ty,
444                            annotated_ty.clone(),
445                            span,
446                        ));
447                        annotated_ty
448                    } else {
449                        // Unknown type name (e.g., struct/enum) - use init type for now.
450                        // Semantic analysis will catch undefined types and verify struct/enum
451                        // field types match the definition.
452                        init_info.ty
453                    }
454                } else {
455                    // No annotation - use the init expression's type
456                    init_info.ty
457                };
458
459                // Record the variable in scope (if it has a name)
460                if let Some(var_name) = name {
461                    ctx.insert_local(
462                        *var_name,
463                        LocalVarInfo {
464                            ty: var_ty.clone(),
465                            is_mut: *is_mut,
466                            span,
467                        },
468                    );
469                }
470
471                // Alloc produces unit type
472                InferType::Concrete(Type::UNIT)
473            }
474
475            // Struct destructuring — register field bindings for type inference
476            InstData::StructDestructure {
477                type_name,
478                fields_start,
479                fields_len,
480                init,
481            } => {
482                self.generate(*init, ctx);
483
484                // Look up the struct type to get field types
485                if let Some(&struct_ty) = self.structs.get(type_name)
486                    && let Some(struct_id) = struct_ty.as_struct()
487                {
488                    let struct_def = self.type_pool.struct_def(struct_id);
489                    let rir_fields = self.rir.get_destructure_fields(*fields_start, *fields_len);
490                    for field in &rir_fields {
491                        if field.is_wildcard {
492                            continue;
493                        }
494                        let field_name = self.interner.resolve(&field.field_name);
495                        if let Some((_, struct_field)) = struct_def.find_field(field_name) {
496                            let binding_name = field.binding_name.unwrap_or(field.field_name);
497                            ctx.insert_local(
498                                binding_name,
499                                LocalVarInfo {
500                                    ty: InferType::Concrete(struct_field.ty),
501                                    is_mut: field.is_mut,
502                                    span,
503                                },
504                            );
505                        }
506                    }
507                }
508
509                InferType::Concrete(Type::UNIT)
510            }
511
512            // Assignment
513            InstData::Assign { name, value } => {
514                let value_info = self.generate(*value, ctx);
515                if let Some(local) = ctx.locals.get(name) {
516                    // Constrain value to match variable type
517                    self.add_constraint(Constraint::equal(value_info.ty, local.ty.clone(), span));
518                }
519                // Assignment produces unit
520                InferType::Concrete(Type::UNIT)
521            }
522
523            // Return statement
524            InstData::Ret(value) => {
525                if let Some(val_ref) = value {
526                    let value_info = self.generate(*val_ref, ctx);
527                    // Constrain return value to match function return type
528                    self.add_constraint(Constraint::equal(
529                        value_info.ty,
530                        InferType::Concrete(ctx.return_type),
531                        span,
532                    ));
533                } else {
534                    // Return without value - function must return unit
535                    self.add_constraint(Constraint::equal(
536                        InferType::Concrete(Type::UNIT),
537                        InferType::Concrete(ctx.return_type),
538                        span,
539                    ));
540                }
541                // Return diverges
542                InferType::Concrete(Type::NEVER)
543            }
544
545            // Function call
546            InstData::Call {
547                name,
548                args_start,
549                args_len,
550            } => {
551                let args = self.rir.get_call_args(*args_start, *args_len);
552                if let Some(func) = self.functions.get(name) {
553                    // For generic functions, skip constraint generation for arguments.
554                    // The types will be checked during specialization when we know
555                    // the concrete type substitutions.
556                    if func.is_generic {
557                        // Process all arguments and build type substitution map
558                        let mut type_subst: std::collections::HashMap<lasso::Spur, Type> =
559                            std::collections::HashMap::new();
560
561                        for (i, arg) in args.iter().enumerate() {
562                            let arg_info = self.generate(arg.value, ctx);
563
564                            // If this is a comptime parameter, extract the type for substitution
565                            if i < func.param_comptime.len() && func.param_comptime[i] {
566                                // The argument should be a TypeConst - extract the concrete type
567                                if let InferType::Concrete(Type::COMPTIME_TYPE) = &arg_info.ty {
568                                    // This is a type value - get the actual type from the RIR
569                                    let arg_inst = self.rir.get(arg.value);
570                                    if let gruel_rir::InstData::TypeConst { type_name } =
571                                        &arg_inst.data
572                                    {
573                                        // Resolve type_name to a concrete Type
574                                        let type_name_str = self.interner.resolve(type_name);
575                                        let concrete_ty = match type_name_str {
576                                            "i8" => Type::I8,
577                                            "i16" => Type::I16,
578                                            "i32" => Type::I32,
579                                            "i64" => Type::I64,
580                                            "u8" => Type::U8,
581                                            "u16" => Type::U16,
582                                            "u32" => Type::U32,
583                                            "u64" => Type::U64,
584                                            "bool" => Type::BOOL,
585                                            "()" => Type::UNIT,
586                                            _ => Type::ERROR, // Unknown type
587                                        };
588                                        if i < func.param_names.len() {
589                                            type_subst.insert(func.param_names[i], concrete_ty);
590                                        }
591                                    }
592                                }
593                            }
594                        }
595
596                        // Compute the actual return type by substituting type parameters
597
598                        if func.return_type == InferType::Concrete(Type::COMPTIME_TYPE) {
599                            // Return type is a type parameter - look it up in substitutions
600                            if let Some(&concrete_ty) = type_subst.get(&func.return_type_sym) {
601                                InferType::Concrete(concrete_ty)
602                            } else {
603                                func.return_type.clone()
604                            }
605                        } else {
606                            func.return_type.clone()
607                        }
608                    } else if args.len() != func.param_types.len() {
609                        // Check argument count matches parameter count.
610                        // Semantic analysis will emit a proper error; we just need to avoid
611                        // panicking and process what we can.
612                        // Still process all arguments to catch type errors within them
613                        for arg in args.iter() {
614                            self.generate(arg.value, ctx);
615                        }
616                        // Return the declared return type (error will be caught in sema)
617                        func.return_type.clone()
618                    } else {
619                        // Generate constraints for each argument
620                        for (arg, param_ty) in args.iter().zip(func.param_types.iter()) {
621                            let arg_info = self.generate(arg.value, ctx);
622                            self.add_constraint(Constraint::equal(
623                                arg_info.ty,
624                                param_ty.clone(),
625                                arg_info.span,
626                            ));
627                        }
628                        func.return_type.clone()
629                    }
630                } else {
631                    // Unknown function - still process arguments for constraint generation
632                    for arg in args.iter() {
633                        self.generate(arg.value, ctx);
634                    }
635                    InferType::Concrete(Type::ERROR)
636                }
637            }
638
639            // Intrinsic call
640            InstData::Intrinsic {
641                name,
642                args_start,
643                args_len,
644            } => {
645                let intrinsic_name = self.interner.resolve(name);
646                let args = self.rir.get_inst_refs(*args_start, *args_len);
647
648                if intrinsic_name == "intCast" || intrinsic_name == "cast" {
649                    // @intCast/@cast: target type is inferred from context
650                    // The argument must be a numeric type (checked in sema)
651                    if !args.is_empty() {
652                        let arg_info = self.generate(args[0], ctx);
653                        let _ = arg_info;
654                    }
655                    // Return type is inferred from context - create a fresh type variable
656                    let result_var = self.fresh_var();
657                    InferType::Var(result_var)
658                } else if intrinsic_name == "read_line" {
659                    // @read_line: returns String (same as string constants)
660                    if let Some(string_spur) = self.interner.get("String") {
661                        if let Some(&string_ty) = self.structs.get(&string_spur) {
662                            InferType::Concrete(string_ty)
663                        } else {
664                            // Fallback if String struct not found
665                            InferType::Concrete(Type::ERROR)
666                        }
667                    } else {
668                        InferType::Concrete(Type::ERROR)
669                    }
670                } else if intrinsic_name == "parse_i32" {
671                    // @parse_i32: takes a String, returns i32
672                    for arg_ref in args.iter() {
673                        self.generate(*arg_ref, ctx);
674                    }
675                    InferType::Concrete(Type::I32)
676                } else if intrinsic_name == "parse_i64" {
677                    // @parse_i64: takes a String, returns i64
678                    for arg_ref in args.iter() {
679                        self.generate(*arg_ref, ctx);
680                    }
681                    InferType::Concrete(Type::I64)
682                } else if intrinsic_name == "parse_u32" {
683                    // @parse_u32: takes a String, returns u32
684                    for arg_ref in args.iter() {
685                        self.generate(*arg_ref, ctx);
686                    }
687                    InferType::Concrete(Type::U32)
688                } else if intrinsic_name == "parse_u64" {
689                    // @parse_u64: takes a String, returns u64
690                    for arg_ref in args.iter() {
691                        self.generate(*arg_ref, ctx);
692                    }
693                    InferType::Concrete(Type::U64)
694                } else if intrinsic_name == "random_u32" {
695                    // @random_u32: no arguments, returns u32
696                    InferType::Concrete(Type::U32)
697                } else if intrinsic_name == "random_u64" {
698                    // @random_u64: no arguments, returns u64
699                    InferType::Concrete(Type::U64)
700                } else if intrinsic_name == "syscall" {
701                    // @syscall: syscall_num and up to 6 args (all u64), returns i64
702                    for arg_ref in args.iter() {
703                        self.generate(*arg_ref, ctx);
704                    }
705                    InferType::Concrete(Type::I64)
706                } else if intrinsic_name == "ptr_to_int" {
707                    // @ptr_to_int: takes a pointer, returns u64
708                    for arg_ref in args.iter() {
709                        self.generate(*arg_ref, ctx);
710                    }
711                    InferType::Concrete(Type::U64)
712                } else if intrinsic_name == "ptr_write" {
713                    // @ptr_write: takes a pointer and value, returns unit
714                    for arg_ref in args.iter() {
715                        self.generate(*arg_ref, ctx);
716                    }
717                    InferType::Concrete(Type::UNIT)
718                } else if intrinsic_name == "is_null" {
719                    // @is_null: takes a pointer, returns bool
720                    for arg_ref in args.iter() {
721                        self.generate(*arg_ref, ctx);
722                    }
723                    InferType::Concrete(Type::BOOL)
724                } else if intrinsic_name == "ptr_read" {
725                    // @ptr_read: takes ptr const T or ptr mut T, returns T
726                    // The return type depends on the pointee type of the argument.
727                    // We create a fresh type variable that will be resolved during
728                    // semantic analysis when the actual pointer type is known.
729                    for arg_ref in args.iter() {
730                        self.generate(*arg_ref, ctx);
731                    }
732                    let result_var = self.fresh_var();
733                    InferType::Var(result_var)
734                } else if intrinsic_name == "ptr_offset" {
735                    // @ptr_offset: takes (ptr T, i64), returns ptr T
736                    // The return type is the same as the input pointer type.
737                    // We create a fresh type variable for proper inference.
738                    for arg_ref in args.iter() {
739                        self.generate(*arg_ref, ctx);
740                    }
741                    let result_var = self.fresh_var();
742                    InferType::Var(result_var)
743                } else if intrinsic_name == "raw" || intrinsic_name == "raw_mut" {
744                    // @raw / @raw_mut: takes a value, returns a pointer to it
745                    // The return type is ptr const T or ptr mut T where T is the argument type.
746                    // We create a fresh type variable for proper inference.
747                    for arg_ref in args.iter() {
748                        self.generate(*arg_ref, ctx);
749                    }
750                    let result_var = self.fresh_var();
751                    InferType::Var(result_var)
752                } else if intrinsic_name == "int_to_ptr" || intrinsic_name == "null_ptr" {
753                    // @int_to_ptr / @null_ptr: returns a pointer type inferred from context
754                    for arg_ref in args.iter() {
755                        self.generate(*arg_ref, ctx);
756                    }
757                    let result_var = self.fresh_var();
758                    InferType::Var(result_var)
759                } else if intrinsic_name == "ptr_copy" {
760                    // @ptr_copy: (dst: ptr mut T, src: ptr const T, count: u64) -> ()
761                    for arg_ref in args.iter() {
762                        self.generate(*arg_ref, ctx);
763                    }
764                    InferType::Concrete(Type::UNIT)
765                } else if intrinsic_name == "target_arch" {
766                    // @target_arch: returns Arch enum
767                    if let Some(arch_spur) = self.interner.get("Arch") {
768                        if let Some(&arch_ty) = self.enums.get(&arch_spur) {
769                            InferType::Concrete(arch_ty)
770                        } else {
771                            InferType::Concrete(Type::ERROR)
772                        }
773                    } else {
774                        InferType::Concrete(Type::ERROR)
775                    }
776                } else if intrinsic_name == "target_os" {
777                    // @target_os: returns Os enum
778                    if let Some(os_spur) = self.interner.get("Os") {
779                        if let Some(&os_ty) = self.enums.get(&os_spur) {
780                            InferType::Concrete(os_ty)
781                        } else {
782                            InferType::Concrete(Type::ERROR)
783                        }
784                    } else {
785                        InferType::Concrete(Type::ERROR)
786                    }
787                } else if intrinsic_name == "range" {
788                    // @range: takes 1-3 integer args, returns the same integer type
789                    // (used as iterable in for-in loops)
790                    if !args.is_empty() {
791                        let first = self.generate(args[0], ctx);
792                        for arg_ref in args.iter().skip(1) {
793                            let arg_info = self.generate(*arg_ref, ctx);
794                            self.add_constraint(Constraint::equal(
795                                first.ty.clone(),
796                                arg_info.ty,
797                                span,
798                            ));
799                        }
800                        first.ty
801                    } else {
802                        InferType::Concrete(Type::ERROR)
803                    }
804                } else if intrinsic_name == "field" {
805                    // @field(value, field_name): returns the type of the named field.
806                    // The field name is a comptime_str resolved at compile time, so
807                    // the return type depends on which field is accessed. Use a fresh
808                    // type variable — sema determines the concrete type.
809                    for arg_ref in args.iter() {
810                        self.generate(*arg_ref, ctx);
811                    }
812                    let result_var = self.fresh_var();
813                    InferType::Var(result_var)
814                } else {
815                    // Generate constraints for arguments (they need to be processed)
816                    for arg_ref in args.iter() {
817                        self.generate(*arg_ref, ctx);
818                    }
819                    // @dbg and other intrinsics return Unit
820                    InferType::Concrete(Type::UNIT)
821                }
822            }
823
824            // Type intrinsic (@size_of, @align_of, @typeName, @typeInfo)
825            InstData::TypeIntrinsic { name, type_arg: _ } => {
826                let intrinsic_name = self.interner.resolve(name);
827                match intrinsic_name {
828                    "typeName" => InferType::Concrete(Type::COMPTIME_STR),
829                    "typeInfo" => {
830                        // @typeInfo returns a comptime struct — use a fresh var
831                        // since the actual type is determined by the comptime evaluator.
832                        InferType::Var(self.fresh_var())
833                    }
834                    _ => {
835                        // @size_of, @align_of return i32
836                        InferType::Concrete(Type::I32)
837                    }
838                }
839            }
840
841            // Block
842            InstData::Block { extra_start, len } => {
843                ctx.push_scope();
844                let mut last_ty = InferType::Concrete(Type::UNIT);
845                let block_insts = self.rir.get_extra(*extra_start, *len);
846                for &inst_raw in block_insts {
847                    let block_inst_ref = InstRef::from_raw(inst_raw);
848                    let info = self.generate(block_inst_ref, ctx);
849                    last_ty = info.ty;
850                }
851                ctx.pop_scope();
852                last_ty
853            }
854
855            // Branch (if/else)
856            InstData::Branch {
857                cond,
858                then_block,
859                else_block,
860            } => {
861                let cond_info = self.generate(*cond, ctx);
862                self.add_constraint(Constraint::equal(
863                    cond_info.ty,
864                    InferType::Concrete(Type::BOOL),
865                    cond_info.span,
866                ));
867
868                let then_info = self.generate(*then_block, ctx);
869
870                if let Some(else_ref) = else_block {
871                    let else_info = self.generate(*else_ref, ctx);
872
873                    // Handle Never type coercion:
874                    // - If one branch is Never, the if-else takes the other branch's type
875                    // - If both are Never, the result is Never
876                    // - Otherwise, both must unify to the same type
877                    let then_is_never = matches!(&then_info.ty, InferType::Concrete(Type::NEVER));
878                    let else_is_never = matches!(&else_info.ty, InferType::Concrete(Type::NEVER));
879
880                    match (then_is_never, else_is_never) {
881                        (true, true) => {
882                            // Both diverge - result is Never
883                            InferType::Concrete(Type::NEVER)
884                        }
885                        (true, false) => {
886                            // Then diverges - result is else type
887                            else_info.ty
888                        }
889                        (false, true) => {
890                            // Else diverges - result is then type
891                            then_info.ty
892                        }
893                        (false, false) => {
894                            // Neither diverges - both must have the same type
895                            let result_var = self.fresh_var();
896                            let result_ty = InferType::Var(result_var);
897                            self.add_constraint(Constraint::equal(
898                                then_info.ty,
899                                result_ty.clone(),
900                                then_info.span,
901                            ));
902                            self.add_constraint(Constraint::equal(
903                                else_info.ty,
904                                result_ty.clone(),
905                                else_info.span,
906                            ));
907                            result_ty
908                        }
909                    }
910                } else {
911                    // No else branch - the if expression has unit type
912                    // (or the then branch type if it's unit-compatible)
913                    InferType::Concrete(Type::UNIT)
914                }
915            }
916
917            // While loop
918            InstData::Loop { cond, body } => {
919                let cond_info = self.generate(*cond, ctx);
920                self.add_constraint(Constraint::equal(
921                    cond_info.ty,
922                    InferType::Concrete(Type::BOOL),
923                    cond_info.span,
924                ));
925
926                ctx.loop_depth += 1;
927                self.generate(*body, ctx);
928                ctx.loop_depth -= 1;
929
930                // Loops produce unit
931                InferType::Concrete(Type::UNIT)
932            }
933
934            // For-in loop (desugared to while in sema, but inference still sees it)
935            InstData::For {
936                binding,
937                is_mut,
938                iterable,
939                body,
940            } => {
941                // Generate constraints for the iterable to determine the element type
942                let iterable_info = self.generate(*iterable, ctx);
943
944                // Determine the binding type from the iterable:
945                // - For @range: the iterable returns the integer type directly
946                // - For arrays: extract the element type from InferType::Array
947                let binding_ty = match &iterable_info.ty {
948                    InferType::Array { element, .. } => *element.clone(),
949                    other => other.clone(),
950                };
951
952                // Register the binding so the body can reference it
953                ctx.insert_local(
954                    *binding,
955                    LocalVarInfo {
956                        ty: binding_ty,
957                        is_mut: *is_mut,
958                        span,
959                    },
960                );
961
962                ctx.loop_depth += 1;
963                self.generate(*body, ctx);
964                ctx.loop_depth -= 1;
965
966                // For loops produce unit
967                InferType::Concrete(Type::UNIT)
968            }
969
970            // Infinite loop
971            InstData::InfiniteLoop { body } => {
972                ctx.loop_depth += 1;
973                self.generate(*body, ctx);
974                ctx.loop_depth -= 1;
975
976                // Infinite loop without break never returns
977                InferType::Concrete(Type::NEVER)
978            }
979
980            // Break/Continue
981            InstData::Break | InstData::Continue => InferType::Concrete(Type::NEVER),
982
983            // Match expression
984            InstData::Match {
985                scrutinee,
986                arms_start,
987                arms_len,
988            } => {
989                let scrutinee_info = self.generate(*scrutinee, ctx);
990                let arms = self.rir.get_match_arms(*arms_start, *arms_len);
991
992                // Collect arm types, handling Never coercion
993                let mut arm_types: Vec<ExprInfo> = Vec::new();
994                for (pattern, body) in arms.iter() {
995                    // Patterns constrain the scrutinee type
996                    let pattern_ty = self.pattern_type(pattern);
997                    self.add_constraint(Constraint::equal(
998                        scrutinee_info.ty.clone(),
999                        pattern_ty,
1000                        pattern.span(),
1001                    ));
1002
1003                    // For DataVariant/StructVariant patterns, add bound variables to scope before
1004                    // generating body constraints, so VarRef lookups resolve correctly.
1005                    let bindings_to_remove = match pattern {
1006                        gruel_rir::RirPattern::DataVariant {
1007                            type_name,
1008                            variant,
1009                            bindings,
1010                            ..
1011                        } => {
1012                            let mut added_bindings = Vec::new();
1013                            if let Some(&enum_ty) = self.enums.get(type_name)
1014                                && let Some(enum_id) = enum_ty.as_enum()
1015                            {
1016                                let enum_def = self.type_pool.enum_def(enum_id);
1017                                let variant_name = self.interner.resolve(variant);
1018                                if let Some(variant_idx) = enum_def.find_variant(variant_name) {
1019                                    let field_types = &enum_def.variants[variant_idx].fields;
1020                                    for (i, binding) in bindings.iter().enumerate() {
1021                                        if !binding.is_wildcard
1022                                            && let Some(name) = binding.name
1023                                        {
1024                                            let field_ty = if i < field_types.len() {
1025                                                InferType::Concrete(field_types[i])
1026                                            } else {
1027                                                InferType::Concrete(Type::ERROR)
1028                                            };
1029                                            let old = ctx.locals.insert(
1030                                                name,
1031                                                LocalVarInfo {
1032                                                    ty: field_ty,
1033                                                    is_mut: binding.is_mut,
1034                                                    span: pattern.span(),
1035                                                },
1036                                            );
1037                                            added_bindings.push((name, old));
1038                                        }
1039                                    }
1040                                }
1041                            } else {
1042                                // Enum not found — likely a comptime type variable.
1043                                // Register bindings with fresh type variables so body
1044                                // constraint generation can still resolve variable references.
1045                                for binding in bindings.iter() {
1046                                    if !binding.is_wildcard
1047                                        && let Some(name) = binding.name
1048                                    {
1049                                        let var = self.fresh_var();
1050                                        let old = ctx.locals.insert(
1051                                            name,
1052                                            LocalVarInfo {
1053                                                ty: InferType::Var(var),
1054                                                is_mut: binding.is_mut,
1055                                                span: pattern.span(),
1056                                            },
1057                                        );
1058                                        added_bindings.push((name, old));
1059                                    }
1060                                }
1061                            }
1062                            added_bindings
1063                        }
1064                        gruel_rir::RirPattern::StructVariant {
1065                            type_name,
1066                            variant,
1067                            field_bindings,
1068                            ..
1069                        } => {
1070                            let mut added_bindings = Vec::new();
1071                            if let Some(&enum_ty) = self.enums.get(type_name)
1072                                && let Some(enum_id) = enum_ty.as_enum()
1073                            {
1074                                let enum_def = self.type_pool.enum_def(enum_id);
1075                                let variant_name = self.interner.resolve(variant);
1076                                if let Some(variant_idx) = enum_def.find_variant(variant_name) {
1077                                    let variant_def = &enum_def.variants[variant_idx];
1078                                    for fb in field_bindings {
1079                                        if !fb.binding.is_wildcard
1080                                            && let Some(name) = fb.binding.name
1081                                        {
1082                                            let field_name = self.interner.resolve(&fb.field_name);
1083                                            let field_ty = if let Some(idx) =
1084                                                variant_def.find_field(field_name)
1085                                            {
1086                                                InferType::Concrete(variant_def.fields[idx])
1087                                            } else {
1088                                                InferType::Concrete(Type::ERROR)
1089                                            };
1090                                            let old = ctx.locals.insert(
1091                                                name,
1092                                                LocalVarInfo {
1093                                                    ty: field_ty,
1094                                                    is_mut: fb.binding.is_mut,
1095                                                    span: pattern.span(),
1096                                                },
1097                                            );
1098                                            added_bindings.push((name, old));
1099                                        }
1100                                    }
1101                                }
1102                            } else {
1103                                // Enum not found — likely a comptime type variable.
1104                                // Register bindings with fresh type variables.
1105                                for fb in field_bindings {
1106                                    if !fb.binding.is_wildcard
1107                                        && let Some(name) = fb.binding.name
1108                                    {
1109                                        let var = self.fresh_var();
1110                                        let old = ctx.locals.insert(
1111                                            name,
1112                                            LocalVarInfo {
1113                                                ty: InferType::Var(var),
1114                                                is_mut: fb.binding.is_mut,
1115                                                span: pattern.span(),
1116                                            },
1117                                        );
1118                                        added_bindings.push((name, old));
1119                                    }
1120                                }
1121                            }
1122                            added_bindings
1123                        }
1124                        _ => Vec::new(),
1125                    };
1126
1127                    // Generate body and collect its type
1128                    let body_info = self.generate(*body, ctx);
1129                    arm_types.push(body_info);
1130
1131                    // Remove DataVariant bindings from scope after body generation
1132                    for (name, old_val) in bindings_to_remove {
1133                        match old_val {
1134                            Some(prev) => {
1135                                ctx.locals.insert(name, prev);
1136                            }
1137                            None => {
1138                                ctx.locals.remove(&name);
1139                            }
1140                        }
1141                    }
1142                }
1143
1144                // Handle Never type coercion:
1145                // Filter out Never arms and use the remaining non-Never types
1146                let non_never_arms: Vec<_> = arm_types
1147                    .iter()
1148                    .filter(|info| !matches!(&info.ty, InferType::Concrete(Type::NEVER)))
1149                    .collect();
1150
1151                if non_never_arms.is_empty() {
1152                    // All arms diverge - result is Never
1153                    InferType::Concrete(Type::NEVER)
1154                } else {
1155                    // Create constraints for non-Never arms to have the same type
1156                    let result_var = self.fresh_var();
1157                    let result_ty = InferType::Var(result_var);
1158                    for arm_info in non_never_arms {
1159                        self.add_constraint(Constraint::equal(
1160                            arm_info.ty.clone(),
1161                            result_ty.clone(),
1162                            arm_info.span,
1163                        ));
1164                    }
1165                    result_ty
1166                }
1167            }
1168
1169            // Struct initialization
1170            InstData::StructInit {
1171                type_name,
1172                fields_start,
1173                fields_len,
1174                ..
1175            } => {
1176                // Check type_subst first (for Self and type parameters in method bodies)
1177                let struct_ty = self
1178                    .type_subst
1179                    .and_then(|subst| subst.get(type_name).copied())
1180                    .or_else(|| self.structs.get(type_name).copied());
1181
1182                if let Some(struct_ty) = struct_ty {
1183                    let fields = self.rir.get_field_inits(*fields_start, *fields_len);
1184                    // Generate constraints for each field
1185                    for (_, value_ref) in fields.iter() {
1186                        self.generate(*value_ref, ctx);
1187                    }
1188                    InferType::Concrete(struct_ty)
1189                } else {
1190                    InferType::Concrete(Type::ERROR)
1191                }
1192            }
1193
1194            // Field access
1195            InstData::FieldGet { base, field: _ } => {
1196                // Generate constraints for the base expression (needed for nested field access)
1197                let _base_info = self.generate(*base, ctx);
1198                // We need to look up the field type from the struct definition.
1199                // For now, use a fresh type variable - full resolution happens during
1200                // semantic analysis which has access to struct definitions.
1201                let result_var = self.fresh_var();
1202                InferType::Var(result_var)
1203            }
1204
1205            // Field assignment
1206            InstData::FieldSet {
1207                base,
1208                field: _,
1209                value,
1210            } => {
1211                self.generate(*base, ctx);
1212                self.generate(*value, ctx);
1213                InferType::Concrete(Type::UNIT)
1214            }
1215
1216            // Enum variant (unit or path)
1217            InstData::EnumVariant { type_name, .. } => {
1218                if let Some(&enum_ty) = self.enums.get(type_name) {
1219                    InferType::Concrete(enum_ty)
1220                } else {
1221                    // May be a comptime type variable — use fresh var
1222                    let var = self.fresh_var();
1223                    InferType::Var(var)
1224                }
1225            }
1226
1227            // Enum struct variant construction
1228            InstData::EnumStructVariant {
1229                type_name,
1230                fields_start,
1231                fields_len,
1232                ..
1233            } => {
1234                // Generate constraints for field value expressions
1235                let fields = self.rir.get_field_inits(*fields_start, *fields_len);
1236                for (_, value_ref) in fields.iter() {
1237                    self.generate(*value_ref, ctx);
1238                }
1239                if let Some(&enum_ty) = self.enums.get(type_name) {
1240                    InferType::Concrete(enum_ty)
1241                } else {
1242                    // May be a comptime type variable — use fresh var
1243                    let var = self.fresh_var();
1244                    InferType::Var(var)
1245                }
1246            }
1247
1248            // Array initialization
1249            InstData::ArrayInit {
1250                elems_start,
1251                elems_len,
1252            } => {
1253                let elements = self.rir.get_inst_refs(*elems_start, *elems_len);
1254                if elements.is_empty() {
1255                    // Empty array - need type annotation to know element type
1256                    // Use a fresh type variable for the element type
1257                    let elem_var = self.fresh_var();
1258                    InferType::Array {
1259                        element: Box::new(InferType::Var(elem_var)),
1260                        length: 0,
1261                    }
1262                } else {
1263                    // Get element type from first element, constrain rest to match
1264                    let first_info = self.generate(elements[0], ctx);
1265                    for elem_ref in elements.iter().skip(1) {
1266                        let elem_info = self.generate(*elem_ref, ctx);
1267                        self.add_constraint(Constraint::equal(
1268                            elem_info.ty,
1269                            first_info.ty.clone(),
1270                            elem_info.span,
1271                        ));
1272                    }
1273                    // Build the array type with the inferred element type
1274                    InferType::Array {
1275                        element: Box::new(first_info.ty),
1276                        length: elements.len() as u64,
1277                    }
1278                }
1279            }
1280
1281            // Array index
1282            InstData::IndexGet { base, index } => {
1283                let base_info = self.generate(*base, ctx);
1284                let index_info = self.generate(*index, ctx);
1285                // Index must be an unsigned integer type
1286                self.add_constraint(Constraint::is_unsigned(index_info.ty, index_info.span));
1287
1288                // Extract element type from array type.
1289                // If base is InferType::Array, we can get the element type directly.
1290                // Otherwise, we need a fresh variable that will be resolved later.
1291                match &base_info.ty {
1292                    InferType::Array { element, .. } => (**element).clone(),
1293                    _ => {
1294                        // Base might be a type variable that will resolve to an array.
1295                        // Use a fresh variable for the element type.
1296                        let result_var = self.fresh_var();
1297                        InferType::Var(result_var)
1298                    }
1299                }
1300            }
1301
1302            // Array index assignment
1303            InstData::IndexSet { base, index, value } => {
1304                let base_info = self.generate(*base, ctx);
1305                let index_info = self.generate(*index, ctx);
1306                // Index must be an unsigned integer type
1307                self.add_constraint(Constraint::is_unsigned(index_info.ty, index_info.span));
1308
1309                let value_info = self.generate(*value, ctx);
1310
1311                // Constrain value type to match array element type
1312                if let InferType::Array { element, .. } = &base_info.ty {
1313                    self.add_constraint(Constraint::equal(
1314                        value_info.ty,
1315                        (**element).clone(),
1316                        value_info.span,
1317                    ));
1318                }
1319
1320                InferType::Concrete(Type::UNIT)
1321            }
1322
1323            // Type declarations don't produce values
1324            InstData::FnDecl { .. }
1325            | InstData::StructDecl { .. }
1326            | InstData::EnumDecl { .. }
1327            | InstData::DropFnDecl { .. }
1328            | InstData::ConstDecl { .. } => InferType::Concrete(Type::UNIT),
1329
1330            // Method call: receiver.method(args)
1331            InstData::MethodCall {
1332                receiver,
1333                method,
1334                args_start,
1335                args_len,
1336            } => {
1337                // Generate type for receiver
1338                let receiver_info = self.generate(*receiver, ctx);
1339                let args = self.rir.get_call_args(*args_start, *args_len);
1340
1341                // Get struct name from receiver type if it's a struct
1342                // If we can't determine the struct type, we still generate constraints
1343                // for the arguments and return a type variable (actual error is in sema)
1344
1345                if let InferType::Concrete(ty) = &receiver_info.ty {
1346                    if let Some(struct_id) = ty.as_struct() {
1347                        // Use StructId directly for method lookup
1348                        let method_key = (struct_id, *method);
1349                        if let Some(method_sig) = self.methods.get(&method_key) {
1350                            // Generate constraints for arguments
1351                            for (arg, param_type) in args.iter().zip(method_sig.param_types.iter())
1352                            {
1353                                let arg_info = self.generate(arg.value, ctx);
1354                                self.add_constraint(Constraint::equal(
1355                                    arg_info.ty,
1356                                    param_type.clone(),
1357                                    arg_info.span,
1358                                ));
1359                            }
1360                            method_sig.return_type.clone()
1361                        } else {
1362                            // Method not found in pre-built context - may be an anonymous
1363                            // struct method registered during comptime evaluation.
1364                            // Use a fresh type variable so inference doesn't poison
1365                            // surrounding expressions with ERROR.
1366                            for arg in args.iter() {
1367                                self.generate(arg.value, ctx);
1368                            }
1369                            InferType::Var(self.fresh_var())
1370                        }
1371                    } else if let Some(enum_id) = ty.as_enum() {
1372                        // Enum receiver - check enum_methods
1373                        let method_key = (enum_id, *method);
1374                        if let Some(method_sig) = self.enum_methods.get(&method_key) {
1375                            for (arg, param_type) in args.iter().zip(method_sig.param_types.iter())
1376                            {
1377                                let arg_info = self.generate(arg.value, ctx);
1378                                self.add_constraint(Constraint::equal(
1379                                    arg_info.ty,
1380                                    param_type.clone(),
1381                                    arg_info.span,
1382                                ));
1383                            }
1384                            method_sig.return_type.clone()
1385                        } else {
1386                            // Enum method not found - use fresh var, sema reports error
1387                            for arg in args.iter() {
1388                                self.generate(arg.value, ctx);
1389                            }
1390                            InferType::Var(self.fresh_var())
1391                        }
1392                    } else {
1393                        // Non-struct/non-enum receiver - sema will report the error
1394                        for arg in args.iter() {
1395                            self.generate(arg.value, ctx);
1396                        }
1397                        InferType::Concrete(Type::ERROR)
1398                    }
1399                } else {
1400                    // Non-concrete receiver type - use fresh var, sema resolves it
1401                    for arg in args.iter() {
1402                        self.generate(arg.value, ctx);
1403                    }
1404                    InferType::Var(self.fresh_var())
1405                }
1406            }
1407
1408            // Associated function call: Type::function(args)
1409            InstData::AssocFnCall {
1410                type_name,
1411                function,
1412                args_start,
1413                args_len,
1414            } => {
1415                let args = self.rir.get_call_args(*args_start, *args_len);
1416                // Get struct ID from type name for method lookup
1417                let struct_id = self.structs.get(type_name).and_then(|ty| ty.as_struct());
1418
1419                if let Some(struct_id) = struct_id {
1420                    let method_key = (struct_id, *function);
1421                    if let Some(method_sig) = self.methods.get(&method_key) {
1422                        // Generate constraints for arguments
1423                        for (arg, param_type) in args.iter().zip(method_sig.param_types.iter()) {
1424                            let arg_info = self.generate(arg.value, ctx);
1425                            self.add_constraint(Constraint::equal(
1426                                arg_info.ty,
1427                                param_type.clone(),
1428                                arg_info.span,
1429                            ));
1430                        }
1431                        method_sig.return_type.clone()
1432                    } else {
1433                        // Method not found - may be an anonymous struct method
1434                        // registered during comptime. Use fresh var to avoid ERROR poison.
1435                        for arg in args.iter() {
1436                            self.generate(arg.value, ctx);
1437                        }
1438                        InferType::Var(self.fresh_var())
1439                    }
1440                } else {
1441                    // Type not found in pre-built context - may be a comptime type var.
1442                    // Use fresh var so inference doesn't poison surrounding expressions.
1443                    for arg in args.iter() {
1444                        self.generate(arg.value, ctx);
1445                    }
1446                    InferType::Var(self.fresh_var())
1447                }
1448            }
1449
1450            // Comptime block: the type depends on whether evaluation succeeds at compile time.
1451            // For type inference, we use a fresh type variable that can unify with
1452            // whatever type is expected from the context (e.g., a let binding's type annotation).
1453            // Similar to integer literals, comptime blocks can adapt to their context.
1454            InstData::Comptime { expr: _ } => {
1455                // Comptime blocks are fully evaluated by the comptime interpreter in sema,
1456                // which handles its own type checking (comptime_str, TypeInfo structs, etc.).
1457                // We don't generate constraints for the inner expression because comptime
1458                // has types (comptime_str, anonymous structs from @typeInfo) that don't
1459                // exist in the regular type system and would cause false unification errors.
1460                // Use a fresh variable so comptime can unify with whatever the context expects.
1461                let var = self.fresh_var();
1462                self.int_literal_vars.push(var);
1463                InferType::Var(var)
1464            }
1465
1466            // Comptime unroll for: the iterable is evaluated at comptime, the body is unrolled.
1467            // Like regular for loops, the result type is unit.
1468            // We must generate constraints for the body so that type inference resolves
1469            // types for runtime expressions inside the loop body (e.g., `total + 1`).
1470            // The binding variable holds a comptime value and is handled by sema, but
1471            // we register it as an integer type variable so VarRef lookups don't fail.
1472            InstData::ComptimeUnrollFor {
1473                binding,
1474                iterable,
1475                body,
1476            } => {
1477                // Generate constraints for the iterable (it's a comptime block)
1478                self.generate(*iterable, ctx);
1479
1480                // Register the binding as a fresh type variable.
1481                // The actual comptime value type is determined by sema, but HM
1482                // inference needs the binding in scope so VarRef doesn't fail.
1483                let binding_ty = {
1484                    let var = self.fresh_var();
1485                    InferType::Var(var)
1486                };
1487                ctx.insert_local(
1488                    *binding,
1489                    LocalVarInfo {
1490                        ty: binding_ty,
1491                        is_mut: false,
1492                        span,
1493                    },
1494                );
1495
1496                // Generate constraints for the body
1497                self.generate(*body, ctx);
1498
1499                InferType::Concrete(Type::UNIT)
1500            }
1501
1502            // Checked block: for type inference purposes, the type is the type of the inner expression
1503            // The actual checking of unchecked operations happens in sema
1504            InstData::Checked { expr } => {
1505                // Generate constraints for the inner expression
1506                let inner_info = self.generate(*expr, ctx);
1507                inner_info.ty
1508            }
1509
1510            // Type constant: a type used as a value (e.g., `i32` in `identity(i32, 42)`)
1511            // This has the special ComptimeType type which indicates it's a type value.
1512            InstData::TypeConst { .. } => InferType::Concrete(Type::COMPTIME_TYPE),
1513
1514            // Anonymous struct type: a struct type used as a comptime value
1515            // This also has the ComptimeType type.
1516            InstData::AnonStructType { .. } => InferType::Concrete(Type::COMPTIME_TYPE),
1517
1518            // Anonymous enum type: an enum type used as a comptime value
1519            // This also has the ComptimeType type.
1520            InstData::AnonEnumType { .. } => InferType::Concrete(Type::COMPTIME_TYPE),
1521        };
1522
1523        // Record the type for this expression
1524        self.record_type(inst_ref, ty.clone());
1525        ExprInfo::new(ty, span)
1526    }
1527
1528    /// Generate constraints for a binary arithmetic operation (+, -, *, /, %).
1529    ///
1530    /// Operands must be numeric (integer or float).
1531    fn generate_binary_arith(
1532        &mut self,
1533        lhs: InstRef,
1534        rhs: InstRef,
1535        ctx: &mut ConstraintContext,
1536    ) -> InferType {
1537        let lhs_info = self.generate(lhs, ctx);
1538        let rhs_info = self.generate(rhs, ctx);
1539
1540        let result_var = self.fresh_var();
1541        let result_ty = InferType::Var(result_var);
1542
1543        self.add_constraint(Constraint::equal(
1544            lhs_info.ty,
1545            result_ty.clone(),
1546            lhs_info.span,
1547        ));
1548        self.add_constraint(Constraint::equal(
1549            rhs_info.ty,
1550            result_ty.clone(),
1551            rhs_info.span,
1552        ));
1553
1554        // Result must be numeric (integer or float)
1555        self.add_constraint(Constraint::is_numeric(result_ty.clone(), lhs_info.span));
1556
1557        result_ty
1558    }
1559
1560    /// Generate constraints for a binary bitwise operation (&, |, ^, <<, >>).
1561    ///
1562    /// Operands must be integers (floats are not allowed).
1563    fn generate_binary_bitwise(
1564        &mut self,
1565        lhs: InstRef,
1566        rhs: InstRef,
1567        ctx: &mut ConstraintContext,
1568    ) -> InferType {
1569        let lhs_info = self.generate(lhs, ctx);
1570        let rhs_info = self.generate(rhs, ctx);
1571
1572        let result_var = self.fresh_var();
1573        let result_ty = InferType::Var(result_var);
1574
1575        self.add_constraint(Constraint::equal(
1576            lhs_info.ty,
1577            result_ty.clone(),
1578            lhs_info.span,
1579        ));
1580        self.add_constraint(Constraint::equal(
1581            rhs_info.ty,
1582            result_ty.clone(),
1583            rhs_info.span,
1584        ));
1585
1586        // Result must be an integer type (no floats)
1587        self.add_constraint(Constraint::is_integer(result_ty.clone(), lhs_info.span));
1588
1589        result_ty
1590    }
1591
1592    /// Get the inferred type for a pattern.
1593    fn pattern_type(&mut self, pattern: &gruel_rir::RirPattern) -> InferType {
1594        match pattern {
1595            gruel_rir::RirPattern::Wildcard(_) => {
1596                // Wildcard matches anything - use a fresh type variable
1597                let var = self.fresh_var();
1598                InferType::Var(var)
1599            }
1600            gruel_rir::RirPattern::Int(_, _) => InferType::IntLiteral,
1601            gruel_rir::RirPattern::Bool(_, _) => InferType::Concrete(Type::BOOL),
1602            gruel_rir::RirPattern::Path { type_name, .. }
1603            | gruel_rir::RirPattern::DataVariant { type_name, .. }
1604            | gruel_rir::RirPattern::StructVariant { type_name, .. } => {
1605                if let Some(&enum_ty) = self.enums.get(type_name) {
1606                    InferType::Concrete(enum_ty)
1607                } else {
1608                    // Enum type not found — may be a comptime type variable
1609                    // (e.g., `let Opt = Option(i32); match x { Opt::Some(v) => ... }`).
1610                    // Use a fresh type variable so arm bodies can still infer types.
1611                    let var = self.fresh_var();
1612                    InferType::Var(var)
1613                }
1614            }
1615        }
1616    }
1617
1618    /// Resolve a type name to an InferType.
1619    ///
1620    /// Handles primitive types, array syntax `[T; N]`, pointer syntax `ptr mut T` / `ptr const T`,
1621    /// and struct/enum types.
1622    fn resolve_type_name(&self, name: &str) -> Option<InferType> {
1623        // Check for array syntax first: [T; N]
1624        if let Some((element_type_str, length)) = parse_array_type_syntax(name) {
1625            // Recursively resolve the element type
1626            let element_ty = self.resolve_type_name(&element_type_str)?;
1627            return Some(InferType::Array {
1628                element: Box::new(element_ty),
1629                length,
1630            });
1631        }
1632
1633        // Check for pointer syntax: ptr mut T / ptr const T
1634        if let Some((pointee_type_str, mutability)) = parse_pointer_type_syntax(name) {
1635            // Recursively resolve the pointee type
1636            let pointee_infer_ty = self.resolve_type_name(&pointee_type_str)?;
1637
1638            // Convert InferType to Type so we can intern the pointer
1639            let pointee_ty = match pointee_infer_ty {
1640                InferType::Concrete(ty) => ty,
1641                // Can't handle non-concrete types in pointer positions during constraint generation
1642                _ => return None,
1643            };
1644
1645            // Intern the pointer type
1646            let ptr_ty = match mutability {
1647                PtrMutability::Mut => {
1648                    let ptr_id = self.type_pool.intern_ptr_mut_from_type(pointee_ty);
1649                    Type::new_ptr_mut(ptr_id)
1650                }
1651                PtrMutability::Const => {
1652                    let ptr_id = self.type_pool.intern_ptr_const_from_type(pointee_ty);
1653                    Type::new_ptr_const(ptr_id)
1654                }
1655            };
1656            return Some(InferType::Concrete(ptr_ty));
1657        }
1658
1659        // Check primitives
1660        let ty = match name {
1661            "i8" => Type::I8,
1662            "i16" => Type::I16,
1663            "i32" => Type::I32,
1664            "i64" => Type::I64,
1665            "u8" => Type::U8,
1666            "u16" => Type::U16,
1667            "u32" => Type::U32,
1668            "u64" => Type::U64,
1669            "isize" => Type::ISIZE,
1670            "usize" => Type::USIZE,
1671            "f16" => Type::F16,
1672            "f32" => Type::F32,
1673            "f64" => Type::F64,
1674            "bool" => Type::BOOL,
1675            "()" => Type::UNIT,
1676            _ => {
1677                // Check for struct types (including builtin String)
1678                if let Some(name_spur) = self.interner.get(name) {
1679                    if let Some(&struct_ty) = self.structs.get(&name_spur) {
1680                        return Some(InferType::Concrete(struct_ty));
1681                    }
1682                    if let Some(&enum_ty) = self.enums.get(&name_spur) {
1683                        return Some(InferType::Concrete(enum_ty));
1684                    }
1685                }
1686                return None;
1687            }
1688        };
1689        Some(InferType::Concrete(ty))
1690    }
1691}
1692
1693#[cfg(test)]
1694mod tests {
1695    use super::*;
1696    use lasso::ThreadedRodeo;
1697
1698    /// Helper to create a minimal RIR, interner, and type pool for testing.
1699    fn make_test_rir_interner_and_type_pool() -> (Rir, ThreadedRodeo, TypeInternPool) {
1700        let rir = Rir::new();
1701        let interner = ThreadedRodeo::new();
1702        let type_pool = TypeInternPool::new();
1703        (rir, interner, type_pool)
1704    }
1705
1706    #[test]
1707    fn test_constraint_generator_int_literal() {
1708        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
1709        let functions = HashMap::new();
1710        let structs = HashMap::new();
1711        let enums = HashMap::new();
1712        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
1713        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
1714
1715        // Add an integer constant to RIR
1716        let inst_ref = rir.add_inst(gruel_rir::Inst {
1717            data: InstData::IntConst(42),
1718            span: Span::new(0, 2),
1719        });
1720
1721        let mut cgen = ConstraintGenerator::new(
1722            &rir,
1723            &interner,
1724            &functions,
1725            &structs,
1726            &enums,
1727            &methods,
1728            &enum_methods,
1729            &type_pool,
1730        );
1731        let params = HashMap::new();
1732        let mut ctx = ConstraintContext::new(&params, Type::I32);
1733
1734        let info = cgen.generate(inst_ref, &mut ctx);
1735
1736        // Integer literals now get a type variable (tracked as int literal var)
1737        assert!(matches!(info.ty, InferType::Var(_)));
1738        // The type variable should be tracked in int_literal_vars
1739        assert_eq!(cgen.int_literal_vars().len(), 1);
1740        // No constraints should be generated for a simple literal
1741        assert_eq!(cgen.constraints().len(), 0);
1742    }
1743
1744    #[test]
1745    fn test_constraint_generator_bool_literal() {
1746        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
1747        let functions = HashMap::new();
1748        let structs = HashMap::new();
1749        let enums = HashMap::new();
1750        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
1751        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
1752
1753        let inst_ref = rir.add_inst(gruel_rir::Inst {
1754            data: InstData::BoolConst(true),
1755            span: Span::new(0, 4),
1756        });
1757
1758        let mut cgen = ConstraintGenerator::new(
1759            &rir,
1760            &interner,
1761            &functions,
1762            &structs,
1763            &enums,
1764            &methods,
1765            &enum_methods,
1766            &type_pool,
1767        );
1768        let params = HashMap::new();
1769        let mut ctx = ConstraintContext::new(&params, Type::BOOL);
1770
1771        let info = cgen.generate(inst_ref, &mut ctx);
1772
1773        assert_eq!(info.ty, InferType::Concrete(Type::BOOL));
1774        assert_eq!(cgen.constraints().len(), 0);
1775    }
1776
1777    #[test]
1778    fn test_constraint_generator_binary_add() {
1779        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
1780        let functions = HashMap::new();
1781        let structs = HashMap::new();
1782        let enums = HashMap::new();
1783        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
1784        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
1785
1786        // Create: 1 + 2
1787        let lhs = rir.add_inst(gruel_rir::Inst {
1788            data: InstData::IntConst(1),
1789            span: Span::new(0, 1),
1790        });
1791        let rhs = rir.add_inst(gruel_rir::Inst {
1792            data: InstData::IntConst(2),
1793            span: Span::new(4, 5),
1794        });
1795        let add = rir.add_inst(gruel_rir::Inst {
1796            data: InstData::Add { lhs, rhs },
1797            span: Span::new(0, 5),
1798        });
1799
1800        let mut cgen = ConstraintGenerator::new(
1801            &rir,
1802            &interner,
1803            &functions,
1804            &structs,
1805            &enums,
1806            &methods,
1807            &enum_methods,
1808            &type_pool,
1809        );
1810        let params = HashMap::new();
1811        let mut ctx = ConstraintContext::new(&params, Type::I32);
1812
1813        let info = cgen.generate(add, &mut ctx);
1814
1815        // Result should be a type variable
1816        assert!(info.ty.is_var());
1817        // Should generate 3 constraints: lhs = result, rhs = result, IsNumeric(result)
1818        assert_eq!(cgen.constraints().len(), 3);
1819        // Verify the third constraint is IsNumeric
1820        match &cgen.constraints()[2] {
1821            Constraint::IsNumeric(_, _) => {}
1822            _ => panic!("Expected IsNumeric constraint for arithmetic result"),
1823        }
1824    }
1825
1826    #[test]
1827    fn test_constraint_generator_comparison() {
1828        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
1829        let functions = HashMap::new();
1830        let structs = HashMap::new();
1831        let enums = HashMap::new();
1832        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
1833        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
1834
1835        // Create: 1 < 2
1836        let lhs = rir.add_inst(gruel_rir::Inst {
1837            data: InstData::IntConst(1),
1838            span: Span::new(0, 1),
1839        });
1840        let rhs = rir.add_inst(gruel_rir::Inst {
1841            data: InstData::IntConst(2),
1842            span: Span::new(4, 5),
1843        });
1844        let lt = rir.add_inst(gruel_rir::Inst {
1845            data: InstData::Lt { lhs, rhs },
1846            span: Span::new(0, 5),
1847        });
1848
1849        let mut cgen = ConstraintGenerator::new(
1850            &rir,
1851            &interner,
1852            &functions,
1853            &structs,
1854            &enums,
1855            &methods,
1856            &enum_methods,
1857            &type_pool,
1858        );
1859        let params = HashMap::new();
1860        let mut ctx = ConstraintContext::new(&params, Type::BOOL);
1861
1862        let info = cgen.generate(lt, &mut ctx);
1863
1864        // Comparisons always return Bool
1865        assert_eq!(info.ty, InferType::Concrete(Type::BOOL));
1866        // Should generate 1 constraint: lhs type = rhs type
1867        assert_eq!(cgen.constraints().len(), 1);
1868    }
1869
1870    #[test]
1871    fn test_constraint_generator_logical_and() {
1872        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
1873        let functions = HashMap::new();
1874        let structs = HashMap::new();
1875        let enums = HashMap::new();
1876        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
1877        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
1878
1879        // Create: true && false
1880        let lhs = rir.add_inst(gruel_rir::Inst {
1881            data: InstData::BoolConst(true),
1882            span: Span::new(0, 4),
1883        });
1884        let rhs = rir.add_inst(gruel_rir::Inst {
1885            data: InstData::BoolConst(false),
1886            span: Span::new(8, 13),
1887        });
1888        let and = rir.add_inst(gruel_rir::Inst {
1889            data: InstData::And { lhs, rhs },
1890            span: Span::new(0, 13),
1891        });
1892
1893        let mut cgen = ConstraintGenerator::new(
1894            &rir,
1895            &interner,
1896            &functions,
1897            &structs,
1898            &enums,
1899            &methods,
1900            &enum_methods,
1901            &type_pool,
1902        );
1903        let params = HashMap::new();
1904        let mut ctx = ConstraintContext::new(&params, Type::BOOL);
1905
1906        let info = cgen.generate(and, &mut ctx);
1907
1908        // Logical operators return Bool
1909        assert_eq!(info.ty, InferType::Concrete(Type::BOOL));
1910        // Should generate 2 constraints: lhs = bool, rhs = bool
1911        assert_eq!(cgen.constraints().len(), 2);
1912    }
1913
1914    #[test]
1915    fn test_constraint_generator_negation() {
1916        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
1917        let functions = HashMap::new();
1918        let structs = HashMap::new();
1919        let enums = HashMap::new();
1920        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
1921        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
1922
1923        // Create: -42
1924        let operand = rir.add_inst(gruel_rir::Inst {
1925            data: InstData::IntConst(42),
1926            span: Span::new(1, 3),
1927        });
1928        let neg = rir.add_inst(gruel_rir::Inst {
1929            data: InstData::Neg { operand },
1930            span: Span::new(0, 3),
1931        });
1932
1933        let mut cgen = ConstraintGenerator::new(
1934            &rir,
1935            &interner,
1936            &functions,
1937            &structs,
1938            &enums,
1939            &methods,
1940            &enum_methods,
1941            &type_pool,
1942        );
1943        let params = HashMap::new();
1944        let mut ctx = ConstraintContext::new(&params, Type::I32);
1945
1946        let info = cgen.generate(neg, &mut ctx);
1947
1948        // Negation preserves the operand type (now a type variable for the int literal)
1949        assert!(matches!(info.ty, InferType::Var(_)));
1950        // Should generate 1 constraint: IsSigned for the result
1951        assert_eq!(cgen.constraints().len(), 1);
1952        // Verify it's an IsSigned constraint
1953        match &cgen.constraints()[0] {
1954            Constraint::IsSigned(_, _) => {}
1955            _ => panic!("Expected IsSigned constraint"),
1956        }
1957    }
1958
1959    #[test]
1960    fn test_constraint_generator_return() {
1961        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
1962        let functions = HashMap::new();
1963        let structs = HashMap::new();
1964        let enums = HashMap::new();
1965        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
1966        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
1967
1968        // Create: return 42
1969        let value = rir.add_inst(gruel_rir::Inst {
1970            data: InstData::IntConst(42),
1971            span: Span::new(7, 9),
1972        });
1973        let ret = rir.add_inst(gruel_rir::Inst {
1974            data: InstData::Ret(Some(value)),
1975            span: Span::new(0, 9),
1976        });
1977
1978        let mut cgen = ConstraintGenerator::new(
1979            &rir,
1980            &interner,
1981            &functions,
1982            &structs,
1983            &enums,
1984            &methods,
1985            &enum_methods,
1986            &type_pool,
1987        );
1988        let params = HashMap::new();
1989        let mut ctx = ConstraintContext::new(&params, Type::I32);
1990
1991        let info = cgen.generate(ret, &mut ctx);
1992
1993        // Return is divergent (Never type)
1994        assert_eq!(info.ty, InferType::Concrete(Type::NEVER));
1995        // Should generate 1 constraint: return value = return type
1996        assert_eq!(cgen.constraints().len(), 1);
1997    }
1998
1999    #[test]
2000    fn test_constraint_generator_if_else() {
2001        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2002        let functions = HashMap::new();
2003        let structs = HashMap::new();
2004        let enums = HashMap::new();
2005        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
2006        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
2007
2008        // Create: if true { 1 } else { 2 }
2009        let cond = rir.add_inst(gruel_rir::Inst {
2010            data: InstData::BoolConst(true),
2011            span: Span::new(3, 7),
2012        });
2013        let then_val = rir.add_inst(gruel_rir::Inst {
2014            data: InstData::IntConst(1),
2015            span: Span::new(10, 11),
2016        });
2017        let else_val = rir.add_inst(gruel_rir::Inst {
2018            data: InstData::IntConst(2),
2019            span: Span::new(20, 21),
2020        });
2021        let branch = rir.add_inst(gruel_rir::Inst {
2022            data: InstData::Branch {
2023                cond,
2024                then_block: then_val,
2025                else_block: Some(else_val),
2026            },
2027            span: Span::new(0, 25),
2028        });
2029
2030        let mut cgen = ConstraintGenerator::new(
2031            &rir,
2032            &interner,
2033            &functions,
2034            &structs,
2035            &enums,
2036            &methods,
2037            &enum_methods,
2038            &type_pool,
2039        );
2040        let params = HashMap::new();
2041        let mut ctx = ConstraintContext::new(&params, Type::I32);
2042
2043        let info = cgen.generate(branch, &mut ctx);
2044
2045        // Result should be a type variable (unified from both branches)
2046        assert!(info.ty.is_var());
2047        // Should generate 3 constraints: cond = bool, then = result, else = result
2048        assert_eq!(cgen.constraints().len(), 3);
2049    }
2050
2051    #[test]
2052    fn test_constraint_generator_while_loop() {
2053        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2054        let functions = HashMap::new();
2055        let structs = HashMap::new();
2056        let enums = HashMap::new();
2057        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
2058        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
2059
2060        // Create: while true { 0 }
2061        let cond = rir.add_inst(gruel_rir::Inst {
2062            data: InstData::BoolConst(true),
2063            span: Span::new(6, 10),
2064        });
2065        let body = rir.add_inst(gruel_rir::Inst {
2066            data: InstData::IntConst(0),
2067            span: Span::new(13, 14),
2068        });
2069        let loop_inst = rir.add_inst(gruel_rir::Inst {
2070            data: InstData::Loop { cond, body },
2071            span: Span::new(0, 15),
2072        });
2073
2074        let mut cgen = ConstraintGenerator::new(
2075            &rir,
2076            &interner,
2077            &functions,
2078            &structs,
2079            &enums,
2080            &methods,
2081            &enum_methods,
2082            &type_pool,
2083        );
2084        let params = HashMap::new();
2085        let mut ctx = ConstraintContext::new(&params, Type::UNIT);
2086
2087        let info = cgen.generate(loop_inst, &mut ctx);
2088
2089        // While loops produce Unit
2090        assert_eq!(info.ty, InferType::Concrete(Type::UNIT));
2091        // Should generate 1 constraint: cond = bool
2092        assert_eq!(cgen.constraints().len(), 1);
2093    }
2094
2095    #[test]
2096    fn test_constraint_context_scope() {
2097        let params = HashMap::new();
2098        let mut ctx = ConstraintContext::new(&params, Type::I32);
2099
2100        // Use an interner to create a symbol
2101        let interner = ThreadedRodeo::new();
2102        let sym = interner.get_or_intern("x");
2103        ctx.insert_local(
2104            sym,
2105            LocalVarInfo {
2106                ty: InferType::Concrete(Type::I32),
2107                is_mut: false,
2108                span: Span::new(0, 1),
2109            },
2110        );
2111
2112        assert!(ctx.locals.contains_key(&sym));
2113
2114        // Push a scope and shadow the variable
2115        ctx.push_scope();
2116        ctx.insert_local(
2117            sym,
2118            LocalVarInfo {
2119                ty: InferType::Concrete(Type::I64),
2120                is_mut: true,
2121                span: Span::new(10, 15),
2122            },
2123        );
2124
2125        // Should see the shadowed version
2126        let local = ctx.locals.get(&sym).unwrap();
2127        assert_eq!(local.ty, InferType::Concrete(Type::I64));
2128        assert!(local.is_mut);
2129
2130        // Pop scope - should restore original
2131        ctx.pop_scope();
2132        let local = ctx.locals.get(&sym).unwrap();
2133        assert_eq!(local.ty, InferType::Concrete(Type::I32));
2134        assert!(!local.is_mut);
2135    }
2136
2137    #[test]
2138    fn test_expr_info_creation() {
2139        let info = ExprInfo::new(InferType::IntLiteral, Span::new(5, 10));
2140        assert!(info.ty.is_int_literal());
2141        assert_eq!(info.span, Span::new(5, 10));
2142    }
2143
2144    /// Helper to create a non-generic FunctionSig for tests
2145    fn make_test_func_sig(param_types: Vec<InferType>, return_type: InferType) -> FunctionSig {
2146        let num_params = param_types.len();
2147        FunctionSig {
2148            param_types,
2149            return_type,
2150            is_generic: false,
2151            param_modes: vec![gruel_rir::RirParamMode::Normal; num_params],
2152            param_comptime: vec![false; num_params],
2153            param_names: vec![],
2154            return_type_sym: lasso::Spur::default(),
2155        }
2156    }
2157
2158    #[test]
2159    fn test_function_sig() {
2160        let sig = make_test_func_sig(
2161            vec![
2162                InferType::Concrete(Type::I32),
2163                InferType::Concrete(Type::BOOL),
2164            ],
2165            InferType::Concrete(Type::I64),
2166        );
2167        assert_eq!(sig.param_types.len(), 2);
2168        assert_eq!(sig.return_type, InferType::Concrete(Type::I64));
2169    }
2170
2171    #[test]
2172    fn test_constraint_generator_infinite_loop() {
2173        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2174        let functions = HashMap::new();
2175        let structs = HashMap::new();
2176        let enums = HashMap::new();
2177        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
2178        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
2179
2180        // Create: loop { 0 }
2181        let body = rir.add_inst(gruel_rir::Inst {
2182            data: InstData::IntConst(0),
2183            span: Span::new(6, 7),
2184        });
2185        let loop_inst = rir.add_inst(gruel_rir::Inst {
2186            data: InstData::InfiniteLoop { body },
2187            span: Span::new(0, 10),
2188        });
2189
2190        let mut cgen = ConstraintGenerator::new(
2191            &rir,
2192            &interner,
2193            &functions,
2194            &structs,
2195            &enums,
2196            &methods,
2197            &enum_methods,
2198            &type_pool,
2199        );
2200        let params = HashMap::new();
2201        let mut ctx = ConstraintContext::new(&params, Type::UNIT);
2202
2203        let info = cgen.generate(loop_inst, &mut ctx);
2204
2205        // Infinite loop produces Never (diverges)
2206        assert_eq!(info.ty, InferType::Concrete(Type::NEVER));
2207        // No constraints for infinite loop itself
2208        assert_eq!(cgen.constraints().len(), 0);
2209    }
2210
2211    #[test]
2212    fn test_constraint_generator_break_continue() {
2213        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2214        let functions = HashMap::new();
2215        let structs = HashMap::new();
2216        let enums = HashMap::new();
2217        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
2218        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
2219
2220        let break_inst = rir.add_inst(gruel_rir::Inst {
2221            data: InstData::Break,
2222            span: Span::new(0, 5),
2223        });
2224
2225        let mut cgen = ConstraintGenerator::new(
2226            &rir,
2227            &interner,
2228            &functions,
2229            &structs,
2230            &enums,
2231            &methods,
2232            &enum_methods,
2233            &type_pool,
2234        );
2235        let params = HashMap::new();
2236        let mut ctx = ConstraintContext::new(&params, Type::UNIT);
2237
2238        let info = cgen.generate(break_inst, &mut ctx);
2239
2240        // Break diverges
2241        assert_eq!(info.ty, InferType::Concrete(Type::NEVER));
2242        assert_eq!(cgen.constraints().len(), 0);
2243    }
2244
2245    #[test]
2246    fn test_constraint_generator_index_get() {
2247        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2248        let functions = HashMap::new();
2249        let structs = HashMap::new();
2250        let enums = HashMap::new();
2251        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
2252        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
2253
2254        // Create: arr[0]
2255        let base = rir.add_inst(gruel_rir::Inst {
2256            data: InstData::IntConst(0), // Placeholder for array
2257            span: Span::new(0, 3),
2258        });
2259        let index = rir.add_inst(gruel_rir::Inst {
2260            data: InstData::IntConst(0),
2261            span: Span::new(4, 5),
2262        });
2263        let index_get = rir.add_inst(gruel_rir::Inst {
2264            data: InstData::IndexGet { base, index },
2265            span: Span::new(0, 6),
2266        });
2267
2268        let mut cgen = ConstraintGenerator::new(
2269            &rir,
2270            &interner,
2271            &functions,
2272            &structs,
2273            &enums,
2274            &methods,
2275            &enum_methods,
2276            &type_pool,
2277        );
2278        let params = HashMap::new();
2279        let mut ctx = ConstraintContext::new(&params, Type::I32);
2280
2281        let info = cgen.generate(index_get, &mut ctx);
2282
2283        // Result is a type variable (element type unknown)
2284        assert!(info.ty.is_var());
2285        // Should generate 1 constraint: index must be unsigned
2286        assert_eq!(cgen.constraints().len(), 1);
2287        match &cgen.constraints()[0] {
2288            Constraint::IsUnsigned(_, _) => {}
2289            _ => panic!("Expected IsUnsigned constraint for index"),
2290        }
2291    }
2292
2293    #[test]
2294    fn test_constraint_generator_index_set() {
2295        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2296        let functions = HashMap::new();
2297        let structs = HashMap::new();
2298        let enums = HashMap::new();
2299        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
2300        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
2301
2302        // Create: arr[0] = 42
2303        let base = rir.add_inst(gruel_rir::Inst {
2304            data: InstData::IntConst(0), // Placeholder for array
2305            span: Span::new(0, 3),
2306        });
2307        let index = rir.add_inst(gruel_rir::Inst {
2308            data: InstData::IntConst(0),
2309            span: Span::new(4, 5),
2310        });
2311        let value = rir.add_inst(gruel_rir::Inst {
2312            data: InstData::IntConst(42),
2313            span: Span::new(9, 11),
2314        });
2315        let index_set = rir.add_inst(gruel_rir::Inst {
2316            data: InstData::IndexSet { base, index, value },
2317            span: Span::new(0, 11),
2318        });
2319
2320        let mut cgen = ConstraintGenerator::new(
2321            &rir,
2322            &interner,
2323            &functions,
2324            &structs,
2325            &enums,
2326            &methods,
2327            &enum_methods,
2328            &type_pool,
2329        );
2330        let params = HashMap::new();
2331        let mut ctx = ConstraintContext::new(&params, Type::UNIT);
2332
2333        let info = cgen.generate(index_set, &mut ctx);
2334
2335        // Index assignment produces Unit
2336        assert_eq!(info.ty, InferType::Concrete(Type::UNIT));
2337        // Should generate 1 constraint: index must be unsigned
2338        assert_eq!(cgen.constraints().len(), 1);
2339        match &cgen.constraints()[0] {
2340            Constraint::IsUnsigned(_, _) => {}
2341            _ => panic!("Expected IsUnsigned constraint for index"),
2342        }
2343    }
2344
2345    #[test]
2346    fn test_constraint_generator_empty_block() {
2347        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2348        let functions = HashMap::new();
2349        let structs = HashMap::new();
2350        let enums = HashMap::new();
2351        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
2352        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
2353
2354        // Create: { } (empty block)
2355        let block = rir.add_inst(gruel_rir::Inst {
2356            data: InstData::Block {
2357                extra_start: 0,
2358                len: 0,
2359            },
2360            span: Span::new(0, 2),
2361        });
2362
2363        let mut cgen = ConstraintGenerator::new(
2364            &rir,
2365            &interner,
2366            &functions,
2367            &structs,
2368            &enums,
2369            &methods,
2370            &enum_methods,
2371            &type_pool,
2372        );
2373        let params = HashMap::new();
2374        let mut ctx = ConstraintContext::new(&params, Type::UNIT);
2375
2376        let info = cgen.generate(block, &mut ctx);
2377
2378        // Empty block produces Unit
2379        assert_eq!(info.ty, InferType::Concrete(Type::UNIT));
2380        assert_eq!(cgen.constraints().len(), 0);
2381    }
2382
2383    #[test]
2384    fn test_constraint_generator_bitwise_not() {
2385        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2386        let functions = HashMap::new();
2387        let structs = HashMap::new();
2388        let enums = HashMap::new();
2389        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
2390        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
2391
2392        // Create: !42 (bitwise NOT)
2393        let operand = rir.add_inst(gruel_rir::Inst {
2394            data: InstData::IntConst(42),
2395            span: Span::new(1, 3),
2396        });
2397        let bitnot = rir.add_inst(gruel_rir::Inst {
2398            data: InstData::BitNot { operand },
2399            span: Span::new(0, 3),
2400        });
2401
2402        let mut cgen = ConstraintGenerator::new(
2403            &rir,
2404            &interner,
2405            &functions,
2406            &structs,
2407            &enums,
2408            &methods,
2409            &enum_methods,
2410            &type_pool,
2411        );
2412        let params = HashMap::new();
2413        let mut ctx = ConstraintContext::new(&params, Type::I32);
2414
2415        let info = cgen.generate(bitnot, &mut ctx);
2416
2417        // Bitwise NOT preserves the operand type (now a type variable for int literal)
2418        assert!(matches!(info.ty, InferType::Var(_)));
2419        // Should generate 1 constraint: IsInteger for the result
2420        assert_eq!(cgen.constraints().len(), 1);
2421        match &cgen.constraints()[0] {
2422            Constraint::IsInteger(_, _) => {}
2423            _ => panic!("Expected IsInteger constraint"),
2424        }
2425    }
2426
2427    #[test]
2428    fn test_constraint_generator_function_call_arg_count_mismatch() {
2429        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2430        let mut functions = HashMap::new();
2431        let structs = HashMap::new();
2432        let enums = HashMap::new();
2433        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
2434        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
2435
2436        // Register a function that takes 2 parameters
2437        let func_name = interner.get_or_intern("foo");
2438        functions.insert(
2439            func_name,
2440            make_test_func_sig(
2441                vec![
2442                    InferType::Concrete(Type::I32),
2443                    InferType::Concrete(Type::I32),
2444                ],
2445                InferType::Concrete(Type::BOOL),
2446            ),
2447        );
2448
2449        // Create a call with only 1 argument (mismatch)
2450        let arg = rir.add_inst(gruel_rir::Inst {
2451            data: InstData::IntConst(42),
2452            span: Span::new(4, 6),
2453        });
2454        let (args_start, args_len) = rir.add_call_args(&[gruel_rir::RirCallArg {
2455            value: arg,
2456            mode: gruel_rir::RirArgMode::Normal,
2457        }]);
2458        let call = rir.add_inst(gruel_rir::Inst {
2459            data: InstData::Call {
2460                name: func_name,
2461                args_start,
2462                args_len,
2463            },
2464            span: Span::new(0, 7),
2465        });
2466
2467        let mut cgen = ConstraintGenerator::new(
2468            &rir,
2469            &interner,
2470            &functions,
2471            &structs,
2472            &enums,
2473            &methods,
2474            &enum_methods,
2475            &type_pool,
2476        );
2477        let params = HashMap::new();
2478        let mut ctx = ConstraintContext::new(&params, Type::BOOL);
2479
2480        let info = cgen.generate(call, &mut ctx);
2481
2482        // Should still return the declared return type
2483        assert_eq!(info.ty, InferType::Concrete(Type::BOOL));
2484        // No constraints generated when arg count mismatches (error will be in sema)
2485        assert_eq!(cgen.constraints().len(), 0);
2486    }
2487
2488    #[test]
2489    fn test_constraint_generator_unknown_function() {
2490        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2491        let functions = HashMap::new(); // Empty - no functions registered
2492        let structs = HashMap::new();
2493        let enums = HashMap::new();
2494        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
2495        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
2496
2497        // Create a call to an unknown function
2498        let unknown_func = interner.get_or_intern("unknown");
2499        let arg = rir.add_inst(gruel_rir::Inst {
2500            data: InstData::IntConst(42),
2501            span: Span::new(8, 10),
2502        });
2503        let (args_start, args_len) = rir.add_call_args(&[gruel_rir::RirCallArg {
2504            value: arg,
2505            mode: gruel_rir::RirArgMode::Normal,
2506        }]);
2507        let call = rir.add_inst(gruel_rir::Inst {
2508            data: InstData::Call {
2509                name: unknown_func,
2510                args_start,
2511                args_len,
2512            },
2513            span: Span::new(0, 11),
2514        });
2515
2516        let mut cgen = ConstraintGenerator::new(
2517            &rir,
2518            &interner,
2519            &functions,
2520            &structs,
2521            &enums,
2522            &methods,
2523            &enum_methods,
2524            &type_pool,
2525        );
2526        let params = HashMap::new();
2527        let mut ctx = ConstraintContext::new(&params, Type::I32);
2528
2529        let info = cgen.generate(call, &mut ctx);
2530
2531        // Unknown function returns Error type
2532        assert_eq!(info.ty, InferType::Concrete(Type::ERROR));
2533        // Arguments should still be processed (but no constraints generated for them)
2534        assert_eq!(cgen.constraints().len(), 0);
2535    }
2536
2537    #[test]
2538    fn test_constraint_generator_match_multiple_arms() {
2539        let (mut rir, interner, type_pool) = make_test_rir_interner_and_type_pool();
2540        let functions = HashMap::new();
2541        let structs = HashMap::new();
2542        let enums = HashMap::new();
2543        let methods: HashMap<(StructId, Spur), MethodSig> = HashMap::new();
2544        let enum_methods: HashMap<(EnumId, Spur), MethodSig> = HashMap::new();
2545
2546        // Create: match x { 1 => 10, 2 => 20, _ => 30 }
2547        let scrutinee = rir.add_inst(gruel_rir::Inst {
2548            data: InstData::IntConst(5),
2549            span: Span::new(6, 7),
2550        });
2551
2552        // Arm 1: 1 => 10
2553        let body1 = rir.add_inst(gruel_rir::Inst {
2554            data: InstData::IntConst(10),
2555            span: Span::new(15, 17),
2556        });
2557        let pattern1 = gruel_rir::RirPattern::Int(1, Span::new(10, 11));
2558
2559        // Arm 2: 2 => 20
2560        let body2 = rir.add_inst(gruel_rir::Inst {
2561            data: InstData::IntConst(20),
2562            span: Span::new(25, 27),
2563        });
2564        let pattern2 = gruel_rir::RirPattern::Int(2, Span::new(20, 21));
2565
2566        // Arm 3: _ => 30
2567        let body3 = rir.add_inst(gruel_rir::Inst {
2568            data: InstData::IntConst(30),
2569            span: Span::new(35, 37),
2570        });
2571        let pattern3 = gruel_rir::RirPattern::Wildcard(Span::new(30, 31));
2572
2573        let arms = vec![(pattern1, body1), (pattern2, body2), (pattern3, body3)];
2574        let (arms_start, arms_len) = rir.add_match_arms(&arms);
2575        let match_inst = rir.add_inst(gruel_rir::Inst {
2576            data: InstData::Match {
2577                scrutinee,
2578                arms_start,
2579                arms_len,
2580            },
2581            span: Span::new(0, 40),
2582        });
2583
2584        let mut cgen = ConstraintGenerator::new(
2585            &rir,
2586            &interner,
2587            &functions,
2588            &structs,
2589            &enums,
2590            &methods,
2591            &enum_methods,
2592            &type_pool,
2593        );
2594        let params = HashMap::new();
2595        let mut ctx = ConstraintContext::new(&params, Type::I32);
2596
2597        let info = cgen.generate(match_inst, &mut ctx);
2598
2599        // Result should be a type variable (unified from all arm bodies)
2600        assert!(info.ty.is_var());
2601
2602        // Should generate 6 constraints:
2603        // - 3 for pattern types matching scrutinee type (each arm)
2604        // - 3 for body types matching result type (each arm)
2605        assert_eq!(cgen.constraints().len(), 6);
2606
2607        // Verify all constraints are Equal constraints
2608        for constraint in cgen.constraints() {
2609            match constraint {
2610                Constraint::Equal(_, _, _) => {}
2611                _ => panic!("Expected Equal constraint in match"),
2612            }
2613        }
2614    }
2615}