Files
scriptc/tests/corpus/6929-class-iterator-protocol.mjs
T
Chris Tate 120c65bfbb Read an iterator's next once, and await return()'s value in for await
JavaScript reads an iterator's `next` when the iterator opens and keeps
calling that function, so a `next` the iterator assigns while it runs is
not called. The for-of loop and the spread drain over an object-literal
iterator read the field on every step; they now read it once after the
open (classIteratorNextRead) and call the function they read. The
checked-dynamic spread drain (%iterator.pack.checked) does the same,
using the stepping the for-of and Array.from drains already use.

`for await` over a synchronous iterator closes it on an early exit the
way the async-from-sync iterator does: return()'s result is read for done
and value, and the value is awaited, so a pending promise settles before
the loop continues and a rejected one rejects the loop (after a throw from
the body, the body's error still wins).
2026-10-24 20:24:11 -05:00

98 lines
2.5 KiB
JavaScript

// for-of and spread in JavaScript over classes whose [Symbol.iterator]() returns an
// iterator object, with return() called on early exits as Node's IteratorClose does.
class Bag {
constructor(items) { this.items = items; }
[Symbol.iterator]() {
let i = 0;
const items = this.items;
return {
next() {
return i < items.length ? { value: items[i++], done: false } : { value: undefined, done: true };
},
return() {
console.log("return at", i);
return { value: undefined, done: true };
},
};
}
}
const bag = new Bag(["a", "b", "c", "d"]);
function firstLong() {
for (const s of bag) if (s >= "b") return s;
return undefined;
}
console.log(firstLong());
try {
for (const s of bag) if (s === "c") throw new Error("stop at " + s);
} catch (e) {
console.log(e.message);
}
outer: for (const n of [1, 2]) {
for (const s of bag) {
if (s === "b") continue outer;
console.log(n, s);
}
}
let all = "";
for (const s of bag) all += s;
console.log(all);
class Counter {
n = 0;
[Symbol.iterator]() { return new CounterIterator(3); }
}
class CounterIterator {
i = 0;
constructor(limit) { this.limit = limit; }
next() {
return this.i < this.limit ? { value: this.i++, done: false } : { value: -1, done: true };
}
return() {
console.log("class return", this.i);
return { value: -1, done: true };
}
}
for (const v of new Counter()) { if (v === 1) break; console.log("v", v); }
console.log([...new Counter()].join());
class Fussy {
[Symbol.iterator]() {
let i = 0;
return {
next() { return { value: i++, done: i > 5 }; },
return() { if (i >= 0) throw new Error("return failed"); return { value: undefined, done: true }; },
};
}
}
try {
for (const v of new Fussy()) if (v === 1) break;
} catch (e) {
console.log("break:", e.message);
}
try {
for (const v of new Fussy()) if (v === 1) throw new Error("body failed");
} catch (e) {
console.log("throw:", e.message);
}
// An object-literal iterator that replaces its next() while it runs: the
// loop and the spread keep calling the next() they read when it opened.
class Swapping {
[Symbol.iterator]() {
let i = 0;
const it = {
next() {
i++;
if (i === 2) it.next = () => ({ value: -1, done: true });
return { value: i, done: i > 4 };
},
};
return it;
}
}
const swapped = [];
for (const v of new Swapping()) swapped.push(v);
console.log(swapped.join(), [...new Swapping()].join());