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}