Skip to main content

tsz_solver/relations/
subtype_cache.rs

1//! Subtype check caching and cycle detection layer.
2//!
3//! This module implements the outer `check_subtype` method which wraps the
4//! structural dispatch in `check_subtype_inner` with:
5//! - Fast paths (identity, `any`, `unknown`, `never`, `error`)
6//! - Cross-checker memoization via `QueryDatabase`
7//! - Coinductive cycle detection via `RecursionGuard`
8//! - DefId-level and SymbolId-level cycle detection for recursive types
9//! - Pre-evaluation intrinsic checks (Object/Function interfaces)
10//! - Meta-type evaluation bridging
11
12use crate::TypeDatabase;
13use crate::def::DefId;
14use crate::relations::subtype::{SubtypeChecker, SubtypeResult, is_disjoint_unit_type};
15use crate::type_resolver::TypeResolver;
16use crate::types::{IntrinsicKind, TypeId};
17use crate::visitor::{application_id, enum_components, lazy_def_id};
18
19impl<'a, R: TypeResolver> SubtypeChecker<'a, R> {
20    /// When a cycle is detected, we return `CycleDetected` (coinductive semantics)
21    /// which implements greatest fixed point semantics - the correct behavior for
22    /// recursive type checking. When depth/iteration limits are exceeded, we return
23    /// `DepthExceeded` (conservative false) for soundness.
24    pub fn check_subtype(&mut self, source: TypeId, target: TypeId) -> SubtypeResult {
25        // =========================================================================
26        // Fast paths (no cycle tracking needed)
27        // =========================================================================
28
29        let allow_any = self.any_propagation.allows_any_at_depth(self.guard.depth());
30        let mut source = source;
31        let mut target = target;
32        if !allow_any {
33            if source == TypeId::ANY {
34                // In strict mode, any doesn't match everything structurally.
35                // We demote it to STRICT_ANY so it only matches top types or itself.
36                source = TypeId::STRICT_ANY;
37            }
38            if target == TypeId::ANY {
39                target = TypeId::STRICT_ANY;
40            }
41        }
42
43        // Same type is always a subtype of itself
44        if source == target {
45            return SubtypeResult::True;
46        }
47
48        // Task #54: Structural Identity Fast-Path (O(1) after canonicalization)
49        // Check if source and target canonicalize to the same TypeId, which means
50        // they are structurally identical. This avoids expensive structural walks
51        // for types that are the same structure but were interned separately.
52        //
53        // Guarded by bypass_evaluation to prevent infinite recursion when called
54        // from TypeEvaluator during simplification (evaluation has already been done).
55        if !self.bypass_evaluation
56            && let Some(db) = self.query_db
57        {
58            let source_canon = db.canonical_id(source);
59            let target_canon = db.canonical_id(target);
60            if source_canon == target_canon {
61                return SubtypeResult::True;
62            }
63        }
64
65        // Any is assignable to anything (when allowed)
66        if allow_any && (source == TypeId::ANY || source == TypeId::STRICT_ANY) {
67            return SubtypeResult::True;
68        }
69
70        // Everything is assignable to any (when allowed)
71        if allow_any && (target == TypeId::ANY || target == TypeId::STRICT_ANY) {
72            return SubtypeResult::True;
73        }
74
75        // If not allowing any (nested strict any), any still matches Top types as source,
76        // but any as target ALWAYS matches (it's a top type).
77        if !allow_any
78            && (source == TypeId::ANY || source == TypeId::STRICT_ANY)
79            && (target == TypeId::ANY || target == TypeId::STRICT_ANY || target == TypeId::UNKNOWN)
80        {
81            return SubtypeResult::True;
82        }
83        // Fall through to structural check (which will fail for STRICT_ANY)
84        if !allow_any && (target == TypeId::ANY || target == TypeId::STRICT_ANY) {
85            return SubtypeResult::True;
86        }
87        // Fall through to structural check (which will fail for STRICT_ANY)
88
89        // Everything is assignable to unknown
90        if target == TypeId::UNKNOWN {
91            return SubtypeResult::True;
92        }
93
94        // Never is assignable to everything
95        if source == TypeId::NEVER {
96            return SubtypeResult::True;
97        }
98
99        // Error types are assignable to/from everything (like `any` in tsc).
100        // This prevents cascading diagnostics when type resolution fails.
101        if source == TypeId::ERROR || target == TypeId::ERROR {
102            return SubtypeResult::True;
103        }
104
105        // Fast path: distinct disjoint unit types are never subtypes.
106        // This avoids expensive structural checks for large unions of literals/enum members.
107        if is_disjoint_unit_type(self.interner, source)
108            && is_disjoint_unit_type(self.interner, target)
109        {
110            return SubtypeResult::False;
111        }
112
113        // =========================================================================
114        // Cross-checker memoization (QueryCache lookup)
115        // =========================================================================
116        // Check the shared cache for a previously computed result.
117        // This avoids re-doing expensive structural checks for type pairs
118        // already resolved by a prior SubtypeChecker instance.
119        if let Some(db) = self.query_db {
120            let key = self.make_cache_key(source, target);
121            if let Some(cached) = db.lookup_subtype_cache(key) {
122                return if cached {
123                    SubtypeResult::True
124                } else {
125                    SubtypeResult::False
126                };
127            }
128        }
129
130        // =========================================================================
131        // Cycle detection (coinduction) via RecursionGuard - BEFORE evaluation!
132        //
133        // RecursionGuard handles iteration limits, depth limits, cycle detection,
134        // and visiting set size limits in one call.
135        // =========================================================================
136
137        let pair = (source, target);
138
139        // Check reversed pair for bivariant cross-recursion detection.
140        if self.guard.is_visiting(&(target, source)) {
141            return SubtypeResult::CycleDetected;
142        }
143
144        use crate::recursion::RecursionResult;
145        match self.guard.enter(pair) {
146            RecursionResult::Cycle => return SubtypeResult::CycleDetected,
147            RecursionResult::DepthExceeded | RecursionResult::IterationExceeded => {
148                return SubtypeResult::DepthExceeded;
149            }
150            RecursionResult::Entered => {}
151        }
152
153        // =======================================================================
154        // DefId-level cycle detection (before evaluation!)
155        // Catches cycles in recursive type aliases BEFORE they expand.
156        //
157        // For non-Application types: extract DefId directly from Lazy/Enum.
158        // For Application types (e.g., List<T>): extract the BASE DefId from
159        // the Application's base type. This enables coinductive cycle detection
160        // for recursive generic interfaces like List<T> extends Sequence<T>
161        // where method return types create infinite expansion chains
162        // (e.g., List<Pair<T,S>> <: Seq<Pair<T,S>> → List<Pair<...>> <: ...).
163        //
164        // For Application types with the SAME base DefId (e.g., Array<number>
165        // vs Array<string>), we skip cycle detection because these are legitimate
166        // comparisons that should not be treated as cycles.
167        // =======================================================================
168
169        let extract_def_id = |interner: &dyn TypeDatabase, type_id: TypeId| -> Option<DefId> {
170            // First try direct Lazy/Enum DefId
171            if let Some(def) = lazy_def_id(interner, type_id) {
172                return Some(def);
173            }
174            if let Some((def, _)) = enum_components(interner, type_id) {
175                return Some(def);
176            }
177            // For Application types, extract the base DefId
178            if let Some(app_id) = application_id(interner, type_id) {
179                let app = interner.type_application(app_id);
180                if let Some(def) = lazy_def_id(interner, app.base) {
181                    return Some(def);
182                }
183            }
184            None
185        };
186
187        let s_def_id = extract_def_id(self.interner, source);
188        let t_def_id = extract_def_id(self.interner, target);
189
190        let def_pair = if let (Some(s_def), Some(t_def)) = (s_def_id, t_def_id) {
191            // Skip same-base Application cycle detection to avoid false positives
192            // (e.g., Array<number> vs Array<string> share the same base)
193            if s_def == t_def
194                && application_id(self.interner, source).is_some()
195                && application_id(self.interner, target).is_some()
196            {
197                None
198            } else {
199                Some((s_def, t_def))
200            }
201        } else {
202            None
203        };
204
205        // =======================================================================
206        // Symbol-level cycle detection for cross-context DefId aliasing.
207        //
208        // The same interface (e.g., Promise) may get different DefIds in different
209        // checker contexts (lib vs user file). When comparing recursive generic
210        // interfaces, the DefId-level cycle detection can miss cycles because
211        // the inner comparison uses different DefIds than the outer one.
212        //
213        // Fix: resolve DefIds to their underlying SymbolIds (stored in
214        // DefinitionInfo). If a (SymbolId, SymbolId) pair is already being
215        // visited via a different DefId pair, treat it as a cycle.
216        // =======================================================================
217        if let (Some(s_def), Some(t_def)) = (s_def_id, t_def_id) {
218            let s_sym = self.resolver.def_to_symbol_id(s_def);
219            let t_sym = self.resolver.def_to_symbol_id(t_def);
220            if let (Some(s_sid), Some(t_sid)) = (s_sym, t_sym) {
221                // Check if any visiting DefId pair maps to the same SymbolId pair
222                if self.def_guard.is_visiting_any(|&(visiting_s, visiting_t)| {
223                    visiting_s != s_def
224                        && visiting_t != t_def
225                        && self.resolver.def_to_symbol_id(visiting_s) == Some(s_sid)
226                        && self.resolver.def_to_symbol_id(visiting_t) == Some(t_sid)
227                }) {
228                    self.guard.leave(pair);
229                    return SubtypeResult::CycleDetected;
230                }
231            }
232        }
233
234        let def_entered = if let Some((s_def, t_def)) = def_pair {
235            // Check reversed pair for bivariant cross-recursion
236            if self.def_guard.is_visiting(&(t_def, s_def)) {
237                self.guard.leave(pair);
238                return SubtypeResult::CycleDetected;
239            }
240            match self.def_guard.enter((s_def, t_def)) {
241                RecursionResult::Cycle => {
242                    self.guard.leave(pair);
243                    return SubtypeResult::CycleDetected;
244                }
245                RecursionResult::Entered => Some((s_def, t_def)),
246                _ => None,
247            }
248        } else {
249            None
250        };
251
252        // =========================================================================
253        // Pre-evaluation intrinsic checks
254        // =========================================================================
255        // Object interface: any non-nullable source is assignable.
256        // In TypeScript, the Object interface from lib.d.ts is the root of
257        // the prototype chain — all types except null/undefined/void are
258        // assignable to it. We must check BEFORE evaluate_type() because
259        // evaluation may change the target TypeId, losing the boxed identity.
260        {
261            let is_object_interface_target = self
262                .resolver
263                .is_boxed_type_id(target, IntrinsicKind::Object)
264                || self
265                    .resolver
266                    .get_boxed_type(IntrinsicKind::Object)
267                    .is_some_and(|boxed| boxed == target)
268                || lazy_def_id(self.interner, target).is_some_and(|def_id| {
269                    self.resolver.is_boxed_def_id(def_id, IntrinsicKind::Object)
270                });
271            if is_object_interface_target {
272                let is_nullable = source.is_nullable();
273                if !is_nullable {
274                    let result = self.check_object_contract(source, target);
275                    if let Some(dp) = def_entered {
276                        self.def_guard.leave(dp);
277                    }
278                    self.guard.leave(pair);
279                    return result;
280                }
281            }
282        }
283
284        // Check if target is the Function interface from lib.d.ts.
285        // We must check BEFORE evaluate_type() because evaluation resolves
286        // Lazy(DefId) → ObjectShape, losing the DefId identity needed to
287        // recognize the type as an intrinsic interface.
288        if !self.bypass_evaluation
289            && (lazy_def_id(self.interner, target).is_some_and(|t_def| {
290                self.resolver
291                    .is_boxed_def_id(t_def, IntrinsicKind::Function)
292            }) || self
293                .resolver
294                .is_boxed_type_id(target, IntrinsicKind::Function))
295        {
296            let source_eval = self.evaluate_type(source);
297            if self.is_callable_type(source_eval) {
298                // North Star Fix: is_callable_type now respects allow_any correctly.
299                // If it returned true, it means either we're in permissive mode OR
300                // the source is genuinely a callable type.
301                if let Some(dp) = def_entered {
302                    self.def_guard.leave(dp);
303                }
304                self.guard.leave(pair);
305                return SubtypeResult::True;
306            }
307        }
308
309        // =========================================================================
310        // Meta-type evaluation (after cycle detection is set up)
311        // =========================================================================
312        let result = if self.bypass_evaluation {
313            if target == TypeId::NEVER {
314                SubtypeResult::False
315            } else {
316                self.check_subtype_inner(source, target)
317            }
318        } else {
319            let source_eval = self.evaluate_type(source);
320            let target_eval = self.evaluate_type(target);
321
322            if source_eval != source || target_eval != target {
323                self.check_subtype(source_eval, target_eval)
324            } else if target == TypeId::NEVER {
325                SubtypeResult::False
326            } else {
327                self.check_subtype_inner(source, target)
328            }
329        };
330
331        // Cleanup: leave both guards
332        if let Some(dp) = def_entered {
333            self.def_guard.leave(dp);
334        }
335        self.guard.leave(pair);
336
337        // Cache definitive results for cross-checker memoization.
338        if let Some(db) = self.query_db {
339            let key = self.make_cache_key(source, target);
340            match result {
341                SubtypeResult::True => db.insert_subtype_cache(key, true),
342                SubtypeResult::False => db.insert_subtype_cache(key, false),
343                SubtypeResult::CycleDetected | SubtypeResult::DepthExceeded => {}
344            }
345        }
346
347        result
348    }
349}