Skip to main content

tsz_solver/inference/
infer.rs

1//! Type inference engine using Union-Find.
2//!
3//! This module implements type inference for generic functions using
4//! the `ena` crate's Union-Find data structure.
5//!
6//! Key features:
7//! - Inference variables for generic type parameters
8//! - Constraint collection during type checking
9//! - Bounds checking (L <: α <: U)
10//! - Best common type calculation
11//! - Efficient unification with path compression
12
13use crate::TypeDatabase;
14#[cfg(test)]
15use crate::types::*;
16use crate::types::{
17    InferencePriority, IntrinsicKind, LiteralValue, TemplateSpan, TypeData, TypeId,
18};
19use crate::visitor::is_literal_type;
20use ena::unify::{InPlaceUnificationTable, NoError, UnifyKey, UnifyValue};
21use rustc_hash::{FxHashMap, FxHashSet};
22use std::cell::RefCell;
23use tsz_common::interner::Atom;
24
25/// Helper function to extend a vector with deduplicated items.
26/// Uses a `HashSet` for O(1) lookups instead of O(n) contains checks.
27fn extend_dedup<T>(target: &mut Vec<T>, items: &[T])
28where
29    T: Clone + Eq + std::hash::Hash,
30{
31    if items.is_empty() {
32        return;
33    }
34
35    // Hot path for inference: most merges add a single item.
36    // Avoid allocating/hash-building a set for that case.
37    if items.len() == 1 {
38        let item = &items[0];
39        if !target.contains(item) {
40            target.push(item.clone());
41        }
42        return;
43    }
44
45    let mut existing: FxHashSet<_> = target.iter().cloned().collect();
46    for item in items {
47        if existing.insert(item.clone()) {
48            target.push(item.clone());
49        }
50    }
51}
52
53/// An inference variable representing an unknown type.
54/// These are created when instantiating generic functions.
55#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
56pub struct InferenceVar(pub u32);
57
58// Uses TypeScript-standard InferencePriority from types.rs
59
60/// A candidate type for an inference variable.
61#[derive(Clone, Debug, PartialEq, Eq, Hash)]
62pub struct InferenceCandidate {
63    pub type_id: TypeId,
64    pub priority: InferencePriority,
65    pub is_fresh_literal: bool,
66    pub from_object_property: bool,
67    pub object_property_index: Option<u32>,
68    pub object_property_name: Option<Atom>,
69}
70
71/// Value stored for each inference variable root.
72#[derive(Clone, Debug, Default)]
73pub struct InferenceInfo {
74    pub candidates: Vec<InferenceCandidate>,
75    pub upper_bounds: Vec<TypeId>,
76    pub resolved: Option<TypeId>,
77}
78
79impl InferenceInfo {
80    pub const fn is_empty(&self) -> bool {
81        self.candidates.is_empty() && self.upper_bounds.is_empty()
82    }
83}
84
85impl UnifyKey for InferenceVar {
86    type Value = InferenceInfo;
87
88    fn index(&self) -> u32 {
89        self.0
90    }
91
92    fn from_index(u: u32) -> Self {
93        Self(u)
94    }
95
96    fn tag() -> &'static str {
97        "InferenceVar"
98    }
99}
100
101impl UnifyValue for InferenceInfo {
102    type Error = NoError;
103
104    fn unify_values(a: &Self, b: &Self) -> Result<Self, Self::Error> {
105        let mut merged = a.clone();
106
107        // Deduplicate candidates using helper
108        extend_dedup(&mut merged.candidates, &b.candidates);
109
110        // Deduplicate upper bounds using helper
111        extend_dedup(&mut merged.upper_bounds, &b.upper_bounds);
112
113        if merged.resolved.is_none() {
114            merged.resolved = b.resolved;
115        }
116        Ok(merged)
117    }
118}
119
120/// Inference error
121#[derive(Clone, Debug)]
122pub enum InferenceError {
123    /// Two incompatible types were unified
124    Conflict(TypeId, TypeId),
125    /// Inference variable was not resolved
126    Unresolved(InferenceVar),
127    /// Circular unification detected (occurs-check)
128    OccursCheck { var: InferenceVar, ty: TypeId },
129    /// Lower bound is not subtype of upper bound
130    BoundsViolation {
131        var: InferenceVar,
132        lower: TypeId,
133        upper: TypeId,
134    },
135    /// Variance violation detected
136    VarianceViolation {
137        var: InferenceVar,
138        expected_variance: &'static str,
139        position: TypeId,
140    },
141}
142
143/// Constraint set for an inference variable.
144/// Tracks both lower bounds (L <: α) and upper bounds (α <: U).
145#[derive(Clone, Debug, Default)]
146pub struct ConstraintSet {
147    /// Lower bounds: types that must be subtypes of this variable
148    /// e.g., from argument types being assigned to a parameter
149    pub lower_bounds: Vec<TypeId>,
150    /// Upper bounds: types that this variable must be a subtype of
151    /// e.g., from `extends` constraints on type parameters
152    pub upper_bounds: Vec<TypeId>,
153}
154
155impl ConstraintSet {
156    pub const fn new() -> Self {
157        Self {
158            lower_bounds: Vec::new(),
159            upper_bounds: Vec::new(),
160        }
161    }
162
163    pub fn from_info(info: &InferenceInfo) -> Self {
164        let mut lower_bounds = Vec::new();
165        let mut upper_bounds = Vec::new();
166        let mut seen_lower = FxHashSet::default();
167        let mut seen_upper = FxHashSet::default();
168
169        for candidate in &info.candidates {
170            if seen_lower.insert(candidate.type_id) {
171                lower_bounds.push(candidate.type_id);
172            }
173        }
174
175        for &upper in &info.upper_bounds {
176            if seen_upper.insert(upper) {
177                upper_bounds.push(upper);
178            }
179        }
180
181        Self {
182            lower_bounds,
183            upper_bounds,
184        }
185    }
186
187    /// Add a lower bound constraint: L <: α
188    pub fn add_lower_bound(&mut self, ty: TypeId) {
189        if !self.lower_bounds.contains(&ty) {
190            self.lower_bounds.push(ty);
191        }
192    }
193
194    /// Add an upper bound constraint: α <: U
195    pub fn add_upper_bound(&mut self, ty: TypeId) {
196        if !self.upper_bounds.contains(&ty) {
197            self.upper_bounds.push(ty);
198        }
199    }
200
201    /// Check if there are any constraints
202    pub const fn is_empty(&self) -> bool {
203        self.lower_bounds.is_empty() && self.upper_bounds.is_empty()
204    }
205
206    pub fn merge_from(&mut self, other: Self) {
207        for ty in other.lower_bounds {
208            self.add_lower_bound(ty);
209        }
210        for ty in other.upper_bounds {
211            self.add_upper_bound(ty);
212        }
213    }
214
215    /// Perform transitive reduction on upper bounds to remove redundant constraints.
216    ///
217    /// If we have constraints (T <: A) and (T <: B) and we know (A <: B),
218    /// then (T <: B) is redundant and can be removed.
219    ///
220    /// This reduces N² pairwise checks in `detect_conflicts` to O(N * `reduced_N`).
221    pub fn transitive_reduction(&mut self, interner: &dyn TypeDatabase) {
222        if self.upper_bounds.len() < 2 {
223            return;
224        }
225
226        let mut redundant = FxHashSet::default();
227        let bounds = &self.upper_bounds;
228
229        for (i, &u1) in bounds.iter().enumerate() {
230            for (j, &u2) in bounds.iter().enumerate() {
231                if i == j || redundant.contains(&u1) || redundant.contains(&u2) {
232                    continue;
233                }
234
235                // If u1 <: u2, then u2 is redundant (u1 is a stricter constraint)
236                if crate::relations::subtype::is_subtype_of(interner, u1, u2) {
237                    redundant.insert(u2);
238                }
239            }
240        }
241
242        if !redundant.is_empty() {
243            self.upper_bounds.retain(|ty| !redundant.contains(ty));
244        }
245    }
246
247    /// Detect early conflicts between collected constraints.
248    /// This allows failing fast before full resolution.
249    pub fn detect_conflicts(&self, interner: &dyn TypeDatabase) -> Option<ConstraintConflict> {
250        // PERF: Transitive reduction of upper bounds to minimize N² checks.
251        let mut reduced_upper = self.upper_bounds.clone();
252        if reduced_upper.len() >= 2 {
253            let mut redundant = FxHashSet::default();
254            for (i, &u1) in reduced_upper.iter().enumerate() {
255                for (j, &u2) in reduced_upper.iter().enumerate() {
256                    if i == j || redundant.contains(&u1) || redundant.contains(&u2) {
257                        continue;
258                    }
259                    if crate::relations::subtype::is_subtype_of(interner, u1, u2) {
260                        redundant.insert(u2);
261                    }
262                }
263            }
264            if !redundant.is_empty() {
265                reduced_upper.retain(|ty| !redundant.contains(ty));
266            }
267        }
268
269        // 1. Check for mutually exclusive upper bounds
270        for (i, &u1) in reduced_upper.iter().enumerate() {
271            for &u2 in &reduced_upper[i + 1..] {
272                if are_disjoint(interner, u1, u2) {
273                    return Some(ConstraintConflict::DisjointUpperBounds(u1, u2));
274                }
275            }
276        }
277
278        // 2. Check if any lower bound is incompatible with any upper bound
279        for &lower in &self.lower_bounds {
280            for &upper in &reduced_upper {
281                // Ignore ERROR and ANY for conflict detection
282                if lower == TypeId::ERROR
283                    || upper == TypeId::ERROR
284                    || lower == TypeId::ANY
285                    || upper == TypeId::ANY
286                {
287                    continue;
288                }
289                if !crate::relations::subtype::is_subtype_of(interner, lower, upper) {
290                    return Some(ConstraintConflict::LowerExceedsUpper(lower, upper));
291                }
292            }
293        }
294
295        None
296    }
297}
298
299/// Conflict detected between constraints on an inference variable.
300#[derive(Clone, Debug)]
301pub enum ConstraintConflict {
302    /// Mutually exclusive upper bounds (e.g., string AND number)
303    DisjointUpperBounds(TypeId, TypeId),
304    /// A lower bound is not a subtype of an upper bound
305    LowerExceedsUpper(TypeId, TypeId),
306}
307
308/// Helper to determine if two types are definitely disjoint (no common inhabitants).
309fn are_disjoint(interner: &dyn TypeDatabase, a: TypeId, b: TypeId) -> bool {
310    if a == b {
311        return false;
312    }
313    if a.is_any_or_unknown() || b.is_any_or_unknown() {
314        return false;
315    }
316
317    let key_a = interner.lookup(a);
318    let key_b = interner.lookup(b);
319
320    match (key_a, key_b) {
321        (Some(TypeData::Intrinsic(k1)), Some(TypeData::Intrinsic(k2))) => {
322            use IntrinsicKind::*;
323            // Basic primitives are disjoint (ignoring object/Function which are more complex)
324            k1 != k2 && !matches!((k1, k2), (Object | Function, _) | (_, Object | Function))
325        }
326        (Some(TypeData::Literal(l1)), Some(TypeData::Literal(l2))) => l1 != l2,
327        (Some(TypeData::Literal(l1)), Some(TypeData::Intrinsic(k2))) => {
328            !is_literal_compatible_with_intrinsic(&l1, k2)
329        }
330        (Some(TypeData::Intrinsic(k1)), Some(TypeData::Literal(l2))) => {
331            !is_literal_compatible_with_intrinsic(&l2, k1)
332        }
333        _ => false,
334    }
335}
336
337fn is_literal_compatible_with_intrinsic(lit: &LiteralValue, kind: IntrinsicKind) -> bool {
338    match lit {
339        LiteralValue::String(_) => kind == IntrinsicKind::String,
340        LiteralValue::Number(_) => kind == IntrinsicKind::Number,
341        LiteralValue::BigInt(_) => kind == IntrinsicKind::Bigint,
342        LiteralValue::Boolean(_) => kind == IntrinsicKind::Boolean,
343    }
344}
345
346/// Maximum iterations for constraint strengthening loops to prevent infinite loops.
347pub const MAX_CONSTRAINT_ITERATIONS: usize = 100;
348
349/// Maximum recursion depth for type containment checks.
350pub const MAX_TYPE_RECURSION_DEPTH: usize = 100;
351
352/// Type inference context for a single function call or expression.
353pub struct InferenceContext<'a> {
354    pub(crate) interner: &'a dyn TypeDatabase,
355    /// Type resolver for semantic lookups (e.g., base class queries)
356    pub(crate) resolver: Option<&'a dyn crate::TypeResolver>,
357    /// Memoized subtype checks used by BCT and bound validation.
358    pub(crate) subtype_cache: RefCell<FxHashMap<(TypeId, TypeId), bool>>,
359    /// Unification table for inference variables
360    pub(crate) table: InPlaceUnificationTable<InferenceVar>,
361    /// Map from type parameter names to inference variables, with const flag
362    pub(crate) type_params: Vec<(Atom, InferenceVar, bool)>,
363}
364
365impl<'a> InferenceContext<'a> {
366    pub(crate) const UPPER_BOUND_INTERSECTION_FAST_PATH_LIMIT: usize = 8;
367    pub(crate) const UPPER_BOUND_INTERSECTION_LARGE_SET_THRESHOLD: usize = 64;
368
369    pub fn new(interner: &'a dyn TypeDatabase) -> Self {
370        InferenceContext {
371            interner,
372            resolver: None,
373            subtype_cache: RefCell::new(FxHashMap::default()),
374            table: InPlaceUnificationTable::new(),
375            type_params: Vec::new(),
376        }
377    }
378
379    pub fn with_resolver(
380        interner: &'a dyn TypeDatabase,
381        resolver: &'a dyn crate::TypeResolver,
382    ) -> Self {
383        InferenceContext {
384            interner,
385            resolver: Some(resolver),
386            subtype_cache: RefCell::new(FxHashMap::default()),
387            table: InPlaceUnificationTable::new(),
388            type_params: Vec::new(),
389        }
390    }
391
392    /// Create a fresh inference variable
393    pub fn fresh_var(&mut self) -> InferenceVar {
394        self.table.new_key(InferenceInfo::default())
395    }
396
397    /// Create an inference variable for a type parameter
398    pub fn fresh_type_param(&mut self, name: Atom, is_const: bool) -> InferenceVar {
399        let var = self.fresh_var();
400        self.type_params.push((name, var, is_const));
401        var
402    }
403
404    /// Register an existing inference variable as representing a type parameter.
405    ///
406    /// This is useful when the caller needs to compute a unique placeholder name
407    /// (and corresponding placeholder `TypeId`) after allocating the inference variable.
408    pub fn register_type_param(&mut self, name: Atom, var: InferenceVar, is_const: bool) {
409        self.type_params.push((name, var, is_const));
410    }
411
412    /// Look up an inference variable by type parameter name
413    pub fn find_type_param(&self, name: Atom) -> Option<InferenceVar> {
414        self.type_params
415            .iter()
416            .find(|(n, _, _)| *n == name)
417            .map(|(_, v, _)| *v)
418    }
419
420    /// Check if an inference variable is a const type parameter
421    pub fn is_var_const(&mut self, var: InferenceVar) -> bool {
422        let root = self.table.find(var);
423        self.type_params
424            .iter()
425            .any(|(_, v, is_const)| self.table.find(*v) == root && *is_const)
426    }
427
428    /// Probe the current value of an inference variable
429    pub fn probe(&mut self, var: InferenceVar) -> Option<TypeId> {
430        self.table.probe_value(var).resolved
431    }
432
433    /// Unify an inference variable with a concrete type
434    pub fn unify_var_type(&mut self, var: InferenceVar, ty: TypeId) -> Result<(), InferenceError> {
435        // Get the root variable
436        let root = self.table.find(var);
437
438        if self.occurs_in(root, ty) {
439            return Err(InferenceError::OccursCheck { var: root, ty });
440        }
441
442        // Check current value
443        match self.table.probe_value(root).resolved {
444            None => {
445                self.table.union_value(
446                    root,
447                    InferenceInfo {
448                        resolved: Some(ty),
449                        ..InferenceInfo::default()
450                    },
451                );
452                Ok(())
453            }
454            Some(existing) => {
455                if self.types_compatible(existing, ty) {
456                    Ok(())
457                } else {
458                    Err(InferenceError::Conflict(existing, ty))
459                }
460            }
461        }
462    }
463
464    /// Unify two inference variables
465    pub fn unify_vars(&mut self, a: InferenceVar, b: InferenceVar) -> Result<(), InferenceError> {
466        let root_a = self.table.find(a);
467        let root_b = self.table.find(b);
468
469        if root_a == root_b {
470            return Ok(());
471        }
472
473        let value_a = self.table.probe_value(root_a).resolved;
474        let value_b = self.table.probe_value(root_b).resolved;
475        if let (Some(a_ty), Some(b_ty)) = (value_a, value_b)
476            && !self.types_compatible(a_ty, b_ty)
477        {
478            return Err(InferenceError::Conflict(a_ty, b_ty));
479        }
480
481        self.table
482            .unify_var_var(root_a, root_b)
483            .map_err(|_| InferenceError::Conflict(TypeId::ERROR, TypeId::ERROR))?;
484        Ok(())
485    }
486
487    /// Check if two types are compatible for unification
488    fn types_compatible(&self, a: TypeId, b: TypeId) -> bool {
489        if a == b {
490            return true;
491        }
492
493        // Any is compatible with everything
494        if a == TypeId::ANY || b == TypeId::ANY {
495            return true;
496        }
497
498        // Unknown is compatible with everything
499        if a == TypeId::UNKNOWN || b == TypeId::UNKNOWN {
500            return true;
501        }
502
503        // Never is compatible with everything
504        if a == TypeId::NEVER || b == TypeId::NEVER {
505            return true;
506        }
507
508        false
509    }
510
511    pub(crate) fn occurs_in(&mut self, var: InferenceVar, ty: TypeId) -> bool {
512        let root = self.table.find(var);
513        if self.type_params.is_empty() {
514            return false;
515        }
516
517        let mut visited = FxHashSet::default();
518        for &(atom, param_var, _) in &self.type_params {
519            if self.table.find(param_var) == root
520                && self.type_contains_param(ty, atom, &mut visited)
521            {
522                return true;
523            }
524        }
525        false
526    }
527
528    pub(crate) fn type_param_names_for_root(&mut self, root: InferenceVar) -> Vec<Atom> {
529        self.type_params
530            .iter()
531            .filter(|&(_name, var, _)| self.table.find(*var) == root)
532            .map(|(name, _var, _)| *name)
533            .collect()
534    }
535
536    pub(crate) fn upper_bound_cycles_param(&mut self, bound: TypeId, targets: &[Atom]) -> bool {
537        let mut params = FxHashSet::default();
538        let mut visited = FxHashSet::default();
539        self.collect_type_params(bound, &mut params, &mut visited);
540
541        for name in params {
542            let mut seen = FxHashSet::default();
543            if self.param_depends_on_targets(name, targets, &mut seen) {
544                return true;
545            }
546        }
547
548        false
549    }
550
551    pub(crate) fn expand_cyclic_upper_bound(
552        &mut self,
553        root: InferenceVar,
554        bound: TypeId,
555        target_names: &[Atom],
556        candidates: &mut Vec<InferenceCandidate>,
557        upper_bounds: &mut Vec<TypeId>,
558    ) {
559        let name = match self.interner.lookup(bound) {
560            Some(TypeData::TypeParameter(info) | TypeData::Infer(info)) => info.name,
561            _ => return,
562        };
563
564        let Some(var) = self.find_type_param(name) else {
565            return;
566        };
567
568        if let Some(resolved) = self.probe(var) {
569            if !upper_bounds.contains(&resolved) {
570                upper_bounds.push(resolved);
571            }
572            return;
573        }
574
575        let bound_root = self.table.find(var);
576        let info = self.table.probe_value(bound_root);
577
578        for candidate in info.candidates {
579            if self.occurs_in(root, candidate.type_id) {
580                continue;
581            }
582            candidates.push(InferenceCandidate {
583                type_id: candidate.type_id,
584                priority: InferencePriority::Circular,
585                is_fresh_literal: candidate.is_fresh_literal,
586                from_object_property: candidate.from_object_property,
587                object_property_index: candidate.object_property_index,
588                object_property_name: candidate.object_property_name,
589            });
590        }
591
592        for ty in info.upper_bounds {
593            if self.occurs_in(root, ty) {
594                continue;
595            }
596            if !target_names.is_empty() && self.upper_bound_cycles_param(ty, target_names) {
597                continue;
598            }
599            if !upper_bounds.contains(&ty) {
600                upper_bounds.push(ty);
601            }
602        }
603    }
604
605    fn collect_type_params(
606        &self,
607        ty: TypeId,
608        params: &mut FxHashSet<Atom>,
609        visited: &mut FxHashSet<TypeId>,
610    ) {
611        if !visited.insert(ty) {
612            return;
613        }
614        let Some(key) = self.interner.lookup(ty) else {
615            return;
616        };
617
618        match key {
619            TypeData::TypeParameter(info) | TypeData::Infer(info) => {
620                params.insert(info.name);
621            }
622            TypeData::Array(elem) => {
623                self.collect_type_params(elem, params, visited);
624            }
625            TypeData::Tuple(elements) => {
626                let elements = self.interner.tuple_list(elements);
627                for element in elements.iter() {
628                    self.collect_type_params(element.type_id, params, visited);
629                }
630            }
631            TypeData::Union(members) | TypeData::Intersection(members) => {
632                let members = self.interner.type_list(members);
633                for &member in members.iter() {
634                    self.collect_type_params(member, params, visited);
635                }
636            }
637            TypeData::Object(shape_id) => {
638                let shape = self.interner.object_shape(shape_id);
639                for prop in &shape.properties {
640                    self.collect_type_params(prop.type_id, params, visited);
641                }
642            }
643            TypeData::ObjectWithIndex(shape_id) => {
644                let shape = self.interner.object_shape(shape_id);
645                for prop in &shape.properties {
646                    self.collect_type_params(prop.type_id, params, visited);
647                }
648                if let Some(index) = shape.string_index.as_ref() {
649                    self.collect_type_params(index.key_type, params, visited);
650                    self.collect_type_params(index.value_type, params, visited);
651                }
652                if let Some(index) = shape.number_index.as_ref() {
653                    self.collect_type_params(index.key_type, params, visited);
654                    self.collect_type_params(index.value_type, params, visited);
655                }
656            }
657            TypeData::Application(app_id) => {
658                let app = self.interner.type_application(app_id);
659                self.collect_type_params(app.base, params, visited);
660                for &arg in &app.args {
661                    self.collect_type_params(arg, params, visited);
662                }
663            }
664            TypeData::Function(shape_id) => {
665                let shape = self.interner.function_shape(shape_id);
666                for param in &shape.params {
667                    self.collect_type_params(param.type_id, params, visited);
668                }
669                if let Some(this_type) = shape.this_type {
670                    self.collect_type_params(this_type, params, visited);
671                }
672                self.collect_type_params(shape.return_type, params, visited);
673            }
674            TypeData::Callable(shape_id) => {
675                let shape = self.interner.callable_shape(shape_id);
676                for sig in &shape.call_signatures {
677                    for param in &sig.params {
678                        self.collect_type_params(param.type_id, params, visited);
679                    }
680                    if let Some(this_type) = sig.this_type {
681                        self.collect_type_params(this_type, params, visited);
682                    }
683                    self.collect_type_params(sig.return_type, params, visited);
684                }
685                for sig in &shape.construct_signatures {
686                    for param in &sig.params {
687                        self.collect_type_params(param.type_id, params, visited);
688                    }
689                    if let Some(this_type) = sig.this_type {
690                        self.collect_type_params(this_type, params, visited);
691                    }
692                    self.collect_type_params(sig.return_type, params, visited);
693                }
694                for prop in &shape.properties {
695                    self.collect_type_params(prop.type_id, params, visited);
696                }
697            }
698            TypeData::Conditional(cond_id) => {
699                let cond = self.interner.conditional_type(cond_id);
700                self.collect_type_params(cond.check_type, params, visited);
701                self.collect_type_params(cond.extends_type, params, visited);
702                self.collect_type_params(cond.true_type, params, visited);
703                self.collect_type_params(cond.false_type, params, visited);
704            }
705            TypeData::Mapped(mapped_id) => {
706                let mapped = self.interner.mapped_type(mapped_id);
707                self.collect_type_params(mapped.constraint, params, visited);
708                if let Some(name_type) = mapped.name_type {
709                    self.collect_type_params(name_type, params, visited);
710                }
711                self.collect_type_params(mapped.template, params, visited);
712            }
713            TypeData::IndexAccess(obj, idx) => {
714                self.collect_type_params(obj, params, visited);
715                self.collect_type_params(idx, params, visited);
716            }
717            TypeData::KeyOf(operand) | TypeData::ReadonlyType(operand) => {
718                self.collect_type_params(operand, params, visited);
719            }
720            TypeData::TemplateLiteral(spans) => {
721                let spans = self.interner.template_list(spans);
722                for span in spans.iter() {
723                    if let TemplateSpan::Type(inner) = span {
724                        self.collect_type_params(*inner, params, visited);
725                    }
726                }
727            }
728            TypeData::StringIntrinsic { type_arg, .. } => {
729                self.collect_type_params(type_arg, params, visited);
730            }
731            TypeData::Enum(_def_id, member_type) => {
732                // Recurse into the structural member type
733                self.collect_type_params(member_type, params, visited);
734            }
735            TypeData::Intrinsic(_)
736            | TypeData::Literal(_)
737            | TypeData::Lazy(_)
738            | TypeData::Recursive(_)
739            | TypeData::BoundParameter(_)
740            | TypeData::TypeQuery(_)
741            | TypeData::UniqueSymbol(_)
742            | TypeData::ThisType
743            | TypeData::ModuleNamespace(_)
744            | TypeData::Error => {}
745            TypeData::NoInfer(inner) => {
746                self.collect_type_params(inner, params, visited);
747            }
748        }
749    }
750
751    fn param_depends_on_targets(
752        &mut self,
753        name: Atom,
754        targets: &[Atom],
755        visited: &mut FxHashSet<Atom>,
756    ) -> bool {
757        if targets.contains(&name) {
758            return true;
759        }
760        if !visited.insert(name) {
761            return false;
762        }
763        let Some(var) = self.find_type_param(name) else {
764            return false;
765        };
766        let root = self.table.find(var);
767        let upper_bounds = self.table.probe_value(root).upper_bounds;
768
769        for bound in upper_bounds {
770            for target in targets {
771                let mut seen = FxHashSet::default();
772                if self.type_contains_param(bound, *target, &mut seen) {
773                    return true;
774                }
775            }
776            if let Some(TypeData::TypeParameter(info)) = self.interner.lookup(bound)
777                && self.param_depends_on_targets(info.name, targets, visited)
778            {
779                return true;
780            }
781        }
782
783        false
784    }
785
786    fn type_contains_param(
787        &self,
788        ty: TypeId,
789        target: Atom,
790        visited: &mut FxHashSet<TypeId>,
791    ) -> bool {
792        if !visited.insert(ty) {
793            return false;
794        }
795
796        let key = match self.interner.lookup(ty) {
797            Some(key) => key,
798            None => return false,
799        };
800
801        match key {
802            TypeData::TypeParameter(info) | TypeData::Infer(info) => info.name == target,
803            TypeData::Array(elem) => self.type_contains_param(elem, target, visited),
804            TypeData::Tuple(elements) => {
805                let elements = self.interner.tuple_list(elements);
806                elements
807                    .iter()
808                    .any(|e| self.type_contains_param(e.type_id, target, visited))
809            }
810            TypeData::Union(members) | TypeData::Intersection(members) => {
811                let members = self.interner.type_list(members);
812                members
813                    .iter()
814                    .any(|&member| self.type_contains_param(member, target, visited))
815            }
816            TypeData::Object(shape_id) => {
817                let shape = self.interner.object_shape(shape_id);
818                shape
819                    .properties
820                    .iter()
821                    .any(|p| self.type_contains_param(p.type_id, target, visited))
822            }
823            TypeData::ObjectWithIndex(shape_id) => {
824                let shape = self.interner.object_shape(shape_id);
825                shape
826                    .properties
827                    .iter()
828                    .any(|p| self.type_contains_param(p.type_id, target, visited))
829                    || shape.string_index.as_ref().is_some_and(|idx| {
830                        self.type_contains_param(idx.key_type, target, visited)
831                            || self.type_contains_param(idx.value_type, target, visited)
832                    })
833                    || shape.number_index.as_ref().is_some_and(|idx| {
834                        self.type_contains_param(idx.key_type, target, visited)
835                            || self.type_contains_param(idx.value_type, target, visited)
836                    })
837            }
838            TypeData::Application(app_id) => {
839                let app = self.interner.type_application(app_id);
840                self.type_contains_param(app.base, target, visited)
841                    || app
842                        .args
843                        .iter()
844                        .any(|&arg| self.type_contains_param(arg, target, visited))
845            }
846            TypeData::Function(shape_id) => {
847                let shape = self.interner.function_shape(shape_id);
848                if shape.type_params.iter().any(|tp| tp.name == target) {
849                    return false;
850                }
851                shape
852                    .this_type
853                    .is_some_and(|this_type| self.type_contains_param(this_type, target, visited))
854                    || shape
855                        .params
856                        .iter()
857                        .any(|p| self.type_contains_param(p.type_id, target, visited))
858                    || self.type_contains_param(shape.return_type, target, visited)
859            }
860            TypeData::Callable(shape_id) => {
861                let shape = self.interner.callable_shape(shape_id);
862                let in_call = shape.call_signatures.iter().any(|sig| {
863                    if sig.type_params.iter().any(|tp| tp.name == target) {
864                        false
865                    } else {
866                        sig.this_type.is_some_and(|this_type| {
867                            self.type_contains_param(this_type, target, visited)
868                        }) || sig
869                            .params
870                            .iter()
871                            .any(|p| self.type_contains_param(p.type_id, target, visited))
872                            || self.type_contains_param(sig.return_type, target, visited)
873                    }
874                });
875                if in_call {
876                    return true;
877                }
878                let in_construct = shape.construct_signatures.iter().any(|sig| {
879                    if sig.type_params.iter().any(|tp| tp.name == target) {
880                        false
881                    } else {
882                        sig.this_type.is_some_and(|this_type| {
883                            self.type_contains_param(this_type, target, visited)
884                        }) || sig
885                            .params
886                            .iter()
887                            .any(|p| self.type_contains_param(p.type_id, target, visited))
888                            || self.type_contains_param(sig.return_type, target, visited)
889                    }
890                });
891                if in_construct {
892                    return true;
893                }
894                shape
895                    .properties
896                    .iter()
897                    .any(|p| self.type_contains_param(p.type_id, target, visited))
898            }
899            TypeData::Conditional(cond_id) => {
900                let cond = self.interner.conditional_type(cond_id);
901                self.type_contains_param(cond.check_type, target, visited)
902                    || self.type_contains_param(cond.extends_type, target, visited)
903                    || self.type_contains_param(cond.true_type, target, visited)
904                    || self.type_contains_param(cond.false_type, target, visited)
905            }
906            TypeData::Mapped(mapped_id) => {
907                let mapped = self.interner.mapped_type(mapped_id);
908                if mapped.type_param.name == target {
909                    return false;
910                }
911                self.type_contains_param(mapped.constraint, target, visited)
912                    || self.type_contains_param(mapped.template, target, visited)
913            }
914            TypeData::IndexAccess(obj, idx) => {
915                self.type_contains_param(obj, target, visited)
916                    || self.type_contains_param(idx, target, visited)
917            }
918            TypeData::KeyOf(operand) | TypeData::ReadonlyType(operand) => {
919                self.type_contains_param(operand, target, visited)
920            }
921            TypeData::TemplateLiteral(spans) => {
922                let spans = self.interner.template_list(spans);
923                spans.iter().any(|span| match span {
924                    TemplateSpan::Text(_) => false,
925                    TemplateSpan::Type(inner) => self.type_contains_param(*inner, target, visited),
926                })
927            }
928            TypeData::StringIntrinsic { type_arg, .. } => {
929                self.type_contains_param(type_arg, target, visited)
930            }
931            TypeData::Enum(_def_id, member_type) => {
932                // Recurse into the structural member type
933                self.type_contains_param(member_type, target, visited)
934            }
935            TypeData::Intrinsic(_)
936            | TypeData::Literal(_)
937            | TypeData::Lazy(_)
938            | TypeData::Recursive(_)
939            | TypeData::BoundParameter(_)
940            | TypeData::TypeQuery(_)
941            | TypeData::UniqueSymbol(_)
942            | TypeData::ThisType
943            | TypeData::ModuleNamespace(_)
944            | TypeData::Error => false,
945            TypeData::NoInfer(inner) => self.type_contains_param(inner, target, visited),
946        }
947    }
948
949    /// Resolve all type parameters to concrete types
950    pub fn resolve_all(&mut self) -> Result<Vec<(Atom, TypeId)>, InferenceError> {
951        // Clone type_params to avoid borrow conflict
952        let type_params: Vec<_> = self.type_params.clone();
953        let mut results = Vec::new();
954        for (name, var, _) in type_params {
955            match self.probe(var) {
956                Some(ty) => results.push((name, ty)),
957                None => return Err(InferenceError::Unresolved(var)),
958            }
959        }
960        Ok(results)
961    }
962
963    /// Get the interner reference
964    pub fn interner(&self) -> &dyn TypeDatabase {
965        self.interner
966    }
967
968    // =========================================================================
969    // Constraint Collection
970    // =========================================================================
971
972    /// Add a lower bound constraint: ty <: var
973    /// This is used when an argument type flows into a type parameter.
974    /// Updated to use `NakedTypeVariable` (highest priority) for direct argument inference.
975    pub fn add_lower_bound(&mut self, var: InferenceVar, ty: TypeId) {
976        self.add_candidate(var, ty, InferencePriority::NakedTypeVariable);
977    }
978
979    /// Add an inference candidate for a variable.
980    pub fn add_candidate(&mut self, var: InferenceVar, ty: TypeId, priority: InferencePriority) {
981        self.add_candidate_with_context(var, ty, priority, false, None, None);
982    }
983
984    /// Add an inference candidate for a variable that originates from object property inference.
985    /// Object-property candidates are tracked so the resolver can apply tighter union handling
986    /// for repeated property positions (e.g. `{ bar: T; baz: T }`).
987    pub fn add_property_candidate(
988        &mut self,
989        var: InferenceVar,
990        ty: TypeId,
991        priority: InferencePriority,
992    ) {
993        self.add_candidate_with_context(var, ty, priority, true, None, None);
994    }
995
996    /// Add an inference candidate for a variable that originates from an object property.
997    /// `object_property_index` captures the source property order and enables deterministic
998    /// tie-breaking when repeated property candidates collapse to a union.
999    pub fn add_property_candidate_with_index(
1000        &mut self,
1001        var: InferenceVar,
1002        ty: TypeId,
1003        priority: InferencePriority,
1004        object_property_index: u32,
1005        object_property_name: Option<Atom>,
1006    ) {
1007        self.add_candidate_with_context(
1008            var,
1009            ty,
1010            priority,
1011            true,
1012            Some(object_property_index),
1013            object_property_name,
1014        );
1015    }
1016
1017    fn add_candidate_with_context(
1018        &mut self,
1019        var: InferenceVar,
1020        ty: TypeId,
1021        priority: InferencePriority,
1022        from_object_property: bool,
1023        object_property_index: Option<u32>,
1024        object_property_name: Option<Atom>,
1025    ) {
1026        let root = self.table.find(var);
1027        let candidate = InferenceCandidate {
1028            type_id: ty,
1029            priority,
1030            is_fresh_literal: is_literal_type(self.interner, ty),
1031            from_object_property,
1032            object_property_index,
1033            object_property_name,
1034        };
1035        self.table.union_value(
1036            root,
1037            InferenceInfo {
1038                candidates: vec![candidate],
1039                ..InferenceInfo::default()
1040            },
1041        );
1042    }
1043
1044    /// Add an upper bound constraint: var <: ty
1045    /// This is used for `extends` constraints on type parameters.
1046    pub fn add_upper_bound(&mut self, var: InferenceVar, ty: TypeId) {
1047        let root = self.table.find(var);
1048        self.table.union_value(
1049            root,
1050            InferenceInfo {
1051                upper_bounds: vec![ty],
1052                ..InferenceInfo::default()
1053            },
1054        );
1055    }
1056
1057    /// Get the constraints for a variable
1058    pub fn get_constraints(&mut self, var: InferenceVar) -> Option<ConstraintSet> {
1059        let root = self.table.find(var);
1060        let info = self.table.probe_value(root);
1061        if info.is_empty() {
1062            None
1063        } else {
1064            Some(ConstraintSet::from_info(&info))
1065        }
1066    }
1067
1068    /// Check if all inference candidates for a variable have `ReturnType` priority.
1069    /// This indicates the type was inferred from callback return types (Round 2),
1070    /// not from direct arguments (Round 1).
1071    pub fn all_candidates_are_return_type(&mut self, var: InferenceVar) -> bool {
1072        let root = self.table.find(var);
1073        let info = self.table.probe_value(root);
1074        !info.candidates.is_empty()
1075            && info
1076                .candidates
1077                .iter()
1078                .all(|c| c.priority == InferencePriority::ReturnType)
1079    }
1080
1081    /// Get the original un-widened literal candidate types for an inference variable.
1082    pub fn get_literal_candidates(&mut self, var: InferenceVar) -> Vec<TypeId> {
1083        let root = self.table.find(var);
1084        let info = self.table.probe_value(root);
1085        info.candidates
1086            .iter()
1087            .filter(|c| c.is_fresh_literal)
1088            .map(|c| c.type_id)
1089            .collect()
1090    }
1091
1092    /// Collect a constraint from an assignment: source flows into target
1093    /// If target is an inference variable, source becomes a lower bound.
1094    /// If source is an inference variable, target becomes an upper bound.
1095    pub const fn collect_constraint(&mut self, _source: TypeId, _target: TypeId) {
1096        // Check if target is an inference variable (via TypeData lookup)
1097        // For now, we rely on the caller to call add_lower_bound/add_upper_bound directly
1098        // This is a placeholder for more sophisticated constraint collection
1099    }
1100}
1101
1102// DISABLED: Tests use deprecated add_candidate / resolve_with_constraints API
1103// The inference system has been refactored to use unification-based inference.
1104#[cfg(test)]
1105#[path = "../../tests/infer_tests.rs"]
1106mod tests;