Skip to main content

nodejs/stdlib/
iterator.rs

1//! Iterator helpers (27.1.4) — `map`, `filter`, `take`, `drop`, `flatMap` and
2//! the terminal `reduce`/`toArray`/`forEach`/`some`/`every`/`find`, plus the
3//! `Iterator` constructor and `Iterator.from`.
4//!
5//! None of it existed: `[1,2,3].values().map(f)` was "map is not a function".
6//!
7//! The helpers are LAZY, which is the whole point of them — `take(3)` on an
8//! endless generator has to stop after three pulls, not materialise anything.
9//! Each is an `@@native = "IteratorHelper"` object holding the iterator it
10//! draws from, so a chain is a chain of pulls; only the terminal operations
11//! drain. That also makes them work over any iterator, including a user object
12//! with a `next` method.
13
14use crate::host::{call_method, is_callable, with_host, JsObj};
15use fusevm::Value;
16use indexmap::IndexMap;
17
18/// The lazy helpers, which return another iterator.
19pub const LAZY: &[&str] = &["map", "filter", "take", "drop", "flatMap"];
20
21/// The terminal operations, which drain the iterator and return a value.
22pub const TERMINAL: &[&str] = &["reduce", "toArray", "forEach", "some", "every", "find"];
23
24/// Everything `Iterator.prototype` carries, so any iterator answers for it.
25pub const METHODS: &[&str] = &[
26    "map",
27    "filter",
28    "take",
29    "drop",
30    "flatMap",
31    "reduce",
32    "toArray",
33    "forEach",
34    "some",
35    "every",
36    "find",
37    "next",
38    "return",
39    "@@iterator",
40];
41
42/// `Iterator`'s own statics.
43pub const STATIC_METHODS: &[&str] = &["from"];
44
45/// True if `name` is one of the helper methods (lazy or terminal).
46pub fn is_helper(name: &str) -> bool {
47    LAZY.contains(&name) || TERMINAL.contains(&name)
48}
49
50/// A lazy helper over `src`.
51fn helper(src: &Value, op: &str, arg: Value) -> Value {
52    with_host(|h| {
53        let mut m = IndexMap::new();
54        m.insert("@@native".into(), h.new_str("IteratorHelper"));
55        m.insert("@@src".into(), src.clone());
56        m.insert("@@op".into(), h.new_str(op));
57        m.insert("@@arg".into(), arg);
58        // `take`/`drop` count down; `flatMap` parks the inner iterator here.
59        m.insert("@@count".into(), Value::Float(0.0));
60        m.insert("@@done".into(), Value::Bool(false));
61        h.new_object(m)
62    })
63}
64
65fn slot(recv: &Value, k: &str) -> Option<Value> {
66    with_host(|h| match h.get(recv) {
67        Some(JsObj::Object(p)) => p.get(k).cloned(),
68        _ => None,
69    })
70}
71
72fn set_slot(recv: &Value, k: &str, v: Value) {
73    with_host(|h| {
74        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
75            p.insert(k.to_string(), v);
76        }
77    });
78}
79
80/// `{ value, done }`.
81fn step(value: Value, done: bool) -> Value {
82    with_host(|h| {
83        let mut m = IndexMap::new();
84        m.insert("value".into(), value);
85        m.insert("done".into(), Value::Bool(done));
86        h.new_object(m)
87    })
88}
89
90/// Mark a helper exhausted and close whatever it was drawing from — a `return`
91/// on any stage of a chain has to reach the generator at the bottom of it.
92pub fn helper_return(recv: &Value) -> Value {
93    let src = slot(recv, "@@src").unwrap_or(Value::Undef);
94    let already = slot(recv, "@@done").is_some_and(|v| with_host(|h| h.truthy(&v)));
95    set_slot(recv, "@@done", Value::Bool(true));
96    if !already {
97        close(&src);
98    }
99    done_step()
100}
101
102/// The `{ value: undefined, done: true }` an exhausted iterator reports.
103pub fn done_step() -> Value {
104    step(Value::Undef, true)
105}
106
107/// Pull one step from an iterator of any kind, as `(value, done)`.
108fn pull(it: &Value) -> Result<(Value, bool), String> {
109    let r = call_method(it, "next", Vec::new())?;
110    let done = crate::builtins::get_property(&r, "done")?;
111    let done = with_host(|h| h.truthy(&done));
112    let value = crate::builtins::get_property(&r, "value")?;
113    Ok((value, done))
114}
115
116/// Close an iterator that is being abandoned early (7.4.9 IteratorClose): its
117/// `return` runs, so a generator's `finally` block fires.
118fn close(it: &Value) {
119    // `get_property`, not `lookup_chain`: a generator's and a helper's `return`
120    // resolve through the stdlib funnel rather than a property map, so the
121    // chain read alone finds neither and every abandoned iterator stayed open.
122    let f = crate::builtins::get_property(it, "return").unwrap_or(Value::Undef);
123    if with_host(|h| is_callable(h, &f)) {
124        let _ = call_method(it, "return", Vec::new());
125    }
126}
127
128/// 27.1.4.x `ToIntegerOrInfinity` for `take`/`drop`'s limit, which must be a
129/// non-negative number — `take(-1)` and `take(NaN)` are RangeErrors, not silent
130/// no-ops.
131fn limit_arg(args: &[Value]) -> Result<f64, String> {
132    let raw = args.first().cloned().unwrap_or(Value::Undef);
133    let n = with_host(|h| h.to_number(&raw));
134    if n.is_nan() {
135        return Err(crate::host::range_error("NaN must be positive"));
136    }
137    if n < 0.0 {
138        let shown = with_host(|h| h.inspect(&Value::Float(n)));
139        return Err(crate::host::range_error(&format!(
140            "{shown} must be positive"
141        )));
142    }
143    Ok(n.trunc())
144}
145
146/// A callable argument, or the `TypeError` a helper raises without one.
147fn fn_arg(args: &[Value]) -> Result<Value, String> {
148    let f = args.first().cloned().unwrap_or(Value::Undef);
149    if !with_host(|h| is_callable(h, &f)) {
150        return Err(crate::host::type_error(
151            &crate::host::not_a_function_message(&f),
152        ));
153    }
154    Ok(f)
155}
156
157/// Dispatch a helper called on the iterator `recv`.
158pub fn call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
159    match method {
160        "map" | "filter" | "flatMap" => Ok(helper(recv, method, fn_arg(args)?)),
161        "take" | "drop" => Ok(helper(recv, method, Value::Float(limit_arg(args)?))),
162        "toArray" => {
163            let mut out = Vec::new();
164            loop {
165                let (v, done) = pull(recv)?;
166                if done {
167                    break;
168                }
169                out.push(v);
170            }
171            Ok(with_host(|h| h.new_array(out)))
172        }
173        "forEach" => {
174            let f = fn_arg(args)?;
175            let mut i = 0.0;
176            loop {
177                let (v, done) = pull(recv)?;
178                if done {
179                    break;
180                }
181                crate::host::invoke(&f, vec![v, Value::Float(i)], None)?;
182                i += 1.0;
183            }
184            Ok(Value::Undef)
185        }
186        "reduce" => {
187            let f = fn_arg(args)?;
188            let mut acc = args.get(1).cloned();
189            let mut i = 0.0;
190            loop {
191                let (v, done) = pull(recv)?;
192                if done {
193                    break;
194                }
195                acc = Some(match acc {
196                    // 27.1.4.11 step 5: with no seed the FIRST value becomes the
197                    // accumulator and the reducer is not called for it.
198                    None => v,
199                    Some(a) => crate::host::invoke(&f, vec![a, v, Value::Float(i)], None)?,
200                });
201                i += 1.0;
202            }
203            acc.ok_or_else(|| {
204                crate::host::type_error("Reduce of a done iterator with no initial value")
205            })
206        }
207        "some" | "every" | "find" => {
208            let f = fn_arg(args)?;
209            let mut i = 0.0;
210            loop {
211                let (v, done) = pull(recv)?;
212                if done {
213                    break;
214                }
215                let r = crate::host::invoke(&f, vec![v.clone(), Value::Float(i)], None)?;
216                let hit = with_host(|h| h.truthy(&r));
217                // Each stops at the first decisive element and CLOSES the
218                // iterator it abandoned.
219                match method {
220                    "some" if hit => {
221                        close(recv);
222                        return Ok(Value::Bool(true));
223                    }
224                    "every" if !hit => {
225                        close(recv);
226                        return Ok(Value::Bool(false));
227                    }
228                    "find" if hit => {
229                        close(recv);
230                        return Ok(v);
231                    }
232                    _ => {}
233                }
234                i += 1.0;
235            }
236            Ok(match method {
237                "some" => Value::Bool(false),
238                "every" => Value::Bool(true),
239                _ => Value::Undef,
240            })
241        }
242        _ => Err(crate::host::type_error(&format!(
243            "{method} is not a function"
244        ))),
245    }
246}
247
248/// One step of a lazy helper: pull from the source until this stage produces a
249/// value or the source runs out.
250pub fn helper_next(recv: &Value) -> Result<Value, String> {
251    if slot(recv, "@@done").is_some_and(|v| with_host(|h| h.truthy(&v))) {
252        return Ok(step(Value::Undef, true));
253    }
254    let src = slot(recv, "@@src").unwrap_or(Value::Undef);
255    let op = slot(recv, "@@op")
256        .map(|v| with_host(|h| h.str_of(&v)))
257        .unwrap_or_default();
258    let arg = slot(recv, "@@arg").unwrap_or(Value::Undef);
259    let finish = || {
260        set_slot(recv, "@@done", Value::Bool(true));
261        step(Value::Undef, true)
262    };
263    match op.as_str() {
264        "take" => {
265            let limit = with_host(|h| h.to_number(&arg));
266            let seen = slot(recv, "@@count")
267                .map(|v| with_host(|h| h.to_number(&v)))
268                .unwrap_or(0.0);
269            if seen >= limit {
270                // The source is abandoned, so it is closed.
271                close(&src);
272                return Ok(finish());
273            }
274            let (v, done) = pull(&src)?;
275            if done {
276                return Ok(finish());
277            }
278            set_slot(recv, "@@count", Value::Float(seen + 1.0));
279            Ok(step(v, false))
280        }
281        "drop" => {
282            let limit = with_host(|h| h.to_number(&arg));
283            let mut dropped = slot(recv, "@@count")
284                .map(|v| with_host(|h| h.to_number(&v)))
285                .unwrap_or(0.0);
286            while dropped < limit {
287                let (_, done) = pull(&src)?;
288                dropped += 1.0;
289                set_slot(recv, "@@count", Value::Float(dropped));
290                if done {
291                    return Ok(finish());
292                }
293            }
294            let (v, done) = pull(&src)?;
295            if done {
296                return Ok(finish());
297            }
298            Ok(step(v, false))
299        }
300        "map" => {
301            let (v, done) = pull(&src)?;
302            if done {
303                return Ok(finish());
304            }
305            let i = slot(recv, "@@count")
306                .map(|x| with_host(|h| h.to_number(&x)))
307                .unwrap_or(0.0);
308            set_slot(recv, "@@count", Value::Float(i + 1.0));
309            let out = crate::host::invoke(&arg, vec![v, Value::Float(i)], None)?;
310            Ok(step(out, false))
311        }
312        "filter" => loop {
313            let (v, done) = pull(&src)?;
314            if done {
315                return Ok(finish());
316            }
317            let i = slot(recv, "@@count")
318                .map(|x| with_host(|h| h.to_number(&x)))
319                .unwrap_or(0.0);
320            set_slot(recv, "@@count", Value::Float(i + 1.0));
321            let keep = crate::host::invoke(&arg, vec![v.clone(), Value::Float(i)], None)?;
322            if with_host(|h| h.truthy(&keep)) {
323                return Ok(step(v, false));
324            }
325        },
326        "flatMap" => loop {
327            // An inner iterator already in flight is drained first.
328            if let Some(inner) = slot(recv, "@@inner") {
329                if !matches!(inner, Value::Undef) {
330                    let (v, done) = pull(&inner)?;
331                    if !done {
332                        return Ok(step(v, false));
333                    }
334                    set_slot(recv, "@@inner", Value::Undef);
335                }
336            }
337            let (v, done) = pull(&src)?;
338            if done {
339                return Ok(finish());
340            }
341            let i = slot(recv, "@@count")
342                .map(|x| with_host(|h| h.to_number(&x)))
343                .unwrap_or(0.0);
344            set_slot(recv, "@@count", Value::Float(i + 1.0));
345            let mapped = crate::host::invoke(&arg, vec![v, Value::Float(i)], None)?;
346            let inner = iterator_of(&mapped)?;
347            set_slot(recv, "@@inner", inner);
348        },
349        // `Iterator.from`'s wrapper: forward each step unchanged.
350        "wrap" => {
351            let (v, done) = pull(&src)?;
352            if done {
353                return Ok(finish());
354            }
355            Ok(step(v, false))
356        }
357        _ => Ok(finish()),
358    }
359}
360
361/// The iterator for a value, via its `Symbol.iterator` — what `flatMap` and
362/// `Iterator.from` both need.
363fn iterator_of(v: &Value) -> Result<Value, String> {
364    // `get_property` rather than `lookup_chain`: an Array's or a String's
365    // `Symbol.iterator` resolves through the stdlib funnel, not a property map,
366    // so the chain read alone reports every builtin as non-iterable.
367    let f = crate::builtins::get_property(v, "@@iterator").unwrap_or(Value::Undef);
368    if with_host(|h| is_callable(h, &f)) {
369        return call_method(v, "@@iterator", Vec::new());
370    }
371    // A raw iterator object — one with `next` but no `Symbol.iterator` — is
372    // taken as-is, which is what `Iterator.from` accepts.
373    let next = crate::builtins::get_property(v, "next").unwrap_or(Value::Undef);
374    if with_host(|h| is_callable(h, &next)) {
375        return Ok(v.clone());
376    }
377    Err(crate::host::type_error(&format!(
378        "{} is not iterable",
379        with_host(|h| h.inspect(v))
380    )))
381}
382
383/// `Iterator.from(x)`.
384pub fn static_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
385    match method {
386        // `Iterator.from(x)` hands back something that HAS the helpers. A plain
387        // object with a `next` method has none of its own, so 27.1.4.1 wraps it
388        // — here in a pass-through helper, which is the same wrapper every
389        // other stage uses.
390        "from" => Some(
391            iterator_of(&args.first().cloned().unwrap_or(Value::Undef)).map(|it| {
392                if super::native_tag(&it).as_deref() == Some("IteratorHelper")
393                    || matches!(
394                        with_host(|h| h.kind_of(&it)),
395                        Some(crate::host::ObjKind::Generator) | Some(crate::host::ObjKind::Iter)
396                    )
397                {
398                    it
399                } else {
400                    helper(&it, "wrap", Value::Undef)
401                }
402            }),
403        ),
404        _ => None,
405    }
406}