Skip to main content

tsz_solver/operations/
iterators.rs

1//! Iterator and async iterator type extraction.
2//!
3//! Extracts iterator protocol information from types, handling both sync
4//! and async iterators. Used by the checker for `for..of` and `for await..of`
5//! loop type checking.
6
7use super::property::PropertyAccessEvaluator;
8use crate::TypeDatabase;
9use crate::types::{PropertyInfo, TypeData, TypeId};
10
11/// Information about an iterator type extracted from a type.
12///
13/// This struct captures the key types needed for iterator/generator type checking:
14/// - The iterator object type itself
15/// - The type yielded by next().value (T in Iterator<T>)
16/// - The type returned when done (`TReturn` in `IteratorResult`<T, `TReturn`>)
17/// - The type accepted by `next()` (`TNext` in Iterator<T, `TReturn`, `TNext`>)
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct IteratorInfo {
20    /// The iterator object type (has `next()` method)
21    pub iterator_type: TypeId,
22    /// The type yielded by the iterator (from `IteratorResult`<T, `TReturn`>)
23    pub yield_type: TypeId,
24    /// The return type when iteration completes
25    pub return_type: TypeId,
26    /// The type accepted by next(val) (contravariant)
27    pub next_type: TypeId,
28}
29
30/// Extract iterator information from a type.
31///
32/// This function handles both sync and async iterators by finding the
33/// appropriate symbol property and extracting the relevant types.
34///
35/// # Arguments
36///
37/// * `db` - The type database/interner
38/// * `type_id` - The type to extract iterator info from
39/// * `is_async` - If true, look for [Symbol.asyncIterator], otherwise [Symbol.iterator]
40///
41/// # Returns
42///
43/// * `Some(IteratorInfo)` - If the type is iterable
44/// * `None` - If the type is not iterable or doesn't have a valid `next()` method
45pub fn get_iterator_info(
46    db: &dyn crate::caches::db::QueryDatabase,
47    type_id: TypeId,
48    is_async: bool,
49) -> Option<IteratorInfo> {
50    use crate::type_queries::is_callable_type;
51
52    // Fast path: Handle intrinsics that are always iterable
53    // The 'any' black hole: any iterates to any
54    if type_id == TypeId::ANY {
55        return Some(IteratorInfo {
56            iterator_type: TypeId::ANY,
57            yield_type: TypeId::ANY,
58            return_type: TypeId::ANY,
59            next_type: TypeId::ANY,
60        });
61    }
62
63    // Fast path: Handle Array and Tuple types
64    if let Some(key) = db.lookup(type_id) {
65        match key {
66            TypeData::Array(elem_type) => {
67                return get_array_iterator_info(type_id, elem_type);
68            }
69            TypeData::Tuple(_) => {
70                return get_tuple_iterator_info(db, type_id);
71            }
72            _ => {}
73        }
74    }
75
76    // Step 1: Find the iterator-producing method
77    let symbol_name = if is_async {
78        "[Symbol.asyncIterator]"
79    } else {
80        "[Symbol.iterator]"
81    };
82
83    let evaluator = PropertyAccessEvaluator::new(db);
84    let iterator_method_type = evaluator
85        .resolve_property_access(type_id, symbol_name)
86        .success_type()?;
87
88    // Step 2: Get the iterator type by "calling" the method
89    // The [Symbol.iterator] property is a method that returns the iterator
90    use crate::type_queries::get_return_type;
91    let iterator_type = if is_callable_type(db, iterator_method_type) {
92        // The symbol is a method - extract its return type
93        // For [Symbol.iterator], the return type is Iterator<T>
94        get_return_type(db, iterator_method_type).unwrap_or(TypeId::ANY)
95    } else {
96        // The symbol property IS the iterator type (non-callable)
97        iterator_method_type
98    };
99
100    // Step 3: Find the next() method on the iterator
101    let next_method_type = evaluator
102        .resolve_property_access(iterator_type, "next")
103        .success_type()?;
104
105    // Step 4: Extract types from the IteratorResult
106    extract_iterator_result_types(db, iterator_type, next_method_type, is_async)
107}
108
109/// Get iterator info for Array types.
110const fn get_array_iterator_info(array_type: TypeId, elem_type: TypeId) -> Option<IteratorInfo> {
111    // Arrays yield their element type
112    // The iterator type for Array<T> has:
113    // - yield: T
114    // - return: undefined
115    // - next: accepts undefined (TNext = undefined)
116    Some(IteratorInfo {
117        iterator_type: array_type,
118        yield_type: elem_type,
119        return_type: TypeId::UNDEFINED,
120        next_type: TypeId::UNDEFINED,
121    })
122}
123
124/// Get iterator info for Tuple types.
125fn get_tuple_iterator_info(db: &dyn TypeDatabase, tuple_type: TypeId) -> Option<IteratorInfo> {
126    // Tuples yield the union of their element types
127    match db.lookup(tuple_type) {
128        Some(TypeData::Tuple(list_id)) => {
129            let elements = db.tuple_list(list_id);
130            let elem_types: Vec<TypeId> = elements.iter().map(|e| e.type_id).collect();
131
132            // Union of all element types (or Never if empty)
133            let yield_type = if elem_types.is_empty() {
134                TypeId::NEVER
135            } else {
136                elem_types
137                    .into_iter()
138                    .reduce(|acc, elem| db.union2(acc, elem))
139                    .unwrap_or(TypeId::NEVER)
140            };
141
142            Some(IteratorInfo {
143                iterator_type: tuple_type,
144                yield_type,
145                return_type: TypeId::UNDEFINED,
146                next_type: TypeId::UNDEFINED,
147            })
148        }
149        _ => None,
150    }
151}
152
153/// Extract T from a Promise<T> type.
154///
155/// Handles two representations:
156/// 1. `Application(base=PROMISE_BASE`, args=[T]) — synthetic promise
157/// 2. Object types with a `then` callback — structurally promise-like
158///
159/// Returns the inner type T, or None if not a promise type.
160fn extract_promise_inner_type(
161    db: &dyn crate::caches::db::QueryDatabase,
162    type_id: TypeId,
163) -> Option<TypeId> {
164    match db.lookup(type_id) {
165        // Application: Promise<T> where base is PROMISE_BASE
166        Some(TypeData::Application(app_id)) => {
167            let app = db.type_application(app_id);
168            if app.base == TypeId::PROMISE_BASE {
169                return app.args.first().copied();
170            }
171            // For other applications, the first arg is typically T
172            // e.g. PromiseLike<T>, custom Promise subclasses
173            app.args.first().copied()
174        }
175        // Object type: look for then(onfulfilled: (value: T) => any) => any
176        Some(TypeData::Object(shape_id)) => {
177            let shape = db.object_shape(shape_id);
178            let then_atom = db.intern_string("then");
179            let then_prop = PropertyInfo::find_in_slice(&shape.properties, then_atom)?;
180            // then is a function: (onfulfilled: (value: T) => any) => any
181            // Extract T from the first parameter of the first parameter
182            match db.lookup(then_prop.type_id) {
183                Some(TypeData::Function(fn_id)) => {
184                    let fn_shape = db.function_shape(fn_id);
185                    let onfulfilled = fn_shape.params.first()?;
186                    // onfulfilled: (value: T) => any — extract T from its first param
187                    match db.lookup(onfulfilled.type_id) {
188                        Some(TypeData::Function(inner_fn_id)) => {
189                            let inner_shape = db.function_shape(inner_fn_id);
190                            inner_shape.params.first().map(|p| p.type_id)
191                        }
192                        _ => None,
193                    }
194                }
195                Some(TypeData::Callable(callable_id)) => {
196                    let callable = db.callable_shape(callable_id);
197                    let sig = callable.call_signatures.first()?;
198                    let onfulfilled = sig.params.first()?;
199                    match db.lookup(onfulfilled.type_id) {
200                        Some(TypeData::Function(inner_fn_id)) => {
201                            let inner_shape = db.function_shape(inner_fn_id);
202                            inner_shape.params.first().map(|p| p.type_id)
203                        }
204                        _ => None,
205                    }
206                }
207                // If then is itself a type (e.g. structural shorthand), just return it
208                _ => Some(then_prop.type_id),
209            }
210        }
211        _ => None,
212    }
213}
214
215/// Extract yield/return/next types from the `next()` method's return type.
216///
217/// For sync iterators: `next()` returns `IteratorResult`<T, `TReturn`>
218/// For async iterators: `next()` returns Promise<`IteratorResult`<T, `TReturn`>>
219fn extract_iterator_result_types(
220    db: &dyn crate::caches::db::QueryDatabase,
221    iterator_type: TypeId,
222    next_method_type: TypeId,
223    is_async: bool,
224) -> Option<IteratorInfo> {
225    use crate::type_queries::is_promise_like;
226
227    // Get the return type and parameter types of next()
228    let (next_return_type, next_params) = match db.lookup(next_method_type) {
229        Some(TypeData::Function(shape_id)) => {
230            let shape = db.function_shape(shape_id);
231            (shape.return_type, shape.params.clone())
232        }
233        Some(TypeData::Callable(shape_id)) => {
234            let shape = db.callable_shape(shape_id);
235            let sig = shape.call_signatures.first()?;
236            (sig.return_type, sig.params.clone())
237        }
238        _ => return None,
239    };
240
241    // For async iterators, unwrap the Promise wrapper
242    let iterator_result_type = if is_async {
243        if is_promise_like(db, next_return_type) {
244            extract_promise_inner_type(db, next_return_type).unwrap_or(next_return_type)
245        } else {
246            return None;
247        }
248    } else {
249        next_return_type
250    };
251
252    // Extract yield_type and return_type from IteratorResult<T, TReturn>
253    // IteratorResult = { value: T, done: false } | { value: TReturn, done: true }
254    let (yield_type, return_type) = extract_iterator_result_value_types(db, iterator_result_type);
255
256    // Extract next_type from the first parameter of next()
257    let next_type = next_params.first().map_or(TypeId::UNDEFINED, |p| p.type_id);
258
259    Some(IteratorInfo {
260        iterator_type,
261        yield_type,
262        return_type,
263        next_type,
264    })
265}
266
267/// Extract yield and return types from an `IteratorResult` type.
268///
269/// `IteratorResult`<T, `TReturn`> is typically:
270///   { value: T, done: false } | { value: `TReturn`, done: true }
271///
272/// Returns (`yield_type`, `return_type`). Yield comes from done:false branches,
273/// return comes from done:true branches.
274fn extract_iterator_result_value_types(
275    db: &dyn crate::caches::db::QueryDatabase,
276    iterator_result_type: TypeId,
277) -> (TypeId, TypeId) {
278    let done_atom = db.intern_string("done");
279    let value_atom = db.intern_string("value");
280
281    match db.lookup(iterator_result_type) {
282        Some(TypeData::Union(list_id)) => {
283            let members = db.type_list(list_id);
284            let mut yield_types = Vec::new();
285            let mut return_types = Vec::new();
286
287            for &member_id in members.iter() {
288                if let Some(TypeData::Object(shape_id)) = db.lookup(member_id) {
289                    let shape = db.object_shape(shape_id);
290                    let value_type = shape
291                        .properties
292                        .iter()
293                        .find(|p| p.name == value_atom)
294                        .map(|p| p.type_id);
295                    let done_type = shape
296                        .properties
297                        .iter()
298                        .find(|p| p.name == done_atom)
299                        .map(|p| p.type_id);
300
301                    match done_type {
302                        // done: true branch → return_type
303                        Some(t) if t == TypeId::BOOLEAN_TRUE => {
304                            if let Some(v) = value_type {
305                                return_types.push(v);
306                            }
307                        }
308                        // done: false branch → yield_type
309                        Some(t) if t == TypeId::BOOLEAN_FALSE => {
310                            if let Some(v) = value_type {
311                                yield_types.push(v);
312                            }
313                        }
314                        // No done property or unknown done → treat as yield
315                        _ => {
316                            if let Some(v) = value_type {
317                                yield_types.push(v);
318                            }
319                        }
320                    }
321                }
322            }
323
324            let yield_type = if yield_types.is_empty() {
325                TypeId::ANY
326            } else {
327                yield_types
328                    .into_iter()
329                    .reduce(|acc, t| db.union2(acc, t))
330                    .unwrap_or(TypeId::ANY)
331            };
332
333            let return_type = if return_types.is_empty() {
334                TypeId::ANY
335            } else {
336                return_types
337                    .into_iter()
338                    .reduce(|acc, t| db.union2(acc, t))
339                    .unwrap_or(TypeId::ANY)
340            };
341
342            (yield_type, return_type)
343        }
344        Some(TypeData::Object(shape_id)) => {
345            let shape = db.object_shape(shape_id);
346            let value_type = shape
347                .properties
348                .iter()
349                .find(|p| p.name == value_atom)
350                .map_or(TypeId::ANY, |p| p.type_id);
351            (value_type, TypeId::ANY)
352        }
353        _ => (TypeId::ANY, TypeId::ANY),
354    }
355}
356
357/// Get the element type yielded by an async iterable type.
358///
359/// This is a convenience wrapper around `get_iterator_info` that extracts
360/// just the yield type from async iterators.
361pub fn get_async_iterable_element_type(
362    db: &dyn crate::caches::db::QueryDatabase,
363    type_id: TypeId,
364) -> TypeId {
365    match get_iterator_info(db, type_id, true) {
366        Some(info) => info.yield_type,
367        None => match get_iterator_info(db, type_id, false) {
368            Some(info) => info.yield_type,
369            None => TypeId::ANY,
370        },
371    }
372}