gruel_air/inference/
unify.rs

1//! Unification engine for Hindley-Milner type inference.
2//!
3//! This module provides the core unification algorithm:
4//! - [`UnifyResult`] - Result of a unification attempt
5//! - [`UnificationError`] - Error with span for reporting
6//! - [`Unifier`] - The unification engine
7
8use super::constraint::{Constraint, Substitution};
9use super::types::{InferType, TypeVarId};
10use crate::Type;
11use gruel_span::Span;
12
13/// Result of unifying two types.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum UnifyResult {
16    /// Unification succeeded.
17    Ok,
18
19    /// Types are incompatible.
20    TypeMismatch {
21        expected: InferType,
22        found: InferType,
23    },
24
25    /// Integer literal cannot unify with non-integer type.
26    IntLiteralNonInteger { found: Type },
27
28    /// Occurs check failed (would create infinite type).
29    OccursCheck { var: TypeVarId, ty: InferType },
30
31    /// Type must be signed but is unsigned.
32    NotSigned { ty: Type },
33
34    /// Type must be an integer but is not.
35    NotInteger { ty: Type },
36
37    /// Type must be unsigned but is signed.
38    NotUnsigned { ty: Type },
39
40    /// Type must be numeric (integer or float) but is not.
41    NotNumeric { ty: Type },
42
43    /// Array lengths don't match.
44    ArrayLengthMismatch { expected: u64, found: u64 },
45}
46
47impl UnifyResult {
48    /// Check if unification succeeded.
49    pub fn is_ok(&self) -> bool {
50        matches!(self, UnifyResult::Ok)
51    }
52}
53
54/// An error that occurred during unification.
55///
56/// Captures the error details along with the span for error reporting.
57#[derive(Debug, Clone)]
58pub struct UnificationError {
59    /// The type of error that occurred.
60    pub kind: UnifyResult,
61    /// The source location where the error occurred.
62    pub span: Span,
63}
64
65impl UnificationError {
66    /// Create a new unification error.
67    pub fn new(kind: UnifyResult, span: Span) -> Self {
68        Self { kind, span }
69    }
70
71    /// Get a human-readable error message.
72    pub fn message(&self) -> String {
73        match &self.kind {
74            UnifyResult::Ok => unreachable!("UnificationError should never contain Ok"),
75            UnifyResult::TypeMismatch { expected, found } => {
76                format!("type mismatch: expected {expected}, found {found}")
77            }
78            UnifyResult::IntLiteralNonInteger { found } => {
79                format!("integer literal cannot be used as {found}")
80            }
81            UnifyResult::OccursCheck { var, ty } => {
82                format!("infinite type: {var} cannot unify with {ty}")
83            }
84            UnifyResult::NotSigned { ty } => {
85                format!("cannot negate unsigned type {ty}")
86            }
87            UnifyResult::NotInteger { ty } => {
88                format!("expected integer type, found {ty}")
89            }
90            UnifyResult::NotUnsigned { ty } => {
91                format!("array index must be unsigned integer type, found {ty}")
92            }
93            UnifyResult::NotNumeric { ty } => {
94                format!("expected numeric type, found {ty}")
95            }
96            UnifyResult::ArrayLengthMismatch { expected, found } => {
97                format!("array length mismatch: expected {expected}, found {found}")
98            }
99        }
100    }
101}
102
103/// Unification engine for type inference.
104///
105/// The unifier processes constraints and builds a substitution mapping
106/// type variables to their resolved types.
107#[derive(Debug)]
108pub struct Unifier {
109    /// The current substitution being built.
110    pub substitution: Substitution,
111}
112
113impl Default for Unifier {
114    fn default() -> Self {
115        Self::new()
116    }
117}
118
119impl Unifier {
120    /// Create a new unifier with an empty substitution.
121    pub fn new() -> Self {
122        Unifier {
123            substitution: Substitution::new(),
124        }
125    }
126
127    /// Create a new unifier with a pre-sized substitution.
128    ///
129    /// Use this when you know how many type variables will be created
130    /// (e.g., from `TypeVarAllocator::count()`).
131    pub fn with_capacity(type_var_count: u32) -> Self {
132        Unifier {
133            substitution: Substitution::with_capacity(type_var_count as usize),
134        }
135    }
136
137    /// Unify two types.
138    ///
139    /// This is the core of Algorithm W. It updates the substitution
140    /// to make the two types equal, or returns an error if they cannot
141    /// be unified.
142    ///
143    /// Unification rules:
144    /// 1. `Concrete(T) = Concrete(T)` → succeeds if types are equal
145    /// 2. `Var(α) = τ` → binds α to τ (after occurs check)
146    /// 3. `τ = Var(α)` → binds α to τ (symmetric)
147    /// 4. `IntLiteral = Concrete(integer)` → succeeds (literal takes integer type)
148    /// 5. `IntLiteral = IntLiteral` → succeeds (both stay as IntLiteral)
149    /// 6. `IntLiteral = Concrete(non-integer)` → fails
150    /// 7. `Array(T1, N) = Array(T2, N)` → succeeds if T1 unifies with T2
151    /// 8. `Array(T1, N1) = Array(T2, N2)` where N1 ≠ N2 → fails
152    pub fn unify(&mut self, lhs: &InferType, rhs: &InferType) -> UnifyResult {
153        // Apply current substitution to get most specific types
154        let lhs_resolved = self.substitution.apply(lhs);
155        let rhs_resolved = self.substitution.apply(rhs);
156
157        match (&lhs_resolved, &rhs_resolved) {
158            // Same concrete types unify
159            (InferType::Concrete(t1), InferType::Concrete(t2)) => {
160                if t1 == t2 || t1.can_coerce_to(t2) || t2.can_coerce_to(t1) {
161                    UnifyResult::Ok
162                } else {
163                    UnifyResult::TypeMismatch {
164                        expected: lhs_resolved,
165                        found: rhs_resolved,
166                    }
167                }
168            }
169
170            // Variable on left: bind it to right (if occurs check passes)
171            (InferType::Var(var), _) => self.bind(*var, &rhs_resolved),
172
173            // Variable on right: bind it to left
174            (_, InferType::Var(var)) => self.bind(*var, &lhs_resolved),
175
176            // IntLiteral with concrete type
177            (InferType::IntLiteral, InferType::Concrete(ty))
178            | (InferType::Concrete(ty), InferType::IntLiteral) => {
179                if ty.is_integer() {
180                    // IntLiteral can become any integer type.
181                    // If the original type was a variable bound to IntLiteral,
182                    // rebind it to the concrete type.
183                    self.rebind_int_literal_to_concrete(lhs, ty);
184                    self.rebind_int_literal_to_concrete(rhs, ty);
185                    UnifyResult::Ok
186                } else if ty.is_error() {
187                    // Error type propagates
188                    UnifyResult::Ok
189                } else {
190                    UnifyResult::IntLiteralNonInteger { found: *ty }
191                }
192            }
193
194            // FloatLiteral with concrete type
195            (InferType::FloatLiteral, InferType::Concrete(ty))
196            | (InferType::Concrete(ty), InferType::FloatLiteral) => {
197                if ty.is_float() {
198                    self.rebind_float_literal_to_concrete(lhs, ty);
199                    self.rebind_float_literal_to_concrete(rhs, ty);
200                    UnifyResult::Ok
201                } else if ty.is_error() {
202                    UnifyResult::Ok
203                } else {
204                    UnifyResult::TypeMismatch {
205                        expected: lhs_resolved,
206                        found: rhs_resolved,
207                    }
208                }
209            }
210
211            // Two IntLiterals unify (both remain as IntLiteral)
212            (InferType::IntLiteral, InferType::IntLiteral) => UnifyResult::Ok,
213
214            // Two FloatLiterals unify (both remain as FloatLiteral)
215            (InferType::FloatLiteral, InferType::FloatLiteral) => UnifyResult::Ok,
216
217            // IntLiteral and FloatLiteral are incompatible
218            (InferType::IntLiteral, InferType::FloatLiteral)
219            | (InferType::FloatLiteral, InferType::IntLiteral) => UnifyResult::TypeMismatch {
220                expected: lhs_resolved,
221                found: rhs_resolved,
222            },
223
224            // Two arrays: lengths must match, element types must unify
225            (
226                InferType::Array {
227                    element: elem1,
228                    length: len1,
229                },
230                InferType::Array {
231                    element: elem2,
232                    length: len2,
233                },
234            ) => {
235                if len1 != len2 {
236                    UnifyResult::ArrayLengthMismatch {
237                        expected: *len1,
238                        found: *len2,
239                    }
240                } else {
241                    // Recursively unify element types
242                    self.unify(elem1, elem2)
243                }
244            }
245
246            // Array with non-array: type mismatch
247            // Note: This also handles Array with IntLiteral since the IntLiteral
248            // cases with Concrete and IntLiteral are already handled above.
249            (InferType::Array { .. }, _) | (_, InferType::Array { .. }) => {
250                UnifyResult::TypeMismatch {
251                    expected: lhs_resolved,
252                    found: rhs_resolved,
253                }
254            }
255        }
256    }
257
258    /// If the original type was a variable bound to IntLiteral,
259    /// rebind it to the concrete integer type.
260    fn rebind_int_literal_to_concrete(&mut self, original: &InferType, concrete_ty: &Type) {
261        if let InferType::Var(var) = original {
262            // Check if this variable is directly bound to IntLiteral
263            if let Some(bound) = self.substitution.get(*var)
264                && bound.is_int_literal()
265            {
266                self.substitution
267                    .insert(*var, InferType::Concrete(*concrete_ty));
268            }
269        }
270    }
271
272    fn rebind_float_literal_to_concrete(&mut self, original: &InferType, concrete_ty: &Type) {
273        if let InferType::Var(var) = original
274            && let Some(bound) = self.substitution.get(*var)
275            && matches!(bound, InferType::FloatLiteral)
276        {
277            self.substitution
278                .insert(*var, InferType::Concrete(*concrete_ty));
279        }
280    }
281
282    /// Bind a type variable to a type.
283    ///
284    /// Performs the occurs check to prevent infinite types.
285    fn bind(&mut self, var: TypeVarId, ty: &InferType) -> UnifyResult {
286        // If binding to itself, it's a no-op
287        if let InferType::Var(id) = ty
288            && *id == var
289        {
290            return UnifyResult::Ok;
291        }
292
293        // Occurs check: prevent infinite types
294        if self.substitution.occurs_in(var, ty) {
295            return UnifyResult::OccursCheck {
296                var,
297                ty: ty.clone(),
298            };
299        }
300
301        self.substitution.insert(var, ty.clone());
302        UnifyResult::Ok
303    }
304
305    /// Check that a type is signed.
306    ///
307    /// Returns an error if the type is a concrete unsigned integer.
308    /// For type variables and IntLiterals, this check is deferred
309    /// (the constraint would need to be stored and checked later).
310    pub fn check_signed(&self, ty: &InferType) -> UnifyResult {
311        let ty = self.substitution.apply(ty);
312        match &ty {
313            InferType::Concrete(concrete) => {
314                if concrete.is_signed() || concrete.is_error() || concrete.is_never() {
315                    UnifyResult::Ok
316                } else if concrete.is_unsigned() {
317                    UnifyResult::NotSigned { ty: *concrete }
318                } else {
319                    // Non-integer type - this will be caught elsewhere
320                    UnifyResult::Ok
321                }
322            }
323            // For variables and IntLiteral, assume OK for now
324            // (IntLiteral defaults to i32 which is signed)
325            InferType::Var(_) | InferType::IntLiteral => UnifyResult::Ok,
326            // FloatLiteral - not an integer, but error will be caught elsewhere
327            InferType::FloatLiteral => UnifyResult::Ok,
328            // Arrays are not signed - error will be caught elsewhere
329            InferType::Array { .. } => UnifyResult::Ok,
330        }
331    }
332
333    /// Check that a type is an integer (signed or unsigned).
334    ///
335    /// Returns an error if the type is a concrete non-integer type.
336    /// For type variables and IntLiterals, the check passes.
337    pub fn check_integer(&self, ty: &InferType) -> UnifyResult {
338        let ty = self.substitution.apply(ty);
339        match &ty {
340            InferType::Concrete(concrete) => {
341                if concrete.is_integer() || concrete.is_error() || concrete.is_never() {
342                    UnifyResult::Ok
343                } else {
344                    UnifyResult::NotInteger { ty: *concrete }
345                }
346            }
347            // Type variables and IntLiteral are OK - they will be resolved to integers
348            InferType::Var(_) | InferType::IntLiteral => UnifyResult::Ok,
349            // FloatLiteral is not an integer
350            InferType::FloatLiteral => UnifyResult::NotInteger { ty: Type::F64 },
351            // Arrays are not integers - return error
352            InferType::Array { .. } => UnifyResult::NotInteger { ty: Type::ERROR },
353        }
354    }
355
356    /// Check that a type is numeric (integer or float).
357    ///
358    /// Returns an error if the type is a concrete non-numeric type (e.g., bool).
359    /// For type variables, IntLiteral, and FloatLiteral, the check passes.
360    pub fn check_numeric(&self, ty: &InferType) -> UnifyResult {
361        let ty = self.substitution.apply(ty);
362        match &ty {
363            InferType::Concrete(concrete) => {
364                if concrete.is_numeric() || concrete.is_error() || concrete.is_never() {
365                    UnifyResult::Ok
366                } else {
367                    UnifyResult::NotNumeric { ty: *concrete }
368                }
369            }
370            // Type variables, IntLiteral, and FloatLiteral are OK
371            InferType::Var(_) | InferType::IntLiteral | InferType::FloatLiteral => UnifyResult::Ok,
372            // Arrays are not numeric
373            InferType::Array { .. } => UnifyResult::NotNumeric { ty: Type::ERROR },
374        }
375    }
376
377    /// Check that a type is an unsigned integer.
378    ///
379    /// Returns an error if the type is a concrete signed integer or non-integer type.
380    /// For type variables, the check is deferred.
381    /// For IntLiteral, we allow it and it will be bound to u64 (the default unsigned type).
382    pub fn check_unsigned(&self, ty: &InferType) -> UnifyResult {
383        let ty = self.substitution.apply(ty);
384        match &ty {
385            InferType::Concrete(concrete) => {
386                if concrete.is_unsigned() || concrete.is_error() || concrete.is_never() {
387                    UnifyResult::Ok
388                } else if concrete.is_signed() {
389                    UnifyResult::NotUnsigned { ty: *concrete }
390                } else {
391                    // Non-integer type - report as not unsigned
392                    UnifyResult::NotUnsigned { ty: *concrete }
393                }
394            }
395            // Type variables - defer check (will be validated in sema)
396            InferType::Var(_) => UnifyResult::Ok,
397            // IntLiteral can be used as unsigned - it will be inferred to u64
398            InferType::IntLiteral => UnifyResult::Ok,
399            // FloatLiteral is not unsigned
400            InferType::FloatLiteral => UnifyResult::NotUnsigned { ty: Type::F64 },
401            // Arrays are not unsigned integers
402            InferType::Array { .. } => UnifyResult::NotUnsigned { ty: Type::ERROR },
403        }
404    }
405
406    /// Solve a list of constraints, collecting any errors.
407    ///
408    /// This is the main entry point for Algorithm W. It processes each constraint
409    /// in order, updates the substitution, and collects errors for reporting.
410    ///
411    /// On error, the unifier continues processing remaining constraints to catch
412    /// as many errors as possible in one pass. Type variables involved in errors
413    /// are bound to `Type::ERROR` for recovery.
414    ///
415    /// Returns a list of errors (empty if all constraints were satisfied).
416    pub fn solve_constraints(&mut self, constraints: &[Constraint]) -> Vec<UnificationError> {
417        let mut errors = Vec::new();
418
419        for constraint in constraints {
420            let result = match constraint {
421                Constraint::Equal(lhs, rhs, span) => {
422                    let result = self.unify(lhs, rhs);
423                    if !result.is_ok() {
424                        // On error, try to bind any unbound type variables to Error
425                        // for recovery
426                        self.recover_from_error(lhs, rhs);
427                        errors.push(UnificationError::new(result, *span));
428                    }
429                    continue;
430                }
431                Constraint::IsSigned(ty, span) => {
432                    let result = self.check_signed(ty);
433                    (result, *span)
434                }
435                Constraint::IsInteger(ty, span) => {
436                    let result = self.check_integer(ty);
437                    (result, *span)
438                }
439                Constraint::IsUnsigned(ty, span) => {
440                    // Special handling: if the type is an unbound variable or IntLiteral,
441                    // bind it to u64. This handles integer literals used as array indices.
442                    let applied = self.substitution.apply(ty);
443                    match &applied {
444                        InferType::IntLiteral => {
445                            // IntLiteral bound through a chain - bind the variable to u64
446                            if let InferType::Var(var) = ty {
447                                self.substitution
448                                    .insert(*var, InferType::Concrete(Type::U64));
449                            }
450                        }
451                        InferType::Var(var) => {
452                            // Unbound variable - bind it to u64
453                            // This happens for integer literal variables that haven't
454                            // been constrained yet.
455                            self.substitution
456                                .insert(*var, InferType::Concrete(Type::U64));
457                        }
458                        _ => {}
459                    }
460                    let result = self.check_unsigned(ty);
461                    (result, *span)
462                }
463                Constraint::IsNumeric(ty, span) => {
464                    let result = self.check_numeric(ty);
465                    (result, *span)
466                }
467            };
468
469            if !result.0.is_ok() {
470                errors.push(UnificationError::new(result.0, result.1));
471            }
472        }
473
474        errors
475    }
476
477    /// Recover from a unification error by binding unbound type variables to Error.
478    ///
479    /// This allows type checking to continue after an error, catching more
480    /// errors in a single pass.
481    fn recover_from_error(&mut self, lhs: &InferType, rhs: &InferType) {
482        // Apply substitution first to find any unbound variables
483        let lhs_applied = self.substitution.apply(lhs);
484        let rhs_applied = self.substitution.apply(rhs);
485
486        // Bind any unbound type variables to Error for recovery
487        if let InferType::Var(var) = lhs_applied {
488            self.substitution
489                .insert(var, InferType::Concrete(Type::ERROR));
490        }
491        if let InferType::Var(var) = rhs_applied {
492            self.substitution
493                .insert(var, InferType::Concrete(Type::ERROR));
494        }
495    }
496
497    /// Default all IntLiteral types bound to type variables to i32.
498    ///
499    /// Called at the end of unification for a function to resolve
500    /// any unconstrained integer literals. This processes all type variables
501    /// in the substitution that are bound to IntLiteral.
502    /// Default unconstrained integer literal type variables to i32.
503    ///
504    /// Takes the list of type variables that were allocated for integer literals
505    /// during constraint generation. Any that remain unbound (not constrained to
506    /// a specific integer type) are defaulted to i32.
507    pub fn default_int_literal_vars(&mut self, int_literal_vars: &[TypeVarId]) {
508        for &var in int_literal_vars {
509            // Check if this variable is already bound to a concrete type
510            let resolved = self.substitution.apply(&InferType::Var(var));
511            if let InferType::Var(_) = resolved {
512                // Still unbound - default to i32
513                self.substitution
514                    .insert(var, InferType::Concrete(Type::I32));
515            }
516            // If it resolved to Concrete, it was already constrained - no action needed
517        }
518    }
519
520    /// Default unconstrained float literal type variables to f64.
521    pub fn default_float_literal_vars(&mut self, float_literal_vars: &[TypeVarId]) {
522        for &var in float_literal_vars {
523            let resolved = self.substitution.apply(&InferType::Var(var));
524            if let InferType::Var(_) = resolved {
525                self.substitution
526                    .insert(var, InferType::Concrete(Type::F64));
527            }
528        }
529    }
530
531    /// Resolve a type to its final form after applying all substitutions.
532    ///
533    /// Follows the substitution chain and defaults IntLiteral to i32.
534    /// For arrays, recursively resolves the element type.
535    /// Returns the fully-resolved `InferType`.
536    pub fn resolve_infer_type(&self, ty: &InferType) -> InferType {
537        let resolved = self.substitution.apply(ty);
538        match resolved {
539            InferType::Concrete(_) => resolved,
540            InferType::Var(_) => resolved, // Unbound variable stays as-is
541            InferType::IntLiteral => InferType::Concrete(Type::I32), // Default to i32
542            InferType::FloatLiteral => InferType::Concrete(Type::F64), // Default to f64
543            InferType::Array { element, length } => {
544                let resolved_element = self.resolve_infer_type(&element);
545                InferType::Array {
546                    element: Box::new(resolved_element),
547                    length,
548                }
549            }
550        }
551    }
552
553    /// Resolve a type to its final concrete type.
554    ///
555    /// Follows the substitution chain and defaults IntLiteral to i32.
556    /// Returns `None` if the type resolves to an unbound type variable or an array
557    /// (arrays need to be handled separately to create ArrayTypeIds).
558    pub fn resolve(&self, ty: &InferType) -> Option<Type> {
559        let resolved = self.resolve_infer_type(ty);
560        match resolved {
561            InferType::Concrete(t) => Some(t),
562            InferType::Var(_) => None,                  // Unbound variable
563            InferType::IntLiteral => Some(Type::I32), // Default to i32 (shouldn't happen after resolve_infer_type)
564            InferType::FloatLiteral => Some(Type::F64), // Default to f64
565            InferType::Array { .. } => None,          // Arrays need special handling
566        }
567    }
568
569    /// Resolve a type to its final concrete type, with error recovery.
570    ///
571    /// Like `resolve`, but returns `Type::ERROR` instead of `None` for
572    /// unbound type variables. This allows compilation to continue
573    /// with error recovery.
574    ///
575    /// Note: For arrays, this returns `Type::ERROR`. Use `resolve_infer_type`
576    /// and handle `InferType::Array` explicitly to create proper ArrayTypeIds.
577    pub fn resolve_or_error(&self, ty: &InferType) -> Type {
578        self.resolve(ty).unwrap_or(Type::ERROR)
579    }
580}
581
582#[cfg(test)]
583mod tests {
584    use super::*;
585
586    #[test]
587    fn test_unify_same_concrete() {
588        let mut unifier = Unifier::new();
589        let result = unifier.unify(
590            &InferType::Concrete(Type::I32),
591            &InferType::Concrete(Type::I32),
592        );
593        assert!(result.is_ok());
594    }
595
596    #[test]
597    fn test_unify_different_concrete() {
598        let mut unifier = Unifier::new();
599        let result = unifier.unify(
600            &InferType::Concrete(Type::I32),
601            &InferType::Concrete(Type::I64),
602        );
603        assert!(!result.is_ok());
604        assert!(matches!(result, UnifyResult::TypeMismatch { .. }));
605    }
606
607    #[test]
608    fn test_unify_var_with_concrete() {
609        let mut unifier = Unifier::new();
610        let v0 = TypeVarId::new(0);
611
612        let result = unifier.unify(&InferType::Var(v0), &InferType::Concrete(Type::BOOL));
613        assert!(result.is_ok());
614
615        // Variable should now be bound to Bool
616        assert_eq!(
617            unifier.substitution.apply(&InferType::Var(v0)),
618            InferType::Concrete(Type::BOOL)
619        );
620    }
621
622    #[test]
623    fn test_unify_int_literal_with_integer() {
624        let mut unifier = Unifier::new();
625        let result = unifier.unify(&InferType::IntLiteral, &InferType::Concrete(Type::I64));
626        assert!(result.is_ok());
627    }
628
629    #[test]
630    fn test_unify_int_literal_with_non_integer() {
631        let mut unifier = Unifier::new();
632        let result = unifier.unify(&InferType::IntLiteral, &InferType::Concrete(Type::BOOL));
633        assert!(!result.is_ok());
634        assert!(matches!(result, UnifyResult::IntLiteralNonInteger { .. }));
635    }
636
637    #[test]
638    fn test_unify_two_int_literals() {
639        let mut unifier = Unifier::new();
640        let result = unifier.unify(&InferType::IntLiteral, &InferType::IntLiteral);
641        assert!(result.is_ok());
642    }
643
644    #[test]
645    fn test_unify_never_coerces() {
646        let mut unifier = Unifier::new();
647        let result = unifier.unify(
648            &InferType::Concrete(Type::NEVER),
649            &InferType::Concrete(Type::I32),
650        );
651        assert!(result.is_ok());
652    }
653
654    #[test]
655    fn test_unify_error_coerces() {
656        let mut unifier = Unifier::new();
657        let result = unifier.unify(
658            &InferType::Concrete(Type::ERROR),
659            &InferType::Concrete(Type::BOOL),
660        );
661        assert!(result.is_ok());
662    }
663
664    #[test]
665    fn test_unify_two_vars() {
666        let mut unifier = Unifier::new();
667        let v0 = TypeVarId::new(0);
668        let v1 = TypeVarId::new(1);
669
670        let result = unifier.unify(&InferType::Var(v0), &InferType::Var(v1));
671        assert!(result.is_ok());
672
673        // One should be bound to the other
674        // (implementation detail: v0 gets bound to v1)
675    }
676
677    #[test]
678    fn test_unify_chain_resolution() {
679        let mut unifier = Unifier::new();
680        let v0 = TypeVarId::new(0);
681        let v1 = TypeVarId::new(1);
682
683        // v0 = v1
684        let result = unifier.unify(&InferType::Var(v0), &InferType::Var(v1));
685        assert!(result.is_ok());
686
687        // v1 = i64
688        let result = unifier.unify(&InferType::Var(v1), &InferType::Concrete(Type::I64));
689        assert!(result.is_ok());
690
691        // v0 should now resolve to i64
692        assert_eq!(
693            unifier.substitution.apply(&InferType::Var(v0)),
694            InferType::Concrete(Type::I64)
695        );
696    }
697
698    #[test]
699    fn test_resolve_concrete() {
700        let unifier = Unifier::new();
701        let ty = InferType::Concrete(Type::BOOL);
702        assert_eq!(unifier.resolve(&ty), Some(Type::BOOL));
703    }
704
705    #[test]
706    fn test_resolve_int_literal_defaults_to_i32() {
707        let unifier = Unifier::new();
708        let ty = InferType::IntLiteral;
709        assert_eq!(unifier.resolve(&ty), Some(Type::I32));
710    }
711
712    #[test]
713    fn test_resolve_unbound_var_returns_none() {
714        let unifier = Unifier::new();
715        let ty = InferType::Var(TypeVarId::new(0));
716        assert_eq!(unifier.resolve(&ty), None);
717    }
718
719    #[test]
720    fn test_resolve_bound_var() {
721        let mut unifier = Unifier::new();
722        let v0 = TypeVarId::new(0);
723        unifier
724            .substitution
725            .insert(v0, InferType::Concrete(Type::U8));
726
727        assert_eq!(unifier.resolve(&InferType::Var(v0)), Some(Type::U8));
728    }
729
730    #[test]
731    fn test_check_signed_with_signed_type() {
732        let unifier = Unifier::new();
733        assert!(
734            unifier
735                .check_signed(&InferType::Concrete(Type::I32))
736                .is_ok()
737        );
738        assert!(
739            unifier
740                .check_signed(&InferType::Concrete(Type::I64))
741                .is_ok()
742        );
743    }
744
745    #[test]
746    fn test_check_signed_with_unsigned_type() {
747        let unifier = Unifier::new();
748        let result = unifier.check_signed(&InferType::Concrete(Type::U32));
749        assert!(!result.is_ok());
750        assert!(matches!(result, UnifyResult::NotSigned { ty: Type::U32 }));
751    }
752
753    #[test]
754    fn test_check_signed_with_int_literal() {
755        let unifier = Unifier::new();
756        // IntLiteral defaults to i32 which is signed, so this is OK
757        assert!(unifier.check_signed(&InferType::IntLiteral).is_ok());
758    }
759
760    #[test]
761    fn test_unify_var_with_itself() {
762        let mut unifier = Unifier::new();
763        let v0 = TypeVarId::new(0);
764
765        // Unifying a variable with itself should succeed (no-op)
766        let result = unifier.unify(&InferType::Var(v0), &InferType::Var(v0));
767        assert!(result.is_ok());
768    }
769
770    #[test]
771    fn test_occurs_check_prevents_infinite_type() {
772        let mut unifier = Unifier::new();
773        let v0 = TypeVarId::new(0);
774        let v1 = TypeVarId::new(1);
775
776        // Bind v1 to v0
777        unifier.substitution.insert(v1, InferType::Var(v0));
778
779        // Now try to bind v0 to v1 - this would create a cycle
780        let result = unifier.bind(v0, &InferType::Var(v1));
781        assert!(matches!(result, UnifyResult::OccursCheck { .. }));
782    }
783
784    #[test]
785    fn test_check_integer_with_integer_types() {
786        let unifier = Unifier::new();
787        assert!(
788            unifier
789                .check_integer(&InferType::Concrete(Type::I32))
790                .is_ok()
791        );
792        assert!(
793            unifier
794                .check_integer(&InferType::Concrete(Type::I64))
795                .is_ok()
796        );
797        assert!(
798            unifier
799                .check_integer(&InferType::Concrete(Type::U8))
800                .is_ok()
801        );
802        assert!(
803            unifier
804                .check_integer(&InferType::Concrete(Type::U64))
805                .is_ok()
806        );
807    }
808
809    #[test]
810    fn test_check_integer_with_non_integer_type() {
811        let unifier = Unifier::new();
812        let result = unifier.check_integer(&InferType::Concrete(Type::BOOL));
813        assert!(!result.is_ok());
814        assert!(matches!(result, UnifyResult::NotInteger { ty: Type::BOOL }));
815    }
816
817    #[test]
818    fn test_check_integer_with_int_literal() {
819        let unifier = Unifier::new();
820        // IntLiteral is OK - it will become an integer type
821        assert!(unifier.check_integer(&InferType::IntLiteral).is_ok());
822    }
823
824    #[test]
825    fn test_check_integer_with_type_variable() {
826        let unifier = Unifier::new();
827        // Type variable is OK - it might become an integer type
828        assert!(
829            unifier
830                .check_integer(&InferType::Var(TypeVarId::new(0)))
831                .is_ok()
832        );
833    }
834
835    #[test]
836    fn test_check_integer_with_error_type() {
837        let unifier = Unifier::new();
838        // Error type is OK - for error recovery
839        assert!(
840            unifier
841                .check_integer(&InferType::Concrete(Type::ERROR))
842                .is_ok()
843        );
844    }
845
846    #[test]
847    fn test_solve_constraints_empty() {
848        let mut unifier = Unifier::new();
849        let errors = unifier.solve_constraints(&[]);
850        assert!(errors.is_empty());
851    }
852
853    #[test]
854    fn test_solve_constraints_simple_equal() {
855        let mut unifier = Unifier::new();
856        let v0 = TypeVarId::new(0);
857        let constraints = vec![Constraint::equal(
858            InferType::Var(v0),
859            InferType::Concrete(Type::I64),
860            Span::new(0, 5),
861        )];
862        let errors = unifier.solve_constraints(&constraints);
863        assert!(errors.is_empty());
864        assert_eq!(unifier.resolve(&InferType::Var(v0)), Some(Type::I64));
865    }
866
867    #[test]
868    fn test_solve_constraints_multiple_equal() {
869        let mut unifier = Unifier::new();
870        let v0 = TypeVarId::new(0);
871        let v1 = TypeVarId::new(1);
872        let constraints = vec![
873            Constraint::equal(InferType::Var(v0), InferType::Var(v1), Span::new(0, 5)),
874            Constraint::equal(
875                InferType::Var(v1),
876                InferType::Concrete(Type::BOOL),
877                Span::new(6, 10),
878            ),
879        ];
880        let errors = unifier.solve_constraints(&constraints);
881        assert!(errors.is_empty());
882        // Both variables should resolve to Bool
883        assert_eq!(unifier.resolve(&InferType::Var(v0)), Some(Type::BOOL));
884        assert_eq!(unifier.resolve(&InferType::Var(v1)), Some(Type::BOOL));
885    }
886
887    #[test]
888    fn test_solve_constraints_type_mismatch() {
889        let mut unifier = Unifier::new();
890        let constraints = vec![Constraint::equal(
891            InferType::Concrete(Type::I32),
892            InferType::Concrete(Type::BOOL),
893            Span::new(0, 5),
894        )];
895        let errors = unifier.solve_constraints(&constraints);
896        assert_eq!(errors.len(), 1);
897        assert!(matches!(errors[0].kind, UnifyResult::TypeMismatch { .. }));
898        assert_eq!(errors[0].span, Span::new(0, 5));
899    }
900
901    #[test]
902    fn test_solve_constraints_int_literal_unifies() {
903        let mut unifier = Unifier::new();
904        let v0 = TypeVarId::new(0);
905        let constraints = vec![
906            Constraint::equal(InferType::Var(v0), InferType::IntLiteral, Span::new(0, 5)),
907            Constraint::equal(
908                InferType::Var(v0),
909                InferType::Concrete(Type::I64),
910                Span::new(6, 10),
911            ),
912        ];
913        let errors = unifier.solve_constraints(&constraints);
914        assert!(errors.is_empty());
915        // Should resolve to i64 (IntLiteral takes the concrete integer type)
916        assert_eq!(unifier.resolve(&InferType::Var(v0)), Some(Type::I64));
917    }
918
919    #[test]
920    fn test_solve_constraints_is_signed() {
921        let mut unifier = Unifier::new();
922        let constraints = vec![Constraint::is_signed(
923            InferType::Concrete(Type::I32),
924            Span::new(0, 5),
925        )];
926        let errors = unifier.solve_constraints(&constraints);
927        assert!(errors.is_empty());
928    }
929
930    #[test]
931    fn test_solve_constraints_is_signed_fails_for_unsigned() {
932        let mut unifier = Unifier::new();
933        let constraints = vec![Constraint::is_signed(
934            InferType::Concrete(Type::U32),
935            Span::new(0, 5),
936        )];
937        let errors = unifier.solve_constraints(&constraints);
938        assert_eq!(errors.len(), 1);
939        assert!(matches!(
940            errors[0].kind,
941            UnifyResult::NotSigned { ty: Type::U32 }
942        ));
943    }
944
945    #[test]
946    fn test_solve_constraints_is_integer() {
947        let mut unifier = Unifier::new();
948        let constraints = vec![Constraint::is_integer(
949            InferType::Concrete(Type::U8),
950            Span::new(0, 5),
951        )];
952        let errors = unifier.solve_constraints(&constraints);
953        assert!(errors.is_empty());
954    }
955
956    #[test]
957    fn test_solve_constraints_is_integer_fails_for_bool() {
958        let mut unifier = Unifier::new();
959        let constraints = vec![Constraint::is_integer(
960            InferType::Concrete(Type::BOOL),
961            Span::new(0, 5),
962        )];
963        let errors = unifier.solve_constraints(&constraints);
964        assert_eq!(errors.len(), 1);
965        assert!(matches!(
966            errors[0].kind,
967            UnifyResult::NotInteger { ty: Type::BOOL }
968        ));
969    }
970
971    #[test]
972    fn test_solve_constraints_multiple_errors() {
973        let mut unifier = Unifier::new();
974        let constraints = vec![
975            Constraint::equal(
976                InferType::Concrete(Type::I32),
977                InferType::Concrete(Type::BOOL),
978                Span::new(0, 5),
979            ),
980            Constraint::is_signed(InferType::Concrete(Type::U64), Span::new(10, 15)),
981        ];
982        let errors = unifier.solve_constraints(&constraints);
983        // Should catch both errors in one pass
984        assert_eq!(errors.len(), 2);
985    }
986
987    #[test]
988    fn test_solve_constraints_error_recovery() {
989        let mut unifier = Unifier::new();
990        let v0 = TypeVarId::new(0);
991        let constraints = vec![
992            // This will fail: can't unify type variable with mismatched concretes
993            Constraint::equal(
994                InferType::Var(v0),
995                InferType::Concrete(Type::I32),
996                Span::new(0, 5),
997            ),
998            Constraint::equal(
999                InferType::Var(v0),
1000                InferType::Concrete(Type::BOOL),
1001                Span::new(6, 10),
1002            ),
1003        ];
1004        let errors = unifier.solve_constraints(&constraints);
1005        // Should report the second constraint as an error (first succeeds)
1006        assert_eq!(errors.len(), 1);
1007        // v0 should still be usable (bound to i32 from first constraint)
1008        assert_eq!(unifier.resolve(&InferType::Var(v0)), Some(Type::I32));
1009    }
1010
1011    #[test]
1012    fn test_default_int_literal_vars() {
1013        let mut unifier = Unifier::new();
1014        let v0 = TypeVarId::new(0);
1015        let v1 = TypeVarId::new(1);
1016        let v2 = TypeVarId::new(2);
1017
1018        // v0 and v1 are int literal vars (unbound)
1019        // v2 is bound to Bool
1020        unifier
1021            .substitution
1022            .insert(v2, InferType::Concrete(Type::BOOL));
1023
1024        // Track v0 and v1 as int literal vars
1025        let int_literal_vars = vec![v0, v1];
1026
1027        unifier.default_int_literal_vars(&int_literal_vars);
1028
1029        // v0 and v1 should now be i32
1030        assert_eq!(unifier.resolve(&InferType::Var(v0)), Some(Type::I32));
1031        assert_eq!(unifier.resolve(&InferType::Var(v1)), Some(Type::I32));
1032        // v2 should still be Bool
1033        assert_eq!(unifier.resolve(&InferType::Var(v2)), Some(Type::BOOL));
1034    }
1035
1036    #[test]
1037    fn test_resolve_or_error_with_bound_var() {
1038        let mut unifier = Unifier::new();
1039        let v0 = TypeVarId::new(0);
1040        unifier
1041            .substitution
1042            .insert(v0, InferType::Concrete(Type::U8));
1043        assert_eq!(unifier.resolve_or_error(&InferType::Var(v0)), Type::U8);
1044    }
1045
1046    #[test]
1047    fn test_resolve_or_error_with_unbound_var() {
1048        let unifier = Unifier::new();
1049        let v0 = TypeVarId::new(0);
1050        // Unbound variable should return Error
1051        assert_eq!(unifier.resolve_or_error(&InferType::Var(v0)), Type::ERROR);
1052    }
1053
1054    #[test]
1055    fn test_resolve_or_error_with_int_literal() {
1056        let unifier = Unifier::new();
1057        // IntLiteral defaults to i32
1058        assert_eq!(unifier.resolve_or_error(&InferType::IntLiteral), Type::I32);
1059    }
1060
1061    #[test]
1062    fn test_unification_error_message() {
1063        let error = UnificationError::new(
1064            UnifyResult::TypeMismatch {
1065                expected: InferType::Concrete(Type::I32),
1066                found: InferType::Concrete(Type::BOOL),
1067            },
1068            Span::new(10, 20),
1069        );
1070        let msg = error.message();
1071        assert!(msg.contains("type mismatch"));
1072        assert!(msg.contains("i32"));
1073        assert!(msg.contains("bool"));
1074    }
1075
1076    #[test]
1077    fn test_unification_error_int_literal_non_integer_message() {
1078        let error = UnificationError::new(
1079            UnifyResult::IntLiteralNonInteger { found: Type::BOOL },
1080            Span::new(0, 5),
1081        );
1082        let msg = error.message();
1083        assert!(msg.contains("integer literal"));
1084        assert!(msg.contains("bool"));
1085    }
1086
1087    #[test]
1088    fn test_unification_error_not_signed_message() {
1089        let error =
1090            UnificationError::new(UnifyResult::NotSigned { ty: Type::U32 }, Span::new(0, 5));
1091        let msg = error.message();
1092        assert!(msg.contains("negate"));
1093        assert!(msg.contains("u32"));
1094    }
1095
1096    #[test]
1097    fn test_unification_error_not_integer_message() {
1098        let error =
1099            UnificationError::new(UnifyResult::NotInteger { ty: Type::BOOL }, Span::new(0, 5));
1100        let msg = error.message();
1101        assert!(msg.contains("expected integer"));
1102        assert!(msg.contains("bool"));
1103    }
1104
1105    #[test]
1106    fn test_solve_constraints_int_literal_with_non_integer() {
1107        let mut unifier = Unifier::new();
1108        let constraints = vec![Constraint::equal(
1109            InferType::IntLiteral,
1110            InferType::Concrete(Type::BOOL),
1111            Span::new(0, 5),
1112        )];
1113        let errors = unifier.solve_constraints(&constraints);
1114        assert_eq!(errors.len(), 1);
1115        assert!(matches!(
1116            errors[0].kind,
1117            UnifyResult::IntLiteralNonInteger { found: Type::BOOL }
1118        ));
1119    }
1120
1121    #[test]
1122    fn test_solve_constraints_chain_resolution() {
1123        // Test: v0 = v1, v1 = v2, v2 = i64
1124        // All should resolve to i64
1125        let mut unifier = Unifier::new();
1126        let v0 = TypeVarId::new(0);
1127        let v1 = TypeVarId::new(1);
1128        let v2 = TypeVarId::new(2);
1129        let constraints = vec![
1130            Constraint::equal(InferType::Var(v0), InferType::Var(v1), Span::new(0, 5)),
1131            Constraint::equal(InferType::Var(v1), InferType::Var(v2), Span::new(6, 10)),
1132            Constraint::equal(
1133                InferType::Var(v2),
1134                InferType::Concrete(Type::I64),
1135                Span::new(11, 15),
1136            ),
1137        ];
1138        let errors = unifier.solve_constraints(&constraints);
1139        assert!(errors.is_empty());
1140        assert_eq!(unifier.resolve(&InferType::Var(v0)), Some(Type::I64));
1141        assert_eq!(unifier.resolve(&InferType::Var(v1)), Some(Type::I64));
1142        assert_eq!(unifier.resolve(&InferType::Var(v2)), Some(Type::I64));
1143    }
1144
1145    #[test]
1146    fn test_solve_constraints_reverse_chain() {
1147        // Test: v2 = i64, v1 = v2, v0 = v1
1148        // Should work in any order
1149        let mut unifier = Unifier::new();
1150        let v0 = TypeVarId::new(0);
1151        let v1 = TypeVarId::new(1);
1152        let v2 = TypeVarId::new(2);
1153        let constraints = vec![
1154            Constraint::equal(
1155                InferType::Var(v2),
1156                InferType::Concrete(Type::I64),
1157                Span::new(0, 5),
1158            ),
1159            Constraint::equal(InferType::Var(v1), InferType::Var(v2), Span::new(6, 10)),
1160            Constraint::equal(InferType::Var(v0), InferType::Var(v1), Span::new(11, 15)),
1161        ];
1162        let errors = unifier.solve_constraints(&constraints);
1163        assert!(errors.is_empty());
1164        assert_eq!(unifier.resolve(&InferType::Var(v0)), Some(Type::I64));
1165        assert_eq!(unifier.resolve(&InferType::Var(v1)), Some(Type::I64));
1166        assert_eq!(unifier.resolve(&InferType::Var(v2)), Some(Type::I64));
1167    }
1168
1169    // =========================================================================
1170    // Additional edge case tests
1171    // =========================================================================
1172
1173    #[test]
1174    fn test_check_unsigned_with_unsigned_types() {
1175        let unifier = Unifier::new();
1176        assert!(
1177            unifier
1178                .check_unsigned(&InferType::Concrete(Type::U8))
1179                .is_ok()
1180        );
1181        assert!(
1182            unifier
1183                .check_unsigned(&InferType::Concrete(Type::U16))
1184                .is_ok()
1185        );
1186        assert!(
1187            unifier
1188                .check_unsigned(&InferType::Concrete(Type::U32))
1189                .is_ok()
1190        );
1191        assert!(
1192            unifier
1193                .check_unsigned(&InferType::Concrete(Type::U64))
1194                .is_ok()
1195        );
1196    }
1197
1198    #[test]
1199    fn test_check_unsigned_with_signed_type() {
1200        let unifier = Unifier::new();
1201        let result = unifier.check_unsigned(&InferType::Concrete(Type::I32));
1202        assert!(!result.is_ok());
1203        assert!(matches!(result, UnifyResult::NotUnsigned { ty: Type::I32 }));
1204    }
1205
1206    #[test]
1207    fn test_check_unsigned_with_int_literal() {
1208        let unifier = Unifier::new();
1209        // IntLiteral is OK - it will become an unsigned type if constrained
1210        assert!(unifier.check_unsigned(&InferType::IntLiteral).is_ok());
1211    }
1212
1213    #[test]
1214    fn test_check_unsigned_with_non_integer_type() {
1215        let unifier = Unifier::new();
1216        let result = unifier.check_unsigned(&InferType::Concrete(Type::BOOL));
1217        assert!(!result.is_ok());
1218        assert!(matches!(
1219            result,
1220            UnifyResult::NotUnsigned { ty: Type::BOOL }
1221        ));
1222    }
1223
1224    #[test]
1225    fn test_solve_constraints_is_unsigned() {
1226        let mut unifier = Unifier::new();
1227        let constraints = vec![Constraint::is_unsigned(
1228            InferType::Concrete(Type::U64),
1229            Span::new(0, 5),
1230        )];
1231        let errors = unifier.solve_constraints(&constraints);
1232        assert!(errors.is_empty());
1233    }
1234
1235    #[test]
1236    fn test_solve_constraints_is_unsigned_fails_for_signed() {
1237        let mut unifier = Unifier::new();
1238        let constraints = vec![Constraint::is_unsigned(
1239            InferType::Concrete(Type::I32),
1240            Span::new(0, 5),
1241        )];
1242        let errors = unifier.solve_constraints(&constraints);
1243        assert_eq!(errors.len(), 1);
1244        assert!(matches!(
1245            errors[0].kind,
1246            UnifyResult::NotUnsigned { ty: Type::I32 }
1247        ));
1248    }
1249
1250    #[test]
1251    fn test_unify_array_same_element_same_length() {
1252        let mut unifier = Unifier::new();
1253        let arr1 = InferType::Array {
1254            element: Box::new(InferType::Concrete(Type::I32)),
1255            length: 5,
1256        };
1257        let arr2 = InferType::Array {
1258            element: Box::new(InferType::Concrete(Type::I32)),
1259            length: 5,
1260        };
1261        let result = unifier.unify(&arr1, &arr2);
1262        assert!(result.is_ok());
1263    }
1264
1265    #[test]
1266    fn test_unify_array_different_length() {
1267        let mut unifier = Unifier::new();
1268        let arr1 = InferType::Array {
1269            element: Box::new(InferType::Concrete(Type::I32)),
1270            length: 3,
1271        };
1272        let arr2 = InferType::Array {
1273            element: Box::new(InferType::Concrete(Type::I32)),
1274            length: 5,
1275        };
1276        let result = unifier.unify(&arr1, &arr2);
1277        assert!(!result.is_ok());
1278        assert!(matches!(result, UnifyResult::ArrayLengthMismatch { .. }));
1279    }
1280
1281    #[test]
1282    fn test_unify_array_different_element_type() {
1283        let mut unifier = Unifier::new();
1284        let arr1 = InferType::Array {
1285            element: Box::new(InferType::Concrete(Type::I32)),
1286            length: 3,
1287        };
1288        let arr2 = InferType::Array {
1289            element: Box::new(InferType::Concrete(Type::BOOL)),
1290            length: 3,
1291        };
1292        let result = unifier.unify(&arr1, &arr2);
1293        assert!(!result.is_ok());
1294        assert!(matches!(result, UnifyResult::TypeMismatch { .. }));
1295    }
1296
1297    #[test]
1298    fn test_unify_array_with_variable_element() {
1299        let mut unifier = Unifier::new();
1300        let v0 = TypeVarId::new(0);
1301        let arr1 = InferType::Array {
1302            element: Box::new(InferType::Var(v0)),
1303            length: 3,
1304        };
1305        let arr2 = InferType::Array {
1306            element: Box::new(InferType::Concrete(Type::I64)),
1307            length: 3,
1308        };
1309        let result = unifier.unify(&arr1, &arr2);
1310        assert!(result.is_ok());
1311
1312        // Variable should now be bound to I64
1313        assert_eq!(
1314            unifier.substitution.apply(&InferType::Var(v0)),
1315            InferType::Concrete(Type::I64)
1316        );
1317    }
1318
1319    #[test]
1320    fn test_unify_array_with_int_literal_element() {
1321        let mut unifier = Unifier::new();
1322        let arr1 = InferType::Array {
1323            element: Box::new(InferType::IntLiteral),
1324            length: 3,
1325        };
1326        let arr2 = InferType::Array {
1327            element: Box::new(InferType::Concrete(Type::I32)),
1328            length: 3,
1329        };
1330        let result = unifier.unify(&arr1, &arr2);
1331        assert!(result.is_ok());
1332    }
1333
1334    #[test]
1335    fn test_unify_array_with_concrete() {
1336        let mut unifier = Unifier::new();
1337        let arr = InferType::Array {
1338            element: Box::new(InferType::Concrete(Type::I32)),
1339            length: 3,
1340        };
1341        let concrete = InferType::Concrete(Type::I32);
1342        let result = unifier.unify(&arr, &concrete);
1343        assert!(!result.is_ok());
1344        assert!(matches!(result, UnifyResult::TypeMismatch { .. }));
1345    }
1346
1347    #[test]
1348    fn test_resolve_array_type() {
1349        let mut unifier = Unifier::new();
1350        let v0 = TypeVarId::new(0);
1351
1352        // Bind v0 to array of I32
1353        unifier.substitution.insert(
1354            v0,
1355            InferType::Array {
1356                element: Box::new(InferType::Concrete(Type::I32)),
1357                length: 10,
1358            },
1359        );
1360
1361        // resolve() returns None for arrays (they need special handling)
1362        let resolved = unifier.resolve(&InferType::Var(v0));
1363        assert!(resolved.is_none());
1364
1365        // resolve_infer_type() should properly resolve arrays
1366        let resolved_infer = unifier.resolve_infer_type(&InferType::Var(v0));
1367        match resolved_infer {
1368            InferType::Array { element, length } => {
1369                assert_eq!(*element, InferType::Concrete(Type::I32));
1370                assert_eq!(length, 10);
1371            }
1372            _ => panic!("Expected InferType::Array, got {:?}", resolved_infer),
1373        }
1374    }
1375
1376    #[test]
1377    fn test_unification_error_not_unsigned_message() {
1378        let error =
1379            UnificationError::new(UnifyResult::NotUnsigned { ty: Type::I32 }, Span::new(0, 5));
1380        let msg = error.message();
1381        assert!(msg.contains("unsigned"));
1382        assert!(msg.contains("i32"));
1383    }
1384
1385    #[test]
1386    fn test_unification_error_array_length_mismatch_message() {
1387        let error = UnificationError::new(
1388            UnifyResult::ArrayLengthMismatch {
1389                expected: 3,
1390                found: 5,
1391            },
1392            Span::new(0, 10),
1393        );
1394        let msg = error.message();
1395        assert!(msg.contains("3"));
1396        assert!(msg.contains("5"));
1397    }
1398
1399    #[test]
1400    fn test_unification_error_occurs_check_message() {
1401        let error = UnificationError::new(
1402            UnifyResult::OccursCheck {
1403                var: TypeVarId::new(0),
1404                ty: InferType::Array {
1405                    element: Box::new(InferType::Var(TypeVarId::new(0))),
1406                    length: 3,
1407                },
1408            },
1409            Span::new(0, 5),
1410        );
1411        let msg = error.message();
1412        assert!(msg.contains("infinite") || msg.contains("occurs"));
1413    }
1414}