Repository navigation
Iterator.from(x) returns an already-exhausted iterator; .map/.filter/.take return undefined #7576
Description
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
proggeramlug commentedon Aug 7, 2026
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 onretired from-space memory while probing its argument:
So
js_iterator_from→js_get_iterator→ aURLSearchParamssniff(
url_search_params_backing_of) →get_field_by_name_object_tail, whichderefs 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=1and run underPERRY_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. Whatcan be said is that #7564's diff touches none of
js_get_iterator,url/search_params.rs, orobject/field_get_set/— so this is apreviously-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, anearly-out returning something exhausted-looking would explain
Iterator.from(x)reportingdoneon its first step.test(iterator): copy-on-write matrix + changelog for #7564
test(iterator): copy-on-write matrix + changelog for #7564
fix(runtime): give iterator helpers their own class id (#7576)
fix(runtime): iterator helpers and the String iterator shared a class…
fix(runtime): link built-in iterator prototypes as a class default, n…
fix(runtime): observe prototype replacement in method calls (#9169)