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;