Skip to main content

tsz_solver/
expression_ops.rs

1//! Expression type computation operations.
2//!
3//! This module implements AST-agnostic type computation for expressions,
4//! migrated from the Checker as part of the Solver-First architecture refactor.
5//!
6//! These functions operate purely on `TypeIds` and maintain no AST dependencies.
7
8use crate::TypeDatabase;
9use crate::TypeResolver;
10use crate::is_subtype_of;
11use crate::subtype::SubtypeChecker;
12use crate::types::{IntrinsicKind, LiteralValue, TypeData, TypeId};
13
14/// Computes the result type of a conditional expression: `condition ? true_branch : false_branch`.
15///
16/// # Arguments
17/// * `interner` - The type database/interner
18/// * `condition` - Type of the condition expression
19/// * `true_type` - Type of the true branch (`when_true`)
20/// * `false_type` - Type of the false branch (`when_false`)
21///
22/// # Returns
23/// * If condition is definitely truthy: returns `true_type`
24/// * If condition is definitely falsy: returns `false_type`
25/// * Otherwise: returns union of `true_type` and `false_type`
26pub fn compute_conditional_expression_type(
27    interner: &dyn TypeDatabase,
28    condition: TypeId,
29    true_type: TypeId,
30    false_type: TypeId,
31) -> TypeId {
32    // Handle error propagation
33    if condition == TypeId::ERROR {
34        return TypeId::ERROR;
35    }
36    if true_type == TypeId::ERROR {
37        return TypeId::ERROR;
38    }
39    if false_type == TypeId::ERROR {
40        return TypeId::ERROR;
41    }
42
43    // Handle special type constants
44    if condition == TypeId::ANY {
45        // any ? A : B -> A | B
46        return interner.union2(true_type, false_type);
47    }
48    if condition == TypeId::NEVER {
49        // never ? A : B -> never (unreachable)
50        return TypeId::NEVER;
51    }
52
53    // Only short-circuit for literal boolean types (true/false).
54    // Do NOT short-circuit based on general type truthiness (e.g., object types),
55    // because the static type may not reflect the actual runtime value.
56    // Example: `<T>null` has type T (object), but value is null (falsy).
57    // The result type should still be the union of both branches.
58    if let Some(TypeData::Literal(LiteralValue::Boolean(true))) = interner.lookup(condition) {
59        return true_type;
60    }
61    if let Some(TypeData::Literal(LiteralValue::Boolean(false))) = interner.lookup(condition) {
62        return false_type;
63    }
64    // Also short-circuit for null/undefined literal conditions
65    // since these are known to be always falsy at runtime
66    if matches!(
67        interner.lookup(condition),
68        Some(TypeData::Intrinsic(IntrinsicKind::Null))
69            | Some(TypeData::Intrinsic(IntrinsicKind::Undefined))
70    ) {
71        return false_type;
72    }
73
74    // If both branches are the same type, no need for union
75    if true_type == false_type {
76        return true_type;
77    }
78
79    // Default: return union of both branches
80    interner.union2(true_type, false_type)
81}
82
83/// Computes the type of a template literal expression.
84///
85/// Template literals always produce string type in TypeScript.
86///
87/// # Arguments
88/// * `_interner` - The type database/interner
89/// * `parts` - Slice of type IDs for each template part
90///
91/// # Returns
92/// * `TypeId::STRING` - Template literals always produce strings
93pub fn compute_template_expression_type(_interner: &dyn TypeDatabase, parts: &[TypeId]) -> TypeId {
94    // Check for error propagation
95    for &part in parts {
96        if part == TypeId::ERROR {
97            return TypeId::ERROR;
98        }
99        if part == TypeId::NEVER {
100            return TypeId::NEVER;
101        }
102    }
103
104    // Template literals always produce string type
105    // The Checker handles type-checking each part's expression
106    TypeId::STRING
107}
108
109/// Computes the best common type (BCT) of a set of types.
110///
111/// This is used for array literal type inference and other contexts
112/// where a single type must be inferred from multiple candidates.
113///
114/// # Arguments
115/// * `interner` - The type database/interner
116/// * `types` - Slice of type IDs to find the best common type of
117/// * `resolver` - Optional `TypeResolver` for nominal hierarchy lookups (class inheritance)
118///
119/// # Returns
120/// * Empty slice: Returns `TypeId::NEVER`
121/// * Single type: Returns that type
122/// * All same type: Returns that type
123/// * Otherwise: Returns union of all types (or common base class if available)
124///
125/// # Note
126/// When `resolver` is provided, this implements the full TypeScript BCT algorithm:
127/// - Find the first candidate that is a supertype of all others
128/// - Handle literal widening (via `TypeChecker`'s pre-widening)
129/// - Handle base class relationships (Dog + Cat -> Animal)
130pub fn compute_best_common_type<R: TypeResolver>(
131    interner: &dyn TypeDatabase,
132    types: &[TypeId],
133    resolver: Option<&R>,
134) -> TypeId {
135    // Handle empty cases
136    if types.is_empty() {
137        return TypeId::NEVER;
138    }
139
140    // Propagate errors
141    for &ty in types {
142        if ty == TypeId::ERROR {
143            return TypeId::ERROR;
144        }
145    }
146
147    // Single type: return it directly
148    if types.len() == 1 {
149        return types[0];
150    }
151
152    // If all types are the same, no need for union
153    let first = types[0];
154    if types.iter().all(|&ty| ty == first) {
155        return first;
156    }
157
158    // Step 1: Apply literal widening for array literals
159    // When we have multiple literal types of the same primitive kind, widen to the primitive
160    // Example: [1, 2] -> number[], ["a", "b"] -> string[]
161    let widened = widen_literals(interner, types);
162
163    // Step 1.5: Enum member widening
164    // If all candidates are enum members from the same parent enum,
165    // infer the parent enum type directly instead of a large union of members.
166    // This matches TypeScript's behavior for expressions like [E.A, E.B] -> E[].
167    if let Some(res) = resolver
168        && let Some(common_enum_type) = common_parent_enum_type(interner, &widened, res)
169    {
170        return common_enum_type;
171    }
172
173    // OPTIMIZATION: Unit-type fast-path
174    // If ALL types are unit types (tuples of literals/enums, or literals themselves),
175    // no single type can be a supertype of the others (unit types are disjoint).
176    // Skip the O(N²) subtype loop and go directly to union creation.
177    // This turns O(N²) into O(N) for cases like enumLiteralsSubtypeReduction.ts
178    // which has 500 distinct enum-tuple return types.
179    if widened.len() > 2 {
180        let all_unit = widened.iter().all(|&ty| interner.is_unit_type(ty));
181        if all_unit {
182            // All unit types -> no common supertype exists, create union
183            return interner.union(widened.to_vec());
184        }
185    }
186
187    // Step 2: Find the best common type from the candidate types
188    // TypeScript rule: The best common type must be one of the input types
189    // For example: [Dog, Cat] -> Dog | Cat (NOT Animal, even if both extend Animal)
190    //              [Dog, Animal] -> Animal (Animal is in the set and is a supertype)
191    //
192    // OPTIMIZATION: Create ONE SubtypeChecker and reuse it for all comparisons.
193    // Previously, check_subtype() created a new SubtypeChecker (with 3 FxHashSets) for
194    // every single comparison. With N candidates and N types, that's O(N²) allocations.
195    // For enumLiteralsSubtypeReduction.ts (512 return types), this was 262,144 allocations!
196    //
197    // We handle the two cases (with/without resolver) separately because SubtypeChecker<R>
198    // and SubtypeChecker<NoopResolver> are different types.
199    if let Some(res) = resolver {
200        let mut checker = SubtypeChecker::with_resolver(interner, res);
201        for &candidate in &widened {
202            let is_supertype = widened.iter().all(|&ty| {
203                // CRITICAL: Reset the recursion guard counters for each top-level check.
204                // Otherwise, iterations accumulate across the loop and eventually
205                // cause spurious DepthExceeded failures (treated as false).
206                checker.guard.reset();
207                checker.is_subtype_of(ty, candidate)
208            });
209            if is_supertype {
210                return candidate;
211            }
212        }
213    } else {
214        let mut checker = SubtypeChecker::new(interner);
215        for &candidate in &widened {
216            let is_supertype = widened.iter().all(|&ty| {
217                checker.guard.reset();
218                checker.is_subtype_of(ty, candidate)
219            });
220            if is_supertype {
221                return candidate;
222            }
223        }
224    }
225
226    // Step 3: Try to find a common base type for primitives/literals
227    // For example, [string, "hello"] -> string
228    if let Some(base) = find_common_base_type(interner, &widened) {
229        // All types share a common base type
230        if all_types_are_narrower_than_base(interner, &widened, base) {
231            return base;
232        }
233    }
234
235    // Step 4: Default to union of all types
236    interner.union(widened.to_vec())
237}
238
239/// Widen literal types to their primitive base types when appropriate.
240///
241/// This implements Rule #10 (Literal Widening) for BCT:
242/// - Fresh literals in arrays are widened to their primitive types
243/// - Example: [1, 2] -> [number, number]
244/// - Example: ["a", "b"] -> [string, string]
245/// - Example: [1, "a"] -> [number, string] (mixed types)
246///
247/// The widening happens for each literal individually, even in mixed arrays.
248/// Non-literal types are preserved as-is.
249fn widen_literals(interner: &dyn TypeDatabase, types: &[TypeId]) -> Vec<TypeId> {
250    // Widen each literal individually, regardless of what else is in the list.
251    // This matches TypeScript's behavior where [1, "a"] infers as (number | string)[]
252    types
253        .iter()
254        .map(|&ty| {
255            if let Some(key) = interner.lookup(ty)
256                && let crate::types::TypeData::Literal(ref lit) = key
257            {
258                return match lit {
259                    crate::types::LiteralValue::String(_) => TypeId::STRING,
260                    crate::types::LiteralValue::Number(_) => TypeId::NUMBER,
261                    crate::types::LiteralValue::Boolean(_) => TypeId::BOOLEAN,
262                    crate::types::LiteralValue::BigInt(_) => TypeId::BIGINT,
263                };
264            }
265            ty // Non-literal types are preserved
266        })
267        .collect()
268}
269
270/// Find a common base type for a set of types.
271/// For example, [string, "hello"] -> Some(string)
272fn find_common_base_type(interner: &dyn TypeDatabase, types: &[TypeId]) -> Option<TypeId> {
273    if types.is_empty() {
274        return None;
275    }
276
277    // Get the base type of the first type
278    let first_base = get_base_type(interner, types[0])?;
279
280    // Check if all other types have the same base type
281    for &ty in types.iter().skip(1) {
282        let base = get_base_type(interner, ty)?;
283        if base != first_base {
284            return None;
285        }
286    }
287
288    Some(first_base)
289}
290
291/// Get the base type of a type (for literals, this is the primitive type).
292fn get_base_type(interner: &dyn TypeDatabase, ty: TypeId) -> Option<TypeId> {
293    match interner.lookup(ty) {
294        Some(crate::types::TypeData::Literal(ref lit)) => {
295            let base = match lit {
296                crate::types::LiteralValue::String(_) => TypeId::STRING,
297                crate::types::LiteralValue::Number(_) => TypeId::NUMBER,
298                crate::types::LiteralValue::Boolean(_) => TypeId::BOOLEAN,
299                crate::types::LiteralValue::BigInt(_) => TypeId::BIGINT,
300            };
301            Some(base)
302        }
303        _ => Some(ty),
304    }
305}
306
307/// Check if all types are narrower than (subtypes of) the given base type.
308fn all_types_are_narrower_than_base(
309    interner: &dyn TypeDatabase,
310    types: &[TypeId],
311    base: TypeId,
312) -> bool {
313    types.iter().all(|&ty| is_subtype_of(interner, ty, base))
314}
315
316/// Return the common parent enum type if all candidates are members of the same enum.
317fn common_parent_enum_type<R: TypeResolver>(
318    interner: &dyn TypeDatabase,
319    types: &[TypeId],
320    resolver: &R,
321) -> Option<TypeId> {
322    let mut parent_def = None;
323
324    for &ty in types {
325        let TypeData::Enum(def_id, _) = interner.lookup(ty)? else {
326            return None;
327        };
328
329        let current_parent = resolver.get_enum_parent_def_id(def_id).unwrap_or(def_id);
330        if let Some(existing) = parent_def {
331            if existing != current_parent {
332                return None;
333            }
334        } else {
335            parent_def = Some(current_parent);
336        }
337    }
338
339    let parent_def = parent_def?;
340    resolver
341        .resolve_lazy(parent_def, interner)
342        .or_else(|| Some(interner.lazy(parent_def)))
343}
344
345#[cfg(test)]
346#[path = "../tests/expression_ops_tests.rs"]
347mod tests;