Skip to main content

tsz_solver/relations/
subtype_helpers.rs

1//! Subtype checker helper methods.
2//!
3//! Contains intersection optimization, cache key construction,
4//! public entry points, and special-case subtype checks
5//! (Object contract, generic index access).
6
7use crate::relations::subtype::{
8    AnyPropagationMode, INTERSECTION_OBJECT_FAST_PATH_THRESHOLD, SubtypeChecker, SubtypeResult,
9};
10use crate::type_resolver::TypeResolver;
11use crate::types::{ObjectFlags, ObjectShape, RelationCacheKey, TypeId, Visibility};
12use crate::visitor::{
13    callable_shape_id, function_shape_id, index_access_parts, literal_string, object_shape_id,
14    object_with_index_shape_id, type_param_info, union_list_id,
15};
16
17impl<'a, R: TypeResolver> SubtypeChecker<'a, R> {
18    pub(crate) fn can_use_object_intersection_fast_path(&self, members: &[TypeId]) -> bool {
19        if members.len() < INTERSECTION_OBJECT_FAST_PATH_THRESHOLD {
20            return false;
21        }
22
23        for &member in members {
24            let resolved = self.resolve_ref_type(member);
25
26            // Callable requirements must remain explicit intersection members.
27            // Collapsing to a merged object target would drop call signatures.
28            if callable_shape_id(self.interner, resolved).is_some()
29                || function_shape_id(self.interner, resolved).is_some()
30            {
31                return false;
32            }
33
34            let Some(shape_id) = object_shape_id(self.interner, resolved)
35                .or_else(|| object_with_index_shape_id(self.interner, resolved))
36            else {
37                return false;
38            };
39
40            let shape = self.interner.object_shape(shape_id);
41            if !shape.flags.is_empty() {
42                return false;
43            }
44            if shape
45                .properties
46                .iter()
47                .any(|prop| prop.visibility != Visibility::Public)
48            {
49                return false;
50            }
51        }
52
53        true
54    }
55
56    pub(crate) fn build_object_intersection_target(
57        &self,
58        target_intersection: TypeId,
59    ) -> Option<TypeId> {
60        use crate::objects::{PropertyCollectionResult, collect_properties};
61
62        match collect_properties(target_intersection, self.interner, self.resolver) {
63            PropertyCollectionResult::Properties {
64                properties,
65                string_index,
66                number_index,
67            } => {
68                let shape = ObjectShape {
69                    flags: ObjectFlags::empty(),
70                    properties,
71                    string_index,
72                    number_index,
73                    symbol: None,
74                };
75
76                if shape.string_index.is_some() || shape.number_index.is_some() {
77                    Some(self.interner.object_with_index(shape))
78                } else {
79                    Some(self.interner.object(shape.properties))
80                }
81            }
82            PropertyCollectionResult::Any => Some(TypeId::ANY),
83            PropertyCollectionResult::NonObject => None,
84        }
85    }
86
87    /// Check if two object types have overlapping properties.
88    ///
89    /// Returns false if any common property has non-overlapping types.
90    /// Construct a `RelationCacheKey` for the current checker configuration.
91    ///
92    /// This packs the Lawyer-layer flags into a compact cache key to ensure that
93    /// results computed under different rules (strict vs non-strict) don't contaminate each other.
94    pub(crate) const fn make_cache_key(&self, source: TypeId, target: TypeId) -> RelationCacheKey {
95        let mut flags: u16 = 0;
96        if self.strict_null_checks {
97            flags |= RelationCacheKey::FLAG_STRICT_NULL_CHECKS;
98        }
99        if self.strict_function_types {
100            flags |= RelationCacheKey::FLAG_STRICT_FUNCTION_TYPES;
101        }
102        if self.exact_optional_property_types {
103            flags |= RelationCacheKey::FLAG_EXACT_OPTIONAL_PROPERTY_TYPES;
104        }
105        if self.no_unchecked_indexed_access {
106            flags |= RelationCacheKey::FLAG_NO_UNCHECKED_INDEXED_ACCESS;
107        }
108        if self.disable_method_bivariance {
109            flags |= RelationCacheKey::FLAG_DISABLE_METHOD_BIVARIANCE;
110        }
111        if self.allow_void_return {
112            flags |= RelationCacheKey::FLAG_ALLOW_VOID_RETURN;
113        }
114        if self.allow_bivariant_rest {
115            flags |= RelationCacheKey::FLAG_ALLOW_BIVARIANT_REST;
116        }
117        if self.allow_bivariant_param_count {
118            flags |= RelationCacheKey::FLAG_ALLOW_BIVARIANT_PARAM_COUNT;
119        }
120
121        // CRITICAL: Calculate effective `any_mode` based on depth.
122        // If `any_propagation` is `TopLevelOnly` but `depth > 0`, the effective mode is "None".
123        // This ensures that top-level checks don't incorrectly hit cached results from nested checks.
124        let any_mode = match self.any_propagation {
125            AnyPropagationMode::All => 0,
126            AnyPropagationMode::TopLevelOnly if self.guard.depth() == 0 => 1,
127            AnyPropagationMode::TopLevelOnly => 2, // Disabled at depth > 0
128        };
129
130        RelationCacheKey::subtype(source, target, flags, any_mode)
131    }
132
133    /// Check if `source` is a subtype of `target`.
134    /// This is the main entry point for subtype checking.
135    ///
136    /// When a `QueryDatabase` is available (via `with_query_db`), fast-path checks
137    /// (identity, any, unknown, never) are done locally, then the full structural
138    /// check is delegated to the internal `check_subtype` which may use Salsa
139    /// memoization for `evaluate_type` calls.
140    pub fn is_subtype_of(&mut self, source: TypeId, target: TypeId) -> bool {
141        self.check_subtype(source, target).is_true()
142    }
143
144    /// Check if `source` is assignable to `target`.
145    /// This is a strict structural check; use `CompatChecker` for TypeScript assignability rules.
146    pub fn is_assignable_to(&mut self, source: TypeId, target: TypeId) -> bool {
147        self.is_subtype_of(source, target)
148    }
149
150    /// Internal subtype check with cycle detection
151    ///
152    /// # Cycle Detection Strategy (Coinductive Semantics)
153    ///
154    /// This function implements coinductive cycle handling for recursive types.
155    /// The key insight is that we must check for cycles BEFORE evaluation to handle
156    /// "expansive" types like `type Deep<T> = { next: Deep<Box<T>> }` that produce
157    /// fresh `TypeIds` on each evaluation.
158    ///
159    /// The algorithm:
160    /// 1. Fast paths (identity, any, unknown, never)
161    /// 2. **Cycle detection FIRST** (before evaluation!)
162    /// 3. Meta-type evaluation (keyof, conditional, mapped, etc.)
163    /// 4. Structural comparison
164    ///
165    /// Check if source satisfies the Object contract (conflicting properties check).
166    ///
167    /// The `Object` interface allows assignment from almost anything, but if the source
168    /// provides properties that overlap with `Object` (e.g. `toString`), they must be compatible.
169    pub(crate) fn check_object_contract(
170        &mut self,
171        source: TypeId,
172        target: TypeId,
173    ) -> SubtypeResult {
174        use crate::visitor::{object_shape_id, object_with_index_shape_id};
175
176        // Resolve source shape first - if not an object, it's valid (primitives match Object)
177        let source_eval = self.evaluate_type(source);
178        let s_shape_id = match object_shape_id(self.interner, source_eval)
179            .or_else(|| object_with_index_shape_id(self.interner, source_eval))
180        {
181            Some(id) => id,
182            None => return SubtypeResult::True,
183        };
184        let s_shape = self.interner.object_shape(s_shape_id);
185
186        // Resolve Object shape (target)
187        let target_eval = self.evaluate_type(target);
188        let t_shape_id = match object_shape_id(self.interner, target_eval)
189            .or_else(|| object_with_index_shape_id(self.interner, target_eval))
190        {
191            Some(id) => id,
192            None => return SubtypeResult::True, // Should not happen for Object interface
193        };
194        let t_shape = self.interner.object_shape(t_shape_id);
195
196        // Check for conflicting properties
197        for s_prop in &s_shape.properties {
198            // Find property in Object interface (target)
199            if let Some(t_prop) =
200                self.lookup_property(&t_shape.properties, Some(t_shape_id), s_prop.name)
201            {
202                // Found potential conflict: check compatibility
203                let result = self.check_property_compatibility(s_prop, t_prop);
204                if !result.is_true() {
205                    return result;
206                }
207            }
208        }
209
210        SubtypeResult::True
211    }
212
213    /// Check if source is a subtype of an `IndexAccess` target where the index is generic.
214    ///
215    /// If `Target` is `Obj[K]` where `K` is generic, we check if `Source <: Obj[C]`
216    /// where `C` is the constraint of `K`.
217    /// Specifically, if `C` is a union of string literals `"a" | "b"`, we verify
218    /// `Source <: Obj["a"]` AND `Source <: Obj["b"]`.
219    pub(crate) fn check_generic_index_access_subtype(
220        &mut self,
221        source: TypeId,
222        target: TypeId,
223    ) -> bool {
224        let Some((t_obj, t_idx)) = index_access_parts(self.interner, target) else {
225            return false;
226        };
227
228        // Check if index is a generic type parameter
229        let Some(t_param) = type_param_info(self.interner, t_idx) else {
230            return false;
231        };
232
233        let Some(constraint) = t_param.constraint else {
234            return false;
235        };
236
237        // Evaluate the constraint to resolve any type aliases/applications
238        let constraint = self.evaluate_type(constraint);
239
240        // Collect all literal types from the constraint (if it's a union of literals)
241        // If constraint is a single literal, treat as union of 1.
242        let mut literals = Vec::new();
243
244        if let Some(s) = literal_string(self.interner, constraint) {
245            literals.push(self.interner.literal_string_atom(s));
246        } else if let Some(union_id) = union_list_id(self.interner, constraint) {
247            let members = self.interner.type_list(union_id);
248            for &m in members.iter() {
249                if let Some(s) = literal_string(self.interner, m) {
250                    literals.push(self.interner.literal_string_atom(s));
251                } else {
252                    // Constraint contains non-string-literal (e.g. number, or generic).
253                    // Can't distribute.
254                    return false;
255                }
256            }
257        } else {
258            // Constraint is not a literal or union of literals.
259            return false;
260        }
261
262        if literals.is_empty() {
263            return false;
264        }
265
266        // Check source <: Obj[L] for all L in literals
267        for lit_type in literals {
268            // Create IndexAccess(Obj, L)
269            // We use evaluate_type here to potentially resolve it to a concrete property type
270            // (e.g. Obj["a"] -> string)
271            let indexed_access = self.interner.index_access(t_obj, lit_type);
272            let evaluated = self.evaluate_type(indexed_access);
273
274            if !self.check_subtype(source, evaluated).is_true() {
275                return false;
276            }
277        }
278
279        true
280    }
281}