Skip to content

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

Closed

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.

Activity

  1. proggeramlug commented on Aug 7, 2026

    @proggeramlug
    ContributorAuthor

    A third symptom on the same path, surfaced while validating #7564's fix.

    Under the #7154 rooting instruments, Iterator.from(x) takes a bus error on
    retired from-space memory
    while probing its argument:

    [gc-fromspace-protect] FAULT: signal 10 at 0x341c5dcfed8
      last-known object: user_ptr=0x341c5dcfee0 obj_type=2 size=72
    2  ...object::field_get_set::get_field_by_name_tail::get_field_by_name_object_tail + 14288
    3  ...url::search_params::url_search_params_backing_of + 204
    4  js_get_iterator + 544
    5  js_iterator_from + 16
    

    So js_iterator_from → js_get_iterator → a URLSearchParams sniff
    (url_search_params_backing_of) → get_field_by_name_object_tail, which
    derefs a stale address. Repro is the same shape as above, in a loop that
    allocates enough to drive copying minors, compiled with
    PERRY_GC_MOVING_LOOP_POLLS=1 and run under
    PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=800.

    Attribution caveat, stated plainly: this could not be A/B'd against the
    pre-#7564 runtime, because that runtime dies earlier on the same input (at
    iterator_helpers::make_iter_result + 172) and never reaches this site. What
    can be said is that #7564's diff touches none of js_get_iterator,
    url/search_params.rs, or object/field_get_set/ — so this is a
    previously-masked defect rather than a new one, though it is unproven by
    bisect.

    It may well share a root cause with the wrong-answer half of this issue: if
    js_get_iterator's probing is reading fields off a stale receiver, an
    early-out returning something exhausted-looking would explain
    Iterator.from(x) reporting done on its first step.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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