Skip to content

Iterator.from(x) returns an already-exhausted iterator; .map/.filter/.take return undefined #7576

Description

@proggeramlug

Iterator.from(x) returns an iterator that is exhausted before it starts, and
the whole TC39 iterator-helpers surface goes with it: .map, .filter,
.take, .drop, .flatMap all return undefined, so any chain throws
TypeError: Cannot read properties of undefined.

Found while working #7564 (which touches the same file). Not caused by it
verified byte-identical on the pre-#7564 runtime.

Repro

function makeCounter(n: number): Iterator<number> {
    let i = 0;
    return {
        next(): IteratorResult<number> {
            return i < n ? { value: i++, done: false } : { value: undefined, done: true };
        },
    };
}

const a = Iterator.from(makeCounter(4));
const out: unknown[] = [];
let s = a.next();
let guard = 0;
while (!s.done && guard++ < 20) { out.push(s.value); s = a.next(); }
console.log("A Iterator.from({next}):", JSON.stringify(out));
console.log("B Iterator.from(arrayIter):", JSON.stringify([...(function () {
    const b = Iterator.from([7, 8, 9][Symbol.iterator]());
    const o: unknown[] = []; let t = b.next(); let g = 0;
    while (!t.done && g++ < 20) { o.push(t.value); t = b.next(); }
    return o;
})()]));

// the same object driven directly, WITHOUT the helper
const c = makeCounter(3);
const outC: unknown[] = []; let sc = c.next(); guard = 0;
while (!sc.done && guard++ < 20) { outC.push(sc.value); sc = c.next(); }
console.log("C bare {next}:", JSON.stringify(outC));

const d = Iterator.from(makeCounter(2));
console.log("D first step:", JSON.stringify(d.next()));
line node 26.5.1 perry
A Iterator.from({next}) [0,1,2,3] []
B Iterator.from(arrayIter) [7,8,9] []
C bare {next} (no helper) [0,1,2] [0,1,2]
D first step {"value":0,"done":false} {"done":true}
E Iterator.from(mapIter) [1,2] []

Row C is the tell: the underlying iterator works fine when driven directly. It
is the helper wrapper that reports done on its very first step, for every
source kind — bare {next} object, array iterator, Map iterator.

And the chain form:

Iterator.from(gen()).map((x) => x * 2).filter((x) => x % 3 !== 0);
// TypeError: Cannot read properties of undefined (reading 'filter')

.map(...) returns undefined, so .filter on it throws.

Where to look

crates/perry-runtime/src/iterator_helpers.rs. Iterator.from is
js_iterator_from (~line 168) building an OP_IDENTITY helper object;
helper_next (~line 188) then drives it through iterator_step. Method
dispatch is dispatch_iterator_helper_method (~line 367), routed from
object/native_call_method/collection_methods.rs:410 and
.../handle_methods.rs:855.

Since .map/.filter return undefined rather than a helper object, the
method-dispatch arm is the first thing to check — an unrecognized method name
falling through to a next-shaped return would explain both symptoms at once
(the identity case reporting done immediately, and the combinators returning
nothing).

Impact

The entire iterator-helpers proposal surface is non-functional, silently for
Iterator.from (wrong answer, no error) and loudly for the combinators. Silent
is the worse half: a program that iterates Iterator.from(x) gets an empty
sequence and no diagnostic.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions