Skip to main content

tsz_solver/
subtype.rs

1//! Structural subtype checking.
2//!
3//! This module implements the core logic engine for TypeScript's structural
4//! subtyping. It uses coinductive semantics to handle recursive types.
5//!
6//! Key features:
7//! - O(1) equality check via `TypeId` comparison
8//! - Cycle detection for recursive types (coinductive)
9//! - Set-theoretic operations for unions and intersections
10//! - `TypeResolver` trait for lazy symbol resolution
11//! - Tracer pattern for zero-cost diagnostic abstraction
12
13use crate::AssignabilityChecker;
14use crate::TypeDatabase;
15use crate::db::QueryDatabase;
16use crate::def::DefId;
17use crate::diagnostics::{DynSubtypeTracer, SubtypeFailureReason};
18#[cfg(test)]
19use crate::types::*;
20use crate::types::{
21    IntrinsicKind, LiteralValue, ObjectFlags, ObjectShape, SymbolRef, TypeData, TypeId, TypeListId,
22};
23use crate::visitor::{
24    TypeVisitor, application_id, array_element_type, callable_shape_id, conditional_type_id,
25    enum_components, function_shape_id, intersection_list_id, intrinsic_kind, is_this_type,
26    keyof_inner_type, lazy_def_id, literal_value, mapped_type_id, object_shape_id,
27    object_with_index_shape_id, readonly_inner_type, ref_symbol, template_literal_id,
28    tuple_list_id, type_param_info, type_query_symbol, union_list_id, unique_symbol_ref,
29};
30use rustc_hash::{FxHashMap, FxHashSet};
31use tsz_common::limits;
32
33/// Maximum recursion depth for subtype checking.
34/// This prevents OOM/stack overflow from infinitely expanding recursive types.
35/// Examples: `interface AA<T extends AA<T>>`, `interface List<T> { next: List<T> }`
36pub(crate) const MAX_SUBTYPE_DEPTH: u32 = limits::MAX_SUBTYPE_DEPTH;
37pub(crate) const INTERSECTION_OBJECT_FAST_PATH_THRESHOLD: usize = 8;
38
39/// Result of a subtype check
40#[derive(Copy, Clone, Debug, PartialEq, Eq)]
41pub enum SubtypeResult {
42    /// The relationship is definitely true
43    True,
44    /// The relationship is definitely false
45    False,
46    /// We're in a valid cycle (coinductive recursion)
47    ///
48    /// This represents finite/cyclic recursion like `interface List { next: List }`.
49    /// The type graph forms a closed loop, which is valid in TypeScript.
50    CycleDetected,
51    /// We've exceeded the recursion depth limit
52    ///
53    /// This represents expansive recursion that grows indefinitely like
54    /// `type T<X> = T<Box<X>>`. TypeScript rejects these as "excessively deep".
55    ///
56    /// This is treated as `false` for soundness - if we can't prove subtyping within
57    /// reasonable limits, we reject the relationship rather than accepting unsoundly.
58    DepthExceeded,
59}
60
61impl SubtypeResult {
62    pub const fn is_true(self) -> bool {
63        matches!(self, Self::True | Self::CycleDetected)
64    }
65
66    pub const fn is_false(self) -> bool {
67        matches!(self, Self::False)
68    }
69}
70
71/// Returns true for unit types where `source != target` implies disjointness.
72///
73/// This intentionally excludes:
74/// - null/undefined/void/never (special-cased assignability semantics)
75/// - Tuples (labeled tuples like [a: 1] vs [b: 1] are compatible despite different `TypeIds`)
76///
77/// Only safe for primitives where identity implies structural equality.
78fn is_disjoint_unit_type(types: &dyn TypeDatabase, ty: TypeId) -> bool {
79    match types.lookup(ty) {
80        Some(TypeData::Literal(_) | TypeData::UniqueSymbol(_)) => true,
81        // Note: Tuples removed to avoid labeled tuple bug
82        // TypeScript treats [a: 1] and [b: 1] as compatible even though they have different TypeIds
83        _ => false,
84    }
85}
86
87/// Controls how `any` is treated during subtype checks.
88#[derive(Copy, Clone, Debug, PartialEq, Eq)]
89pub enum AnyPropagationMode {
90    /// `any` is treated as top/bottom everywhere (TypeScript default).
91    All,
92    /// `any` is treated as top/bottom only at the top-level comparison.
93    TopLevelOnly,
94}
95
96impl AnyPropagationMode {
97    #[inline]
98    pub(crate) const fn allows_any_at_depth(self, depth: u32) -> bool {
99        match self {
100            Self::All => true,
101            Self::TopLevelOnly => depth == 0,
102        }
103    }
104}
105
106// TypeResolver, NoopResolver, and TypeEnvironment are defined in type_resolver.rs
107pub use crate::type_resolver::{NoopResolver, TypeEnvironment, TypeResolver};
108
109// SubtypeVisitor is defined in subtype_visitor.rs
110pub use crate::subtype_visitor::SubtypeVisitor;
111
112/// Subtype checking context.
113/// Maintains the "seen" set for cycle detection.
114pub struct SubtypeChecker<'a, R: TypeResolver = NoopResolver> {
115    pub(crate) interner: &'a dyn TypeDatabase,
116    /// Optional query database for Salsa-backed memoization.
117    /// When set, routes `evaluate_type` and `is_subtype_of` through Salsa.
118    pub(crate) query_db: Option<&'a dyn QueryDatabase>,
119    pub(crate) resolver: &'a R,
120    /// Unified recursion guard for TypeId-pair cycle detection, depth, and iteration limits.
121    pub(crate) guard: crate::recursion::RecursionGuard<(TypeId, TypeId)>,
122    /// Active `SymbolRef` pairs being checked (for DefId-level cycle detection)
123    /// This catches cycles in Ref types before they're resolved, preventing
124    /// infinite expansion of recursive type aliases and interfaces.
125    pub(crate) seen_refs: FxHashSet<(SymbolRef, SymbolRef)>,
126    /// Unified recursion guard for DefId-pair cycle detection.
127    /// Catches cycles in Lazy(DefId) types before they're resolved.
128    pub(crate) def_guard: crate::recursion::RecursionGuard<(DefId, DefId)>,
129    /// Symbol-pair visiting set for Object-level cycle detection.
130    /// Catches cycles when comparing evaluated Object types with symbols
131    /// (e.g., `Promise<X>` vs `PromiseLike<Y>`) where `DefId` information is lost
132    /// after type evaluation. Without this, recursive interfaces like `Promise`
133    /// cause infinite expansion when comparing `then` method return types.
134    sym_visiting: FxHashSet<(tsz_binder::SymbolId, tsz_binder::SymbolId)>,
135    /// Whether to use strict function types (contravariant parameters).
136    /// Default: true (sound, correct behavior)
137    pub strict_function_types: bool,
138    /// Whether to allow any return type when the target return is void.
139    pub allow_void_return: bool,
140    /// Whether rest parameters of any/unknown should be treated as bivariant.
141    /// See <https://github.com/microsoft/TypeScript/issues/20007>.
142    pub allow_bivariant_rest: bool,
143    /// When true, skip the `evaluate_type()` call in `check_subtype`.
144    /// This prevents infinite recursion when `TypeEvaluator` calls `SubtypeChecker`
145    /// for simplification, since `TypeEvaluator` has already evaluated the types.
146    pub bypass_evaluation: bool,
147    /// Maximum recursion depth for subtype checking.
148    /// Used by `TypeEvaluator` simplification to prevent stack overflow.
149    /// Default: `MAX_SUBTYPE_DEPTH` (100)
150    pub max_depth: u32,
151    /// Whether required parameter count mismatches are allowed for bivariant methods.
152    pub allow_bivariant_param_count: bool,
153    /// Whether optional properties are exact (exclude implicit `undefined`).
154    /// Default: false (legacy TS behavior).
155    pub exact_optional_property_types: bool,
156    /// Whether null/undefined are treated as separate types.
157    /// Default: true (strict null checks).
158    pub strict_null_checks: bool,
159    /// Whether indexed access includes `undefined`.
160    /// Default: false (legacy TS behavior).
161    pub no_unchecked_indexed_access: bool,
162    // When true, disables method bivariance (methods use contravariance).
163    // Default: false (methods are bivariant in TypeScript for compatibility).
164    pub disable_method_bivariance: bool,
165    /// Optional inheritance graph for O(1) nominal class subtype checking.
166    /// When provided, enables fast nominal checks for class inheritance.
167    pub inheritance_graph: Option<&'a crate::inheritance::InheritanceGraph>,
168    /// Optional callback to check if a symbol is a class (for nominal subtyping).
169    /// Returns true if the symbol has the CLASS flag set.
170    pub is_class_symbol: Option<&'a dyn Fn(SymbolRef) -> bool>,
171    /// Controls how `any` is treated during subtype checks.
172    pub any_propagation: AnyPropagationMode,
173    /// Cache for `evaluate_type` results within this `SubtypeChecker`'s lifetime.
174    /// This prevents O(n²) behavior when the same type (e.g., a large union) is
175    /// evaluated multiple times across different subtype checks.
176    /// Key is (`TypeId`, `no_unchecked_indexed_access`) since that flag affects evaluation.
177    pub(crate) eval_cache: FxHashMap<(TypeId, bool), TypeId>,
178    /// Optional tracer for collecting subtype failure diagnostics.
179    /// When `Some`, enables detailed failure reason collection for error messages.
180    /// When `None`, disables tracing for maximum performance (default).
181    pub tracer: Option<&'a mut dyn DynSubtypeTracer>,
182}
183
184impl<'a> SubtypeChecker<'a, NoopResolver> {
185    /// Create a new `SubtypeChecker` without a resolver (basic mode).
186    pub fn new(interner: &'a dyn TypeDatabase) -> Self {
187        static NOOP: NoopResolver = NoopResolver;
188        SubtypeChecker {
189            interner,
190            query_db: None,
191            resolver: &NOOP,
192            guard: crate::recursion::RecursionGuard::with_profile(
193                crate::recursion::RecursionProfile::SubtypeCheck,
194            ),
195            seen_refs: FxHashSet::default(),
196            def_guard: crate::recursion::RecursionGuard::with_profile(
197                crate::recursion::RecursionProfile::SubtypeCheck,
198            ),
199            sym_visiting: FxHashSet::default(),
200            strict_function_types: true, // Default to strict (sound) behavior
201            allow_void_return: false,
202            allow_bivariant_rest: false,
203            allow_bivariant_param_count: false,
204            exact_optional_property_types: false,
205            strict_null_checks: true,
206            no_unchecked_indexed_access: false,
207            disable_method_bivariance: false,
208            inheritance_graph: None,
209            is_class_symbol: None,
210            any_propagation: AnyPropagationMode::All,
211            bypass_evaluation: false,
212            max_depth: MAX_SUBTYPE_DEPTH,
213            eval_cache: FxHashMap::default(),
214            tracer: None,
215        }
216    }
217}
218
219impl<'a, R: TypeResolver> SubtypeChecker<'a, R> {
220    /// Create a new `SubtypeChecker` with a custom resolver.
221    pub fn with_resolver(interner: &'a dyn TypeDatabase, resolver: &'a R) -> Self {
222        SubtypeChecker {
223            interner,
224            query_db: None,
225            resolver,
226            guard: crate::recursion::RecursionGuard::with_profile(
227                crate::recursion::RecursionProfile::SubtypeCheck,
228            ),
229            seen_refs: FxHashSet::default(),
230            def_guard: crate::recursion::RecursionGuard::with_profile(
231                crate::recursion::RecursionProfile::SubtypeCheck,
232            ),
233            sym_visiting: FxHashSet::default(),
234            strict_function_types: true,
235            allow_void_return: false,
236            allow_bivariant_rest: false,
237            allow_bivariant_param_count: false,
238            exact_optional_property_types: false,
239            strict_null_checks: true,
240            no_unchecked_indexed_access: false,
241            disable_method_bivariance: false,
242            inheritance_graph: None,
243            is_class_symbol: None,
244            any_propagation: AnyPropagationMode::All,
245            bypass_evaluation: false,
246            max_depth: MAX_SUBTYPE_DEPTH,
247            eval_cache: FxHashMap::default(),
248            tracer: None,
249        }
250    }
251
252    /// Set the inheritance graph for O(1) nominal class subtype checking.
253    pub const fn with_inheritance_graph(
254        mut self,
255        graph: &'a crate::inheritance::InheritanceGraph,
256    ) -> Self {
257        self.inheritance_graph = Some(graph);
258        self
259    }
260
261    /// Set the callback to check if a symbol is a class.
262    pub fn with_class_check(mut self, check: &'a dyn Fn(SymbolRef) -> bool) -> Self {
263        self.is_class_symbol = Some(check);
264        self
265    }
266
267    /// Configure how `any` is treated during subtype checks.
268    pub const fn with_any_propagation_mode(mut self, mode: AnyPropagationMode) -> Self {
269        self.any_propagation = mode;
270        self
271    }
272
273    /// Set the tracer for collecting subtype failure diagnostics.
274    /// When set, enables detailed failure reason collection for error messages.
275    pub fn with_tracer(mut self, tracer: &'a mut dyn DynSubtypeTracer) -> Self {
276        self.tracer = Some(tracer);
277        self
278    }
279
280    /// Set the query database for Salsa-backed memoization.
281    /// When set, routes `evaluate_type` and `is_subtype_of` through Salsa.
282    pub fn with_query_db(mut self, db: &'a dyn QueryDatabase) -> Self {
283        self.query_db = Some(db);
284        self
285    }
286
287    /// Set whether strict null checks are enabled.
288    /// When false, null and undefined are assignable to any type.
289    pub const fn with_strict_null_checks(mut self, strict_null_checks: bool) -> Self {
290        self.strict_null_checks = strict_null_checks;
291        self
292    }
293
294    /// Reset per-check state so this checker can be reused for another subtype check.
295    ///
296    /// This clears cycle detection sets and counters while preserving configuration
297    /// (`strict_function_types`, `allow_void_return`, etc.) and borrowed references
298    /// (interner, resolver, `inheritance_graph`, etc.).
299    ///
300    /// Uses `.clear()` instead of re-allocating, so hash set memory is reused.
301    #[inline]
302    pub fn reset(&mut self) {
303        self.guard.reset();
304        self.seen_refs.clear();
305        self.def_guard.reset();
306        self.sym_visiting.clear();
307        self.eval_cache.clear();
308    }
309
310    /// Whether the recursion depth was exceeded during subtype checking.
311    pub const fn depth_exceeded(&self) -> bool {
312        self.guard.is_exceeded()
313    }
314
315    /// Apply compiler flags from a packed u16 bitmask.
316    ///
317    /// This unpacks the flags used by `RelationCacheKey` and applies them to the checker.
318    /// The bit layout matches the cache key definition in types.rs:
319    /// - bit 0: `strict_null_checks`
320    /// - bit 1: `strict_function_types`
321    /// - bit 2: `exact_optional_property_types`
322    /// - bit 3: `no_unchecked_indexed_access`
323    /// - bit 4: `disable_method_bivariance`
324    /// - bit 5: `allow_void_return`
325    /// - bit 6: `allow_bivariant_rest`
326    /// - bit 7: `allow_bivariant_param_count`
327    pub(crate) const fn apply_flags(mut self, flags: u16) -> Self {
328        self.strict_null_checks = (flags & (1 << 0)) != 0;
329        self.strict_function_types = (flags & (1 << 1)) != 0;
330        self.exact_optional_property_types = (flags & (1 << 2)) != 0;
331        self.no_unchecked_indexed_access = (flags & (1 << 3)) != 0;
332        self.disable_method_bivariance = (flags & (1 << 4)) != 0;
333        self.allow_void_return = (flags & (1 << 5)) != 0;
334        self.allow_bivariant_rest = (flags & (1 << 6)) != 0;
335        self.allow_bivariant_param_count = (flags & (1 << 7)) != 0;
336        self
337    }
338
339    pub(crate) fn resolve_ref_type(&self, type_id: TypeId) -> TypeId {
340        // Handle DefId-based Lazy types (new API)
341        if let Some(def_id) = lazy_def_id(self.interner, type_id) {
342            return self
343                .resolver
344                .resolve_lazy(def_id, self.interner)
345                .unwrap_or(type_id);
346        }
347
348        // Handle legacy SymbolRef-based types (old API)
349        if let Some(symbol) = ref_symbol(self.interner, type_id) {
350            self.resolver
351                .resolve_symbol_ref(symbol, self.interner)
352                .unwrap_or(type_id)
353        } else {
354            type_id
355        }
356    }
357
358    pub(crate) fn resolve_lazy_type(&self, type_id: TypeId) -> TypeId {
359        if let Some(def_id) = lazy_def_id(self.interner, type_id) {
360            self.resolver
361                .resolve_lazy(def_id, self.interner)
362                .unwrap_or(type_id)
363        } else {
364            type_id
365        }
366    }
367
368    /// When a cycle is detected, we return `CycleDetected` (coinductive semantics)
369    /// which implements greatest fixed point semantics - the correct behavior for
370    /// recursive type checking. When depth/iteration limits are exceeded, we return
371    /// `DepthExceeded` (conservative false) for soundness.
372    pub fn check_subtype(&mut self, source: TypeId, target: TypeId) -> SubtypeResult {
373        // =========================================================================
374        // Fast paths (no cycle tracking needed)
375        // =========================================================================
376
377        let allow_any = self.any_propagation.allows_any_at_depth(self.guard.depth());
378        let mut source = source;
379        let mut target = target;
380        if !allow_any {
381            if source == TypeId::ANY {
382                // In strict mode, any doesn't match everything structurally.
383                // We demote it to STRICT_ANY so it only matches top types or itself.
384                source = TypeId::STRICT_ANY;
385            }
386            if target == TypeId::ANY {
387                target = TypeId::STRICT_ANY;
388            }
389        }
390
391        // Same type is always a subtype of itself
392        if source == target {
393            return SubtypeResult::True;
394        }
395
396        // Task #54: Structural Identity Fast-Path (O(1) after canonicalization)
397        // Check if source and target canonicalize to the same TypeId, which means
398        // they are structurally identical. This avoids expensive structural walks
399        // for types that are the same structure but were interned separately.
400        //
401        // Guarded by bypass_evaluation to prevent infinite recursion when called
402        // from TypeEvaluator during simplification (evaluation has already been done).
403        if !self.bypass_evaluation
404            && let Some(db) = self.query_db
405        {
406            let source_canon = db.canonical_id(source);
407            let target_canon = db.canonical_id(target);
408            if source_canon == target_canon {
409                return SubtypeResult::True;
410            }
411        }
412
413        // Any is assignable to anything (when allowed)
414        if allow_any && (source == TypeId::ANY || source == TypeId::STRICT_ANY) {
415            return SubtypeResult::True;
416        }
417
418        // Everything is assignable to any (when allowed)
419        if allow_any && (target == TypeId::ANY || target == TypeId::STRICT_ANY) {
420            return SubtypeResult::True;
421        }
422
423        // If not allowing any (nested strict any), any still matches Top types as source,
424        // but any as target ALWAYS matches (it's a top type).
425        if !allow_any
426            && (source == TypeId::ANY || source == TypeId::STRICT_ANY)
427            && (target == TypeId::ANY || target == TypeId::STRICT_ANY || target == TypeId::UNKNOWN)
428        {
429            return SubtypeResult::True;
430        }
431        // Fall through to structural check (which will fail for STRICT_ANY)
432        if !allow_any && (target == TypeId::ANY || target == TypeId::STRICT_ANY) {
433            return SubtypeResult::True;
434        }
435        // Fall through to structural check (which will fail for STRICT_ANY)
436
437        // Everything is assignable to unknown
438        if target == TypeId::UNKNOWN {
439            return SubtypeResult::True;
440        }
441
442        // Never is assignable to everything
443        if source == TypeId::NEVER {
444            return SubtypeResult::True;
445        }
446
447        // Error types are assignable to/from everything (like `any` in tsc).
448        // This prevents cascading diagnostics when type resolution fails.
449        if source == TypeId::ERROR || target == TypeId::ERROR {
450            return SubtypeResult::True;
451        }
452
453        // Fast path: distinct disjoint unit types are never subtypes.
454        // This avoids expensive structural checks for large unions of literals/enum members.
455        if is_disjoint_unit_type(self.interner, source)
456            && is_disjoint_unit_type(self.interner, target)
457        {
458            return SubtypeResult::False;
459        }
460
461        // =========================================================================
462        // Cross-checker memoization (QueryCache lookup)
463        // =========================================================================
464        // Check the shared cache for a previously computed result.
465        // This avoids re-doing expensive structural checks for type pairs
466        // already resolved by a prior SubtypeChecker instance.
467        if let Some(db) = self.query_db {
468            let key = self.make_cache_key(source, target);
469            if let Some(cached) = db.lookup_subtype_cache(key) {
470                return if cached {
471                    SubtypeResult::True
472                } else {
473                    SubtypeResult::False
474                };
475            }
476        }
477
478        // =========================================================================
479        // Cycle detection (coinduction) via RecursionGuard - BEFORE evaluation!
480        //
481        // RecursionGuard handles iteration limits, depth limits, cycle detection,
482        // and visiting set size limits in one call.
483        // =========================================================================
484
485        let pair = (source, target);
486
487        // Check reversed pair for bivariant cross-recursion detection.
488        if self.guard.is_visiting(&(target, source)) {
489            return SubtypeResult::CycleDetected;
490        }
491
492        use crate::recursion::RecursionResult;
493        match self.guard.enter(pair) {
494            RecursionResult::Cycle => return SubtypeResult::CycleDetected,
495            RecursionResult::DepthExceeded | RecursionResult::IterationExceeded => {
496                return SubtypeResult::DepthExceeded;
497            }
498            RecursionResult::Entered => {}
499        }
500
501        // =======================================================================
502        // DefId-level cycle detection (before evaluation!)
503        // Catches cycles in recursive type aliases BEFORE they expand.
504        //
505        // For non-Application types: extract DefId directly from Lazy/Enum.
506        // For Application types (e.g., List<T>): extract the BASE DefId from
507        // the Application's base type. This enables coinductive cycle detection
508        // for recursive generic interfaces like List<T> extends Sequence<T>
509        // where method return types create infinite expansion chains
510        // (e.g., List<Pair<T,S>> <: Seq<Pair<T,S>> → List<Pair<...>> <: ...).
511        //
512        // For Application types with the SAME base DefId (e.g., Array<number>
513        // vs Array<string>), we skip cycle detection because these are legitimate
514        // comparisons that should not be treated as cycles.
515        // =======================================================================
516
517        let extract_def_id =
518            |interner: &dyn crate::TypeDatabase, type_id: TypeId| -> Option<DefId> {
519                // First try direct Lazy/Enum DefId
520                if let Some(def) = lazy_def_id(interner, type_id) {
521                    return Some(def);
522                }
523                if let Some((def, _)) = enum_components(interner, type_id) {
524                    return Some(def);
525                }
526                // For Application types, extract the base DefId
527                if let Some(app_id) = application_id(interner, type_id) {
528                    let app = interner.type_application(app_id);
529                    if let Some(def) = lazy_def_id(interner, app.base) {
530                        return Some(def);
531                    }
532                }
533                None
534            };
535
536        let s_def_id = extract_def_id(self.interner, source);
537        let t_def_id = extract_def_id(self.interner, target);
538
539        let def_pair = if let (Some(s_def), Some(t_def)) = (s_def_id, t_def_id) {
540            // Skip same-base Application cycle detection to avoid false positives
541            // (e.g., Array<number> vs Array<string> share the same base)
542            if s_def == t_def
543                && application_id(self.interner, source).is_some()
544                && application_id(self.interner, target).is_some()
545            {
546                None
547            } else {
548                Some((s_def, t_def))
549            }
550        } else {
551            None
552        };
553
554        // =======================================================================
555        // Symbol-level cycle detection for cross-context DefId aliasing.
556        //
557        // The same interface (e.g., Promise) may get different DefIds in different
558        // checker contexts (lib vs user file). When comparing recursive generic
559        // interfaces, the DefId-level cycle detection can miss cycles because
560        // the inner comparison uses different DefIds than the outer one.
561        //
562        // Fix: resolve DefIds to their underlying SymbolIds (stored in
563        // DefinitionInfo). If a (SymbolId, SymbolId) pair is already being
564        // visited via a different DefId pair, treat it as a cycle.
565        // =======================================================================
566        if let (Some(s_def), Some(t_def)) = (s_def_id, t_def_id) {
567            let s_sym = self.resolver.def_to_symbol_id(s_def);
568            let t_sym = self.resolver.def_to_symbol_id(t_def);
569            if let (Some(s_sid), Some(t_sid)) = (s_sym, t_sym) {
570                // Check if any visiting DefId pair maps to the same SymbolId pair
571                if self.def_guard.is_visiting_any(|&(visiting_s, visiting_t)| {
572                    visiting_s != s_def
573                        && visiting_t != t_def
574                        && self.resolver.def_to_symbol_id(visiting_s) == Some(s_sid)
575                        && self.resolver.def_to_symbol_id(visiting_t) == Some(t_sid)
576                }) {
577                    self.guard.leave(pair);
578                    return SubtypeResult::CycleDetected;
579                }
580            }
581        }
582
583        let def_entered = if let Some((s_def, t_def)) = def_pair {
584            // Check reversed pair for bivariant cross-recursion
585            if self.def_guard.is_visiting(&(t_def, s_def)) {
586                self.guard.leave(pair);
587                return SubtypeResult::CycleDetected;
588            }
589            match self.def_guard.enter((s_def, t_def)) {
590                RecursionResult::Cycle => {
591                    self.guard.leave(pair);
592                    return SubtypeResult::CycleDetected;
593                }
594                RecursionResult::Entered => Some((s_def, t_def)),
595                _ => None,
596            }
597        } else {
598            None
599        };
600
601        // =========================================================================
602        // Pre-evaluation intrinsic checks
603        // =========================================================================
604        // Object interface: any non-nullable source is assignable.
605        // In TypeScript, the Object interface from lib.d.ts is the root of
606        // the prototype chain — all types except null/undefined/void are
607        // assignable to it. We must check BEFORE evaluate_type() because
608        // evaluation may change the target TypeId, losing the boxed identity.
609        {
610            let is_object_interface_target = self
611                .resolver
612                .is_boxed_type_id(target, IntrinsicKind::Object)
613                || self
614                    .resolver
615                    .get_boxed_type(IntrinsicKind::Object)
616                    .is_some_and(|boxed| boxed == target)
617                || lazy_def_id(self.interner, target).is_some_and(|def_id| {
618                    self.resolver.is_boxed_def_id(def_id, IntrinsicKind::Object)
619                });
620            if is_object_interface_target {
621                let is_nullable = matches!(source, TypeId::NULL | TypeId::UNDEFINED | TypeId::VOID);
622                if !is_nullable {
623                    let result = self.check_object_contract(source, target);
624                    if let Some(dp) = def_entered {
625                        self.def_guard.leave(dp);
626                    }
627                    self.guard.leave(pair);
628                    return result;
629                }
630            }
631        }
632
633        // Check if target is the Function interface from lib.d.ts.
634        // We must check BEFORE evaluate_type() because evaluation resolves
635        // Lazy(DefId) → ObjectShape, losing the DefId identity needed to
636        // recognize the type as an intrinsic interface.
637        if !self.bypass_evaluation
638            && (lazy_def_id(self.interner, target).is_some_and(|t_def| {
639                self.resolver
640                    .is_boxed_def_id(t_def, IntrinsicKind::Function)
641            }) || self
642                .resolver
643                .is_boxed_type_id(target, IntrinsicKind::Function))
644        {
645            let source_eval = self.evaluate_type(source);
646            if self.is_callable_type(source_eval) {
647                // North Star Fix: is_callable_type now respects allow_any correctly.
648                // If it returned true, it means either we're in permissive mode OR
649                // the source is genuinely a callable type.
650                if let Some(dp) = def_entered {
651                    self.def_guard.leave(dp);
652                }
653                self.guard.leave(pair);
654                return SubtypeResult::True;
655            }
656        }
657
658        // =========================================================================
659        // Meta-type evaluation (after cycle detection is set up)
660        // =========================================================================
661        let result = if self.bypass_evaluation {
662            if target == TypeId::NEVER {
663                SubtypeResult::False
664            } else {
665                self.check_subtype_inner(source, target)
666            }
667        } else {
668            let source_eval = self.evaluate_type(source);
669            let target_eval = self.evaluate_type(target);
670
671            if source_eval != source || target_eval != target {
672                self.check_subtype(source_eval, target_eval)
673            } else if target == TypeId::NEVER {
674                SubtypeResult::False
675            } else {
676                self.check_subtype_inner(source, target)
677            }
678        };
679
680        // Cleanup: leave both guards
681        if let Some(dp) = def_entered {
682            self.def_guard.leave(dp);
683        }
684        self.guard.leave(pair);
685
686        // Cache definitive results for cross-checker memoization.
687        if let Some(db) = self.query_db {
688            let key = self.make_cache_key(source, target);
689            match result {
690                SubtypeResult::True => db.insert_subtype_cache(key, true),
691                SubtypeResult::False => db.insert_subtype_cache(key, false),
692                SubtypeResult::CycleDetected | SubtypeResult::DepthExceeded => {}
693            }
694        }
695
696        result
697    }
698
699    /// Inner subtype check (after cycle detection and type evaluation)
700    fn check_subtype_inner(&mut self, source: TypeId, target: TypeId) -> SubtypeResult {
701        // Types are already evaluated in check_subtype, so no need to re-evaluate here
702
703        if !self.strict_null_checks && source.is_nullish() {
704            return SubtypeResult::True;
705        }
706
707        // Note: Canonicalization-based structural identity (Task #36) was previously
708        // called here as a "fast path", but it was actually SLOWER than the normal path
709        // because it allocated a fresh Canonicalizer per call (FxHashMap + Vecs) and
710        // triggered O(n²) union reduction via interner.union(). The existing QueryCache
711        // already provides O(1) memoization for repeated subtype checks.
712        // The Canonicalizer remains available for its intended purpose: detecting
713        // structural identity of recursive type aliases (graph isomorphism).
714        // See: are_types_structurally_identical() and isomorphism_tests.rs
715
716        // Note: Weak type checking is handled by CompatChecker (compat.rs:167-170).
717        // Removed redundant check here to avoid double-checking which caused false positives.
718
719        if let Some(shape) = self.apparent_primitive_shape_for_type(source) {
720            if let Some(t_shape_id) = object_shape_id(self.interner, target) {
721                let t_shape = self.interner.object_shape(t_shape_id);
722                return self.check_object_subtype(&shape, None, &t_shape);
723            }
724            if let Some(t_shape_id) = object_with_index_shape_id(self.interner, target) {
725                let t_shape = self.interner.object_shape(t_shape_id);
726                return self.check_object_with_index_subtype(&shape, None, &t_shape);
727            }
728        }
729
730        if let Some(source_cond_id) = conditional_type_id(self.interner, source) {
731            if let Some(target_cond_id) = conditional_type_id(self.interner, target) {
732                let source_cond = self.interner.conditional_type(source_cond_id);
733                let target_cond = self.interner.conditional_type(target_cond_id);
734                return self.check_conditional_subtype(source_cond.as_ref(), target_cond.as_ref());
735            }
736
737            let source_cond = self.interner.conditional_type(source_cond_id);
738            return self.conditional_branches_subtype(source_cond.as_ref(), target);
739        }
740
741        if let Some(target_cond_id) = conditional_type_id(self.interner, target) {
742            let target_cond = self.interner.conditional_type(target_cond_id);
743            return self.subtype_of_conditional_target(source, target_cond.as_ref());
744        }
745
746        // Note: Source union/intersection handling is consolidated as follows:
747        //
748        // 1. Source union: Kept here (not moved to visitor) because it must run BEFORE
749        //    the target union check. This order dependency is critical for correct
750        //    union-to-union semantics: Union(A,B) <: Union(C,D) means ALL members of
751        //    source must be subtypes of the target union (delegating to target union check).
752        //
753        // 2. Source intersection: Moved to visitor pattern (visit_intersection) which
754        //    handles both the "at least one member" check AND the property merging logic
755        //    for object targets. This removed ~50 lines of duplicate code.
756        //
757        // Source union check must run BEFORE target union check to handle union-to-union cases:
758        // Union(A, B) <: Union(C, D) means (A <: Union(C, D)) AND (B <: Union(C, D))
759        // This is different from the target union check which does: Source <: C OR Source <: D
760        if let Some(members) = union_list_id(self.interner, source) {
761            let member_list = self.interner.type_list(members);
762            for &member in member_list.iter() {
763                if !self.check_subtype(member, target).is_true() {
764                    // Trace: No union member matches target
765                    if let Some(tracer) = &mut self.tracer
766                        && !tracer.on_mismatch_dyn(SubtypeFailureReason::NoUnionMemberMatches {
767                            source_type: source,
768                            target_union_members: vec![target],
769                        })
770                    {
771                        return SubtypeResult::False;
772                    }
773                    return SubtypeResult::False;
774                }
775            }
776            return SubtypeResult::True;
777        }
778
779        if let Some(members) = union_list_id(self.interner, target) {
780            if keyof_inner_type(self.interner, source).is_some()
781                && self.is_keyof_subtype_of_string_number_symbol_union(members)
782            {
783                return SubtypeResult::True;
784            }
785
786            // Rule #7: Open Numeric Enums - number is assignable to unions containing numeric enums
787            if source == TypeId::NUMBER {
788                let member_list = self.interner.type_list(members);
789                for &member in member_list.iter() {
790                    let def_id = lazy_def_id(self.interner, member)
791                        .or_else(|| enum_components(self.interner, member).map(|(d, _)| d));
792                    if let Some(def_id) = def_id
793                        && self.resolver.is_numeric_enum(def_id)
794                    {
795                        return SubtypeResult::True;
796                    }
797                }
798            }
799
800            let member_list = self.interner.type_list(members);
801
802            // Fast path: TypeId equality pre-scan before expensive structural checks.
803            // If source has the same TypeId as any union member, it's trivially a subtype.
804            // This avoids O(n × cost) structural comparisons when the match is by identity.
805            for &member in member_list.iter() {
806                if source == member {
807                    return SubtypeResult::True;
808                }
809            }
810
811            for &member in member_list.iter() {
812                if self.check_subtype(source, member).is_true() {
813                    return SubtypeResult::True;
814                }
815            }
816
817            // Type parameter constraint check: if source is a type parameter with a constraint,
818            // check if its constraint is assignable to the entire target union.
819            // e.g., Bottom extends T | U should be assignable to T | U
820            if let Some(s_info) = type_param_info(self.interner, source)
821                && let Some(constraint) = s_info.constraint
822                && self.check_subtype(constraint, target).is_true()
823            {
824                return SubtypeResult::True;
825            }
826
827            // Distributive intersection factoring:
828            // S <: (A & S) | (B & S) is equivalent to S <: A | B
829            let s_arc;
830            let source_members: &[TypeId] =
831                if let Some(s_list) = intersection_list_id(self.interner, source) {
832                    s_arc = self.interner.type_list(s_list);
833                    &s_arc
834                } else {
835                    std::slice::from_ref(&source)
836                };
837
838            let mut factored_members = Vec::new();
839            let mut all_contain_source = true;
840            for &member in member_list.iter() {
841                let i_arc;
842                let i_list: &[TypeId] =
843                    if let Some(i_members) = intersection_list_id(self.interner, member) {
844                        i_arc = self.interner.type_list(i_members);
845                        &i_arc
846                    } else {
847                        std::slice::from_ref(&member)
848                    };
849
850                let mut contains_all = true;
851                for &s_m in source_members.iter() {
852                    if !i_list.contains(&s_m) {
853                        contains_all = false;
854                        break;
855                    }
856                }
857
858                // DEBUG LOGGING
859                // println!("source: {:?}, target member: {:?}, source_members: {:?}, i_list: {:?}, contains: {}",
860                //          self.interner.lookup(source), self.interner.lookup(member), source_members, i_list, contains_all);
861
862                if contains_all {
863                    let mut rem = Vec::new();
864                    for &i_m in i_list.iter() {
865                        if !source_members.contains(&i_m) {
866                            rem.push(i_m);
867                        }
868                    }
869                    factored_members.push(self.interner.intersection(rem));
870                } else {
871                    all_contain_source = false;
872                    break;
873                }
874            }
875
876            if all_contain_source && !factored_members.is_empty() {
877                let factored_target = self.interner.union(factored_members);
878                // println!("ALL CONTAIN SOURCE! checking subtype against factored target: {:?}", self.interner.lookup(factored_target));
879                if self.check_subtype(source, factored_target).is_true() {
880                    return SubtypeResult::True;
881                }
882            }
883
884            // Discriminated union check: if the source has discriminant properties
885            // that distinguish between target union members, check each discriminant
886            // value against the matching target members with a narrowed source.
887            // See TypeScript's typeRelatedToDiscriminatedType.
888            if self
889                .type_related_to_discriminated_type(source, &member_list)
890                .is_true()
891            {
892                return SubtypeResult::True;
893            }
894
895            // Intersection source check: if source is an intersection, check if any
896            // member is assignable to the target union as a whole.
897            // e.g., (A & B) <: C | D if A <: C | D
898            if let Some(s_list) = intersection_list_id(self.interner, source) {
899                let s_member_list = self.interner.type_list(s_list);
900                for &s_member in s_member_list.iter() {
901                    if self.check_subtype(s_member, target).is_true() {
902                        return SubtypeResult::True;
903                    }
904                }
905            }
906
907            // Trace: Source is not a subtype of any union member
908            if let Some(tracer) = &mut self.tracer
909                && !tracer.on_mismatch_dyn(SubtypeFailureReason::NoUnionMemberMatches {
910                    source_type: source,
911                    target_union_members: member_list.iter().copied().collect(),
912                })
913            {
914                return SubtypeResult::False;
915            }
916            return SubtypeResult::False;
917        }
918
919        // Note: Source intersection check removed - handled by visitor pattern dispatch
920        // at the end of this function. The visitor includes the property merging logic.
921
922        if let Some(members) = intersection_list_id(self.interner, target) {
923            let member_list = self.interner.type_list(members);
924
925            // Keep diagnostic precision when collecting mismatch reasons via tracer.
926            if self.tracer.is_none()
927                && self.can_use_object_intersection_fast_path(&member_list)
928                && let Some(merged_target) = self.build_object_intersection_target(target)
929            {
930                return self.check_subtype(source, merged_target);
931            }
932
933            for &member in member_list.iter() {
934                if !self.check_subtype(source, member).is_true() {
935                    return SubtypeResult::False;
936                }
937            }
938            return SubtypeResult::True;
939        }
940
941        if let (Some(s_kind), Some(t_kind)) = (
942            intrinsic_kind(self.interner, source),
943            intrinsic_kind(self.interner, target),
944        ) {
945            return self.check_intrinsic_subtype(s_kind, t_kind);
946        }
947
948        // Type parameter checks BEFORE boxed primitive check
949        // Unconstrained type parameters should be handled before other checks
950        if let Some(s_info) = type_param_info(self.interner, source) {
951            return self.check_type_parameter_subtype(&s_info, target);
952        }
953
954        if let Some(_t_info) = type_param_info(self.interner, target) {
955            // Special case: T & SomeType <: T
956            // If source is an intersection containing the target type parameter,
957            // the intersection is a more specific version (excluding null/undefined)
958            // and is assignable. This handles the common pattern: T & {} <: T.
959            if let Some(members) = intersection_list_id(self.interner, source) {
960                let member_list = self.interner.type_list(members);
961                for &member in member_list.iter() {
962                    if member == target {
963                        return SubtypeResult::True;
964                    }
965                }
966            }
967
968            // A concrete type is never a subtype of an opaque type parameter.
969            // The type parameter T could be instantiated as any type satisfying its constraint,
970            // so we cannot guarantee that source <: T unless source is never/any (handled above).
971            //
972            // This is the correct TypeScript behavior:
973            // - "hello" is NOT assignable to T extends string (T could be "world")
974            // - { value: number } is NOT assignable to unconstrained T (T defaults to unknown)
975            //
976            // Note: When the type parameter is the SOURCE (e.g., T <: string), we check
977            // against its constraint. But as TARGET, we return False.
978
979            // Trace: Concrete type not assignable to type parameter
980            if let Some(tracer) = &mut self.tracer
981                && !tracer.on_mismatch_dyn(SubtypeFailureReason::TypeMismatch {
982                    source_type: source,
983                    target_type: target,
984                })
985            {
986                return SubtypeResult::False;
987            }
988            return SubtypeResult::False;
989        }
990
991        if let Some(s_kind) = intrinsic_kind(self.interner, source) {
992            if self.is_boxed_primitive_subtype(s_kind, target) {
993                return SubtypeResult::True;
994            }
995            // `object` keyword is structurally equivalent to `{}` (empty object).
996            // It's assignable to any object type where all properties are optional,
997            // since no required properties need to be satisfied.
998            if s_kind == IntrinsicKind::Object {
999                let target_shape = object_shape_id(self.interner, target)
1000                    .or_else(|| object_with_index_shape_id(self.interner, target));
1001                if let Some(t_shape_id) = target_shape {
1002                    let t_shape = self.interner.object_shape(t_shape_id);
1003                    if t_shape.properties.iter().all(|p| p.optional) {
1004                        return SubtypeResult::True;
1005                    }
1006                }
1007            }
1008            // Trace: Intrinsic type mismatch (boxed primitive check failed)
1009            if let Some(tracer) = &mut self.tracer
1010                && !tracer.on_mismatch_dyn(SubtypeFailureReason::TypeMismatch {
1011                    source_type: source,
1012                    target_type: target,
1013                })
1014            {
1015                return SubtypeResult::False;
1016            }
1017            return SubtypeResult::False;
1018        }
1019
1020        if let (Some(lit), Some(t_kind)) = (
1021            literal_value(self.interner, source),
1022            intrinsic_kind(self.interner, target),
1023        ) {
1024            return self.check_literal_to_intrinsic(&lit, t_kind);
1025        }
1026
1027        if let (Some(s_lit), Some(t_lit)) = (
1028            literal_value(self.interner, source),
1029            literal_value(self.interner, target),
1030        ) {
1031            if s_lit == t_lit {
1032                return SubtypeResult::True;
1033            }
1034            // Trace: Literal type mismatch
1035            if let Some(tracer) = &mut self.tracer
1036                && !tracer.on_mismatch_dyn(SubtypeFailureReason::LiteralTypeMismatch {
1037                    source_type: source,
1038                    target_type: target,
1039                })
1040            {
1041                return SubtypeResult::False;
1042            }
1043            return SubtypeResult::False;
1044        }
1045
1046        if let (Some(LiteralValue::String(s_lit)), Some(t_spans)) = (
1047            literal_value(self.interner, source),
1048            template_literal_id(self.interner, target),
1049        ) {
1050            return self.check_literal_matches_template_literal(s_lit, t_spans);
1051        }
1052
1053        if intrinsic_kind(self.interner, target) == Some(IntrinsicKind::Object) {
1054            if self.is_object_keyword_type(source) {
1055                return SubtypeResult::True;
1056            }
1057            // Trace: Source is not object-compatible
1058            if let Some(tracer) = &mut self.tracer
1059                && !tracer.on_mismatch_dyn(SubtypeFailureReason::TypeMismatch {
1060                    source_type: source,
1061                    target_type: target,
1062                })
1063            {
1064                return SubtypeResult::False;
1065            }
1066            return SubtypeResult::False;
1067        }
1068
1069        // Check if target is the Function intrinsic (TypeId::FUNCTION) or the
1070        // Function interface from lib.d.ts. We check three ways:
1071        // 1. Target is the Function intrinsic (TypeId::FUNCTION)
1072        // 2. Target matches the registered boxed Function TypeId
1073        // 3. Target was resolved from a Lazy(DefId) whose DefId is a known Function DefId
1074        //    (handles the case where get_type_of_symbol and resolve_lib_type_by_name
1075        //    produce different TypeIds for the same Function interface)
1076        let is_function_target = intrinsic_kind(self.interner, target)
1077            == Some(IntrinsicKind::Function)
1078            || self
1079                .resolver
1080                .is_boxed_type_id(target, IntrinsicKind::Function)
1081            || self
1082                .resolver
1083                .get_boxed_type(IntrinsicKind::Function)
1084                .is_some_and(|boxed| boxed == target)
1085            || lazy_def_id(self.interner, target).is_some_and(|def_id| {
1086                self.resolver
1087                    .is_boxed_def_id(def_id, IntrinsicKind::Function)
1088            });
1089        if is_function_target {
1090            if self.is_callable_type(source) {
1091                return SubtypeResult::True;
1092            }
1093            // Trace: Source is not function-compatible
1094            if let Some(tracer) = &mut self.tracer
1095                && !tracer.on_mismatch_dyn(SubtypeFailureReason::TypeMismatch {
1096                    source_type: source,
1097                    target_type: target,
1098                })
1099            {
1100                return SubtypeResult::False;
1101            }
1102            return SubtypeResult::False;
1103        }
1104
1105        // Check if target is the global `Object` interface from lib.d.ts.
1106        // This is a separate path from intrinsic `object`:
1107        // - `object` (lowercase) includes callable values.
1108        // - `Object` (capitalized interface) should follow TS structural rules and
1109        //   exclude bare callable types from primitive-style object assignability.
1110        let is_global_object_target = self
1111            .resolver
1112            .is_boxed_type_id(target, IntrinsicKind::Object)
1113            || self
1114                .resolver
1115                .get_boxed_type(IntrinsicKind::Object)
1116                .is_some_and(|boxed| boxed == target)
1117            || lazy_def_id(self.interner, target)
1118                .is_some_and(|t_def| self.resolver.is_boxed_def_id(t_def, IntrinsicKind::Object));
1119        if is_global_object_target {
1120            let source_eval = self.evaluate_type(source);
1121            if self.is_global_object_interface_type(source_eval) {
1122                return SubtypeResult::True;
1123            }
1124
1125            if let Some(tracer) = &mut self.tracer
1126                && !tracer.on_mismatch_dyn(SubtypeFailureReason::TypeMismatch {
1127                    source_type: source,
1128                    target_type: target,
1129                })
1130            {
1131                return SubtypeResult::False;
1132            }
1133            return SubtypeResult::False;
1134        }
1135
1136        if let (Some(s_elem), Some(t_elem)) = (
1137            array_element_type(self.interner, source),
1138            array_element_type(self.interner, target),
1139        ) {
1140            return self.check_subtype(s_elem, t_elem);
1141        }
1142
1143        if let (Some(s_elems), Some(t_elems)) = (
1144            tuple_list_id(self.interner, source),
1145            tuple_list_id(self.interner, target),
1146        ) {
1147            // OPTIMIZATION: Unit-tuple disjointness fast-path (O(1) cached lookup)
1148            // Two different unit tuples (tuples of literals/enums only) are guaranteed disjoint.
1149            // Since we already checked source == target at the top and returned True,
1150            // reaching here means source != target. If both are unit tuples, they're disjoint.
1151            // This avoids O(N) structural recursion for each comparison in BCT's O(N²) loop.
1152            if self.interner.is_unit_type(source) && self.interner.is_unit_type(target) {
1153                return SubtypeResult::False;
1154            }
1155            let s_elems = self.interner.tuple_list(s_elems);
1156            let t_elems = self.interner.tuple_list(t_elems);
1157            return self.check_tuple_subtype(&s_elems, &t_elems);
1158        }
1159
1160        if let (Some(s_elems), Some(t_elem)) = (
1161            tuple_list_id(self.interner, source),
1162            array_element_type(self.interner, target),
1163        ) {
1164            return self.check_tuple_to_array_subtype(s_elems, t_elem);
1165        }
1166
1167        if let (Some(s_elem), Some(t_elems)) = (
1168            array_element_type(self.interner, source),
1169            tuple_list_id(self.interner, target),
1170        ) {
1171            let t_elems = self.interner.tuple_list(t_elems);
1172            return self.check_array_to_tuple_subtype(s_elem, &t_elems);
1173        }
1174
1175        if let (Some(s_shape_id), Some(t_shape_id)) = (
1176            object_shape_id(self.interner, source),
1177            object_shape_id(self.interner, target),
1178        ) {
1179            let s_shape = self.interner.object_shape(s_shape_id);
1180            let t_shape = self.interner.object_shape(t_shape_id);
1181
1182            // Symbol-level cycle detection for recursive interface types.
1183            // When both objects have symbols (e.g., Promise<X> vs PromiseLike<Y>),
1184            // check if we're already comparing objects with the same symbol pair.
1185            // This catches cycles where type evaluation loses DefId identity:
1186            // Promise<never> evaluates to Object(51) which has no DefId, but its
1187            // `then` method returns Promise<TResult> which, after instantiation and
1188            // evaluation, produces another Object with the same Promise symbol.
1189            if let (Some(s_sym), Some(t_sym)) = (s_shape.symbol, t_shape.symbol)
1190                && s_sym != t_sym
1191            {
1192                let sym_pair = (s_sym, t_sym);
1193                if !self.sym_visiting.insert(sym_pair) {
1194                    // Already visiting this symbol pair — coinductive cycle
1195                    return SubtypeResult::CycleDetected;
1196                }
1197                let result = self.check_object_subtype(&s_shape, Some(s_shape_id), &t_shape);
1198                self.sym_visiting.remove(&sym_pair);
1199                return result;
1200            }
1201
1202            return self.check_object_subtype(&s_shape, Some(s_shape_id), &t_shape);
1203        }
1204
1205        if let (Some(s_shape_id), Some(t_shape_id)) = (
1206            object_with_index_shape_id(self.interner, source),
1207            object_with_index_shape_id(self.interner, target),
1208        ) {
1209            let s_shape = self.interner.object_shape(s_shape_id);
1210            let t_shape = self.interner.object_shape(t_shape_id);
1211            return self.check_object_with_index_subtype(&s_shape, Some(s_shape_id), &t_shape);
1212        }
1213
1214        if let (Some(s_shape_id), Some(t_shape_id)) = (
1215            object_with_index_shape_id(self.interner, source),
1216            object_shape_id(self.interner, target),
1217        ) {
1218            let s_shape = self.interner.object_shape(s_shape_id);
1219            let t_shape = self.interner.object_shape(t_shape_id);
1220            return self.check_object_with_index_to_object(
1221                &s_shape,
1222                s_shape_id,
1223                &t_shape.properties,
1224            );
1225        }
1226
1227        if let (Some(s_shape_id), Some(t_shape_id)) = (
1228            object_shape_id(self.interner, source),
1229            object_with_index_shape_id(self.interner, target),
1230        ) {
1231            let s_shape = self.interner.object_shape(s_shape_id);
1232            let t_shape = self.interner.object_shape(t_shape_id);
1233            return self.check_object_to_indexed(&s_shape.properties, Some(s_shape_id), &t_shape);
1234        }
1235
1236        if let (Some(s_fn_id), Some(t_fn_id)) = (
1237            function_shape_id(self.interner, source),
1238            function_shape_id(self.interner, target),
1239        ) {
1240            let s_fn = self.interner.function_shape(s_fn_id);
1241            let t_fn = self.interner.function_shape(t_fn_id);
1242            return self.check_function_subtype(&s_fn, &t_fn);
1243        }
1244
1245        // Compatibility bridge: function-like values are assignable to interfaces
1246        // that only require Function members like `call`/`apply`.
1247        // This aligns with tsc behavior for:
1248        //   interface Callable { call(blah: any): any }
1249        //   const x: Callable = () => {}
1250        let source_function_like = function_shape_id(self.interner, source).is_some()
1251            || callable_shape_id(self.interner, source).is_some_and(|sid| {
1252                let shape = self.interner.callable_shape(sid);
1253                !shape.call_signatures.is_empty()
1254            })
1255            || source == TypeId::FUNCTION;
1256        if source_function_like {
1257            if let Some(t_callable_id) = callable_shape_id(self.interner, target) {
1258                let t_shape = self.interner.callable_shape(t_callable_id);
1259                if t_shape.call_signatures.is_empty() && t_shape.construct_signatures.is_empty() {
1260                    let required_props: Vec<_> =
1261                        t_shape.properties.iter().filter(|p| !p.optional).collect();
1262                    if required_props.len() == 1 {
1263                        let name = self.interner.resolve_atom(required_props[0].name);
1264                        if name == "call" || name == "apply" {
1265                            return SubtypeResult::True;
1266                        }
1267                    }
1268                }
1269            }
1270            if let Some(t_shape_id) = object_shape_id(self.interner, target)
1271                .or_else(|| object_with_index_shape_id(self.interner, target))
1272            {
1273                let t_shape = self.interner.object_shape(t_shape_id);
1274                let required_props: Vec<_> =
1275                    t_shape.properties.iter().filter(|p| !p.optional).collect();
1276                if required_props.len() == 1 {
1277                    let name = self.interner.resolve_atom(required_props[0].name);
1278                    if name == "call" || name == "apply" {
1279                        return SubtypeResult::True;
1280                    }
1281                }
1282            }
1283        }
1284
1285        if let (Some(s_callable_id), Some(t_callable_id)) = (
1286            callable_shape_id(self.interner, source),
1287            callable_shape_id(self.interner, target),
1288        ) {
1289            let s_callable = self.interner.callable_shape(s_callable_id);
1290            let t_callable = self.interner.callable_shape(t_callable_id);
1291            return self.check_callable_subtype(&s_callable, &t_callable);
1292        }
1293
1294        if let (Some(s_fn_id), Some(t_callable_id)) = (
1295            function_shape_id(self.interner, source),
1296            callable_shape_id(self.interner, target),
1297        ) {
1298            return self.check_function_to_callable_subtype(s_fn_id, t_callable_id);
1299        }
1300
1301        if let (Some(s_callable_id), Some(t_fn_id)) = (
1302            callable_shape_id(self.interner, source),
1303            function_shape_id(self.interner, target),
1304        ) {
1305            return self.check_callable_to_function_subtype(s_callable_id, t_fn_id);
1306        }
1307
1308        if let (Some(s_app_id), Some(t_app_id)) = (
1309            application_id(self.interner, source),
1310            application_id(self.interner, target),
1311        ) {
1312            return self.check_application_to_application_subtype(s_app_id, t_app_id);
1313        }
1314
1315        // When both source and target are applications, try mapped-to-mapped
1316        // comparison before falling through to one-sided expansion. This handles
1317        // cases like Readonly<T> <: Partial<T> where both resolve to mapped types
1318        // over a generic type parameter that can't be concretely expanded.
1319        if let (Some(s_app_id), Some(t_app_id)) = (
1320            application_id(self.interner, source),
1321            application_id(self.interner, target),
1322        ) {
1323            let result = self.check_application_to_application(source, target, s_app_id, t_app_id);
1324            if result != SubtypeResult::False {
1325                return result;
1326            }
1327            // Fall through to one-sided expansion
1328        }
1329
1330        if let Some(app_id) = application_id(self.interner, source) {
1331            return self.check_application_expansion_target(source, target, app_id);
1332        }
1333
1334        if let Some(app_id) = application_id(self.interner, target) {
1335            return self.check_source_to_application_expansion(source, target, app_id);
1336        }
1337
1338        // Check mapped-to-mapped structural comparison (for raw mapped types).
1339        if let (Some(source_mapped_id), Some(target_mapped_id)) = (
1340            mapped_type_id(self.interner, source),
1341            mapped_type_id(self.interner, target),
1342        ) {
1343            let result =
1344                self.check_mapped_to_mapped(source, target, source_mapped_id, target_mapped_id);
1345            if result != SubtypeResult::False {
1346                return result;
1347            }
1348        }
1349
1350        if let Some(mapped_id) = mapped_type_id(self.interner, source) {
1351            return self.check_mapped_expansion_target(source, target, mapped_id);
1352        }
1353
1354        if let Some(mapped_id) = mapped_type_id(self.interner, target) {
1355            return self.check_source_to_mapped_expansion(source, target, mapped_id);
1356        }
1357
1358        // =======================================================================
1359        // ENUM TYPE CHECKING (Nominal Identity)
1360        // =======================================================================
1361        // Enums are nominal types - two different enums with the same member types
1362        // are NOT compatible. Enum(DefId, MemberType) preserves both:
1363        // - DefId: For nominal identity (E1 != E2)
1364        // - MemberType: For structural assignability to primitives (E1 <: number)
1365        // =======================================================================
1366
1367        if let (Some((s_def_id, _s_members)), Some((t_def_id, _t_members))) = (
1368            enum_components(self.interner, source),
1369            enum_components(self.interner, target),
1370        ) {
1371            // Enum to Enum: Nominal check - DefIds must match
1372            if s_def_id == t_def_id {
1373                return SubtypeResult::True;
1374            }
1375
1376            // Check for member-to-parent relationship (e.g., E.A -> E)
1377            // If source is a member of the target enum, it is a subtype
1378            if self.resolver.get_enum_parent_def_id(s_def_id) == Some(t_def_id) {
1379                // Source is a member of target enum
1380                // Only allow if target is the full enum type (not a different member)
1381                if self.resolver.is_enum_type(target, self.interner) {
1382                    return SubtypeResult::True;
1383                }
1384            }
1385
1386            // Different enums are NOT compatible (nominal typing)
1387            // Trace: Enum nominal mismatch
1388            if let Some(tracer) = &mut self.tracer
1389                && !tracer.on_mismatch_dyn(SubtypeFailureReason::TypeMismatch {
1390                    source_type: source,
1391                    target_type: target,
1392                })
1393            {
1394                return SubtypeResult::False;
1395            }
1396            return SubtypeResult::False;
1397        }
1398
1399        // Source is Enum, Target is not - check structural member type
1400        if let Some((_s_def_id, s_members)) = enum_components(self.interner, source) {
1401            return self.check_subtype(s_members, target);
1402        }
1403
1404        // Target is Enum, Source is not - check Rule #7 first, then structural member type
1405        if let Some((t_def_id, t_members)) = enum_components(self.interner, target) {
1406            // Rule #7: number is assignable to numeric enums
1407            if source == TypeId::NUMBER && self.resolver.is_numeric_enum(t_def_id) {
1408                return SubtypeResult::True;
1409            }
1410            if matches!(
1411                self.interner.lookup(source),
1412                Some(TypeData::Literal(LiteralValue::Number(_)))
1413            ) && self.resolver.is_numeric_enum(t_def_id)
1414            {
1415                return SubtypeResult::True;
1416            }
1417            return self.check_subtype(source, t_members);
1418        }
1419
1420        // =======================================================================
1421        // PHASE 3.2: PRIORITIZE DefId (Lazy) OVER SymbolRef (Ref)
1422        // =======================================================================
1423        // We now check Lazy(DefId) types before Ref(SymbolRef) types to establish
1424        // DefId as the primary type identity system. The InheritanceGraph bridge
1425        // enables Lazy types to use O(1) nominal subtype checking.
1426        // =======================================================================
1427
1428        if let (Some(s_def), Some(t_def)) = (
1429            lazy_def_id(self.interner, source),
1430            lazy_def_id(self.interner, target),
1431        ) {
1432            // Use DefId-level cycle detection (checked before Ref types)
1433            return self.check_lazy_lazy_subtype(source, target, s_def, t_def);
1434        }
1435
1436        // =======================================================================
1437        // Rule #7: Open Numeric Enums - Number <-> Numeric Enum Assignability
1438        // =======================================================================
1439        // In TypeScript, numeric enums are "open" - they allow bidirectional
1440        // assignability with the number type. This is unsound but matches tsc behavior.
1441        // See docs/specs/TS_UNSOUNDNESS_CATALOG.md Item #7.
1442
1443        // Helper to extract DefId from Enum or Lazy types
1444        let get_enum_def_id = |type_id: TypeId| -> Option<DefId> {
1445            match self.interner.lookup(type_id) {
1446                Some(TypeData::Enum(def_id, _)) | Some(TypeData::Lazy(def_id)) => Some(def_id),
1447                _ => None,
1448            }
1449        };
1450
1451        // Check: source is numeric enum, target is Number
1452        if let Some(s_def) = get_enum_def_id(source)
1453            && target == TypeId::NUMBER
1454            && self.resolver.is_numeric_enum(s_def)
1455        {
1456            return SubtypeResult::True;
1457        }
1458
1459        // Check: source is Number (or numeric literal), target is numeric enum
1460        if let Some(t_def) = get_enum_def_id(target) {
1461            if source == TypeId::NUMBER && self.resolver.is_numeric_enum(t_def) {
1462                return SubtypeResult::True;
1463            }
1464            // Also check for numeric literals (subtypes of number)
1465            if matches!(
1466                self.interner.lookup(source),
1467                Some(TypeData::Literal(LiteralValue::Number(_)))
1468            ) && self.resolver.is_numeric_enum(t_def)
1469            {
1470                // For numeric literals, we need to check if they're assignable to the enum
1471                // Fall through to structural check (e.g., 0 -> E.A might succeed if E.A = 0)
1472                return self.check_subtype(source, self.resolve_lazy_type(target));
1473            }
1474        }
1475
1476        if lazy_def_id(self.interner, source).is_some() {
1477            let resolved = self.resolve_lazy_type(source);
1478            return if resolved != source {
1479                self.check_subtype(resolved, target)
1480            } else {
1481                SubtypeResult::False
1482            };
1483        }
1484
1485        if lazy_def_id(self.interner, target).is_some() {
1486            let resolved = self.resolve_lazy_type(target);
1487            return if resolved != target {
1488                self.check_subtype(source, resolved)
1489            } else {
1490                SubtypeResult::False
1491            };
1492        }
1493
1494        // =======================================================================
1495        // Ref(SymbolRef) checks - now secondary to Lazy(DefId)
1496        // =======================================================================
1497
1498        if let (Some(s_sym), Some(t_sym)) = (
1499            ref_symbol(self.interner, source),
1500            ref_symbol(self.interner, target),
1501        ) {
1502            return self.check_ref_ref_subtype(source, target, s_sym, t_sym);
1503        }
1504
1505        if let Some(s_sym) = ref_symbol(self.interner, source) {
1506            return self.check_ref_subtype(source, target, s_sym);
1507        }
1508
1509        if let Some(t_sym) = ref_symbol(self.interner, target) {
1510            return self.check_to_ref_subtype(source, target, t_sym);
1511        }
1512
1513        if let (Some(s_sym), Some(t_sym)) = (
1514            type_query_symbol(self.interner, source),
1515            type_query_symbol(self.interner, target),
1516        ) {
1517            return self.check_typequery_typequery_subtype(source, target, s_sym, t_sym);
1518        }
1519
1520        if let Some(s_sym) = type_query_symbol(self.interner, source) {
1521            return self.check_typequery_subtype(source, target, s_sym);
1522        }
1523
1524        if let Some(t_sym) = type_query_symbol(self.interner, target) {
1525            return self.check_to_typequery_subtype(source, target, t_sym);
1526        }
1527
1528        if let (Some(s_inner), Some(t_inner)) = (
1529            keyof_inner_type(self.interner, source),
1530            keyof_inner_type(self.interner, target),
1531        ) {
1532            return self.check_subtype(t_inner, s_inner);
1533        }
1534
1535        if let (Some(s_inner), Some(t_inner)) = (
1536            readonly_inner_type(self.interner, source),
1537            readonly_inner_type(self.interner, target),
1538        ) {
1539            return self.check_subtype(s_inner, t_inner);
1540        }
1541
1542        // Readonly target peeling: T <: Readonly<U> if T <: U
1543        // A mutable type can always be treated as readonly (readonly is a supertype)
1544        // CRITICAL: Only peel if source is NOT Readonly. If source IS Readonly, we must
1545        // fall through to the visitor to compare Readonly<S> vs Readonly<T>.
1546        if let Some(t_inner) = readonly_inner_type(self.interner, target)
1547            && readonly_inner_type(self.interner, source).is_none()
1548        {
1549            return self.check_subtype(source, t_inner);
1550        }
1551
1552        // Readonly source to mutable target case is handled by SubtypeVisitor::visit_readonly_type
1553        // which returns False (correctly, because Readonly is not assignable to Mutable)
1554
1555        if let (Some(s_sym), Some(t_sym)) = (
1556            unique_symbol_ref(self.interner, source),
1557            unique_symbol_ref(self.interner, target),
1558        ) {
1559            return if s_sym == t_sym {
1560                SubtypeResult::True
1561            } else {
1562                SubtypeResult::False
1563            };
1564        }
1565
1566        if unique_symbol_ref(self.interner, source).is_some()
1567            && intrinsic_kind(self.interner, target) == Some(IntrinsicKind::Symbol)
1568        {
1569            return SubtypeResult::True;
1570        }
1571
1572        if is_this_type(self.interner, source) && is_this_type(self.interner, target) {
1573            return SubtypeResult::True;
1574        }
1575
1576        if let (Some(s_spans), Some(t_spans)) = (
1577            template_literal_id(self.interner, source),
1578            template_literal_id(self.interner, target),
1579        ) {
1580            return self.check_template_assignable_to_template(s_spans, t_spans);
1581        }
1582
1583        if template_literal_id(self.interner, source).is_some()
1584            && intrinsic_kind(self.interner, target) == Some(IntrinsicKind::String)
1585        {
1586            return SubtypeResult::True;
1587        }
1588
1589        let source_is_callable = function_shape_id(self.interner, source).is_some()
1590            || callable_shape_id(self.interner, source).is_some();
1591        if source_is_callable {
1592            // Build a source ObjectShape from callable properties for structural comparison.
1593            // IMPORTANT: Sort properties by name (Atom) to match the merge scan's expectation.
1594            let source_props = if let Some(callable_id) = callable_shape_id(self.interner, source) {
1595                let callable = self.interner.callable_shape(callable_id);
1596                let mut props = callable.properties.clone();
1597                props.sort_by_key(|a| a.name);
1598                Some(ObjectShape {
1599                    flags: ObjectFlags::empty(),
1600                    properties: props,
1601                    string_index: callable.string_index.clone(),
1602                    number_index: callable.number_index.clone(),
1603                    symbol: callable.symbol,
1604                })
1605            } else {
1606                None
1607            };
1608
1609            if let Some(t_shape_id) = object_shape_id(self.interner, target) {
1610                let t_shape = self.interner.object_shape(t_shape_id);
1611                if t_shape.properties.is_empty() {
1612                    return SubtypeResult::True;
1613                }
1614                // If source is a CallableShape with properties, check structural compatibility
1615                if let Some(ref s_shape) = source_props {
1616                    return self.check_object_subtype(s_shape, None, &t_shape);
1617                }
1618                // FunctionShape has no properties - not assignable to non-empty object
1619                if let Some(tracer) = &mut self.tracer
1620                    && !tracer.on_mismatch_dyn(SubtypeFailureReason::TypeMismatch {
1621                        source_type: source,
1622                        target_type: target,
1623                    })
1624                {
1625                    return SubtypeResult::False;
1626                }
1627                return SubtypeResult::False;
1628            }
1629            if let Some(t_shape_id) = object_with_index_shape_id(self.interner, target) {
1630                let t_shape = self.interner.object_shape(t_shape_id);
1631                if t_shape.properties.is_empty() {
1632                    return SubtypeResult::True;
1633                }
1634                // If source is a CallableShape with properties, check structural compatibility
1635                if let Some(ref s_shape) = source_props {
1636                    return self.check_object_subtype(s_shape, None, &t_shape);
1637                }
1638                // FunctionShape has no properties - not assignable to non-empty indexed object
1639                if let Some(tracer) = &mut self.tracer
1640                    && !tracer.on_mismatch_dyn(SubtypeFailureReason::TypeMismatch {
1641                        source_type: source,
1642                        target_type: target,
1643                    })
1644                {
1645                    return SubtypeResult::False;
1646                }
1647                return SubtypeResult::False;
1648            }
1649        }
1650
1651        let source_is_array_or_tuple = array_element_type(self.interner, source).is_some()
1652            || tuple_list_id(self.interner, source).is_some();
1653        if source_is_array_or_tuple {
1654            if let Some(t_shape_id) = object_shape_id(self.interner, target) {
1655                let t_shape = self.interner.object_shape(t_shape_id);
1656                if t_shape.properties.is_empty() {
1657                    return SubtypeResult::True;
1658                }
1659                // Check if all target properties are satisfiable by the array.
1660                // First try a quick check for length-only targets.
1661                let only_length = t_shape
1662                    .properties
1663                    .iter()
1664                    .all(|p| self.interner.resolve_atom(p.name) == "length");
1665                if only_length {
1666                    let all_ok = t_shape
1667                        .properties
1668                        .iter()
1669                        .all(|p| self.check_subtype(TypeId::NUMBER, p.type_id).is_true());
1670                    if all_ok {
1671                        return SubtypeResult::True;
1672                    }
1673                }
1674                // Try the Array<T> interface for full structural comparison.
1675                // This handles cases like: number[] <: { toString(): string }
1676                if let Some(elem) = array_element_type(self.interner, source)
1677                    && let Some(result) = self.check_array_interface_subtype(elem, target)
1678                {
1679                    return result;
1680                }
1681                // Trace: Array/tuple not compatible with object
1682                if let Some(tracer) = &mut self.tracer
1683                    && !tracer.on_mismatch_dyn(SubtypeFailureReason::TypeMismatch {
1684                        source_type: source,
1685                        target_type: target,
1686                    })
1687                {
1688                    return SubtypeResult::False;
1689                }
1690                return SubtypeResult::False;
1691            }
1692            if let Some(t_shape_id) = object_with_index_shape_id(self.interner, target) {
1693                let t_shape = self.interner.object_shape(t_shape_id);
1694                if t_shape.properties.is_empty() {
1695                    if let Some(ref num_idx) = t_shape.number_index {
1696                        let elem_type =
1697                            array_element_type(self.interner, source).unwrap_or(TypeId::ANY);
1698                        if !self.check_subtype(elem_type, num_idx.value_type).is_true() {
1699                            // Trace: Array element type mismatch with index signature
1700                            if let Some(tracer) = &mut self.tracer
1701                                && !tracer.on_mismatch_dyn(
1702                                    SubtypeFailureReason::IndexSignatureMismatch {
1703                                        index_kind: "number",
1704                                        source_value_type: elem_type,
1705                                        target_value_type: num_idx.value_type,
1706                                    },
1707                                )
1708                            {
1709                                return SubtypeResult::False;
1710                            }
1711                            return SubtypeResult::False;
1712                        }
1713                    }
1714                    return SubtypeResult::True;
1715                }
1716                // Target has non-empty properties + index signature.
1717                // Try the Array<T> interface for full structural comparison.
1718                if let Some(elem) = array_element_type(self.interner, source)
1719                    && let Some(result) = self.check_array_interface_subtype(elem, target)
1720                {
1721                    return result;
1722                }
1723                // Trace: Array/tuple not compatible with indexed object with non-empty properties
1724                if let Some(tracer) = &mut self.tracer
1725                    && !tracer.on_mismatch_dyn(SubtypeFailureReason::TypeMismatch {
1726                        source_type: source,
1727                        target_type: target,
1728                    })
1729                {
1730                    return SubtypeResult::False;
1731                }
1732                return SubtypeResult::False;
1733            }
1734        }
1735
1736        // =======================================================================
1737        // VISITOR PATTERN DISPATCH (Task #48.4)
1738        // =======================================================================
1739        // After all special-case checks above, dispatch to the visitor for
1740        // general structural type checking. The visitor implements double-
1741        // dispatch pattern to handle source type variants and their interaction
1742        // with the target type.
1743        // =======================================================================
1744
1745        // Extract the interner reference FIRST (Copy trait)
1746        // This must happen before creating the visitor which mutably borrows self
1747        let interner = self.interner;
1748
1749        // Create the visitor with a mutable reborrow of self
1750        let mut visitor = SubtypeVisitor {
1751            checker: self,
1752            source,
1753            target,
1754        };
1755
1756        // Dispatch to the visitor using the extracted interner
1757        let result = visitor.visit_type(interner, source);
1758
1759        if result == SubtypeResult::False && self.check_generic_index_access_subtype(source, target)
1760        {
1761            return SubtypeResult::True;
1762        }
1763
1764        // Trace: Generic fallback type mismatch (no specific reason matched above)
1765        if result == SubtypeResult::False
1766            && let Some(tracer) = &mut self.tracer
1767            && !tracer.on_mismatch_dyn(SubtypeFailureReason::TypeMismatch {
1768                source_type: source,
1769                target_type: target,
1770            })
1771        {
1772            return SubtypeResult::False;
1773        }
1774
1775        result
1776    }
1777
1778    /// Check if a deferred keyof type is a subtype of string | number | symbol.
1779    /// This handles the case where `keyof T` (T is a type parameter) should be
1780    /// considered a subtype of `string | number | symbol` because in TypeScript,
1781    /// keyof always produces a subtype of those three types.
1782    fn is_keyof_subtype_of_string_number_symbol_union(&self, members: TypeListId) -> bool {
1783        let member_list = self.interner.type_list(members);
1784        // Check if the union contains string, number, and symbol
1785        let mut has_string = false;
1786        let mut has_number = false;
1787        let mut has_symbol = false;
1788        for &member in member_list.iter() {
1789            if member == TypeId::STRING {
1790                has_string = true;
1791            } else if member == TypeId::NUMBER {
1792                has_number = true;
1793            } else if member == TypeId::SYMBOL {
1794                has_symbol = true;
1795            }
1796        }
1797        has_string && has_number && has_symbol
1798    }
1799}
1800
1801/// Convenience function for one-off subtype checks (without resolver)
1802pub fn is_subtype_of(interner: &dyn TypeDatabase, source: TypeId, target: TypeId) -> bool {
1803    let mut checker = SubtypeChecker::new(interner);
1804    checker.is_subtype_of(source, target)
1805}
1806
1807impl<'a, R: TypeResolver> AssignabilityChecker for SubtypeChecker<'a, R> {
1808    fn is_assignable_to(&mut self, source: TypeId, target: TypeId) -> bool {
1809        SubtypeChecker::is_assignable_to(self, source, target)
1810    }
1811
1812    fn is_assignable_to_bivariant_callback(&mut self, source: TypeId, target: TypeId) -> bool {
1813        let prev_strict = self.strict_function_types;
1814        let prev_param_count = self.allow_bivariant_param_count;
1815        self.strict_function_types = false;
1816        self.allow_bivariant_param_count = true;
1817        let result = SubtypeChecker::is_assignable_to(self, source, target);
1818        self.allow_bivariant_param_count = prev_param_count;
1819        self.strict_function_types = prev_strict;
1820        result
1821    }
1822
1823    fn evaluate_type(&mut self, type_id: TypeId) -> TypeId {
1824        SubtypeChecker::evaluate_type(self, type_id)
1825    }
1826}
1827
1828/// Check if two types are structurally identical using De Bruijn indices for cycles.
1829///
1830/// This is the O(1) alternative to bidirectional subtyping for identity checks.
1831/// It transforms cyclic graphs into trees to solve the Graph Isomorphism problem.
1832pub fn are_types_structurally_identical<R: TypeResolver>(
1833    interner: &dyn TypeDatabase,
1834    resolver: &R,
1835    a: TypeId,
1836    b: TypeId,
1837) -> bool {
1838    if a == b {
1839        return true;
1840    }
1841    let mut canonicalizer = crate::canonicalize::Canonicalizer::new(interner, resolver);
1842    let canon_a = canonicalizer.canonicalize(a);
1843    let canon_b = canonicalizer.canonicalize(b);
1844
1845    // After canonicalization, structural identity reduces to TypeId equality
1846    canon_a == canon_b
1847}
1848
1849/// Convenience function for one-off subtype checks routed through a `QueryDatabase`.
1850/// The `QueryDatabase` enables Salsa memoization when available.
1851pub fn is_subtype_of_with_db(db: &dyn QueryDatabase, source: TypeId, target: TypeId) -> bool {
1852    let mut checker = SubtypeChecker::new(db.as_type_database()).with_query_db(db);
1853    checker.is_subtype_of(source, target)
1854}
1855
1856/// Convenience function for one-off subtype checks with compiler flags.
1857/// The flags are a packed u16 bitmask matching RelationCacheKey.flags.
1858pub fn is_subtype_of_with_flags(
1859    interner: &dyn TypeDatabase,
1860    source: TypeId,
1861    target: TypeId,
1862    flags: u16,
1863) -> bool {
1864    let mut checker = SubtypeChecker::new(interner).apply_flags(flags);
1865    checker.is_subtype_of(source, target)
1866}
1867
1868// Re-enabled subtype tests - verifying API compatibility
1869#[cfg(test)]
1870#[path = "../tests/subtype_tests.rs"]
1871mod tests;
1872
1873#[cfg(test)]
1874#[path = "../tests/index_signature_tests.rs"]
1875mod index_signature_tests;
1876
1877#[cfg(test)]
1878#[path = "../tests/generics_rules_tests.rs"]
1879mod generics_rules_tests;
1880
1881#[cfg(test)]
1882#[path = "../tests/callable_tests.rs"]
1883mod callable_tests;
1884
1885#[cfg(test)]
1886#[path = "../tests/union_tests.rs"]
1887mod union_tests;
1888
1889#[cfg(test)]
1890#[path = "../tests/typescript_quirks_tests.rs"]
1891mod typescript_quirks_tests;
1892
1893#[cfg(test)]
1894#[path = "../tests/type_predicate_tests.rs"]
1895mod type_predicate_tests;
1896
1897#[cfg(test)]
1898#[path = "../tests/overlap_tests.rs"]
1899mod overlap_tests;