Park typed collection iterators on the entry they read

An early exit from a loop over an iterator-typed Map or Set method parks
the iterator with a second yield of the current entry. That yield read the
entry again by its index, which aborted with a map iteration error when
the loop had deleted the entry. Each step now reads the entry once into a
local that both yields answer.

`entries()` takes the typed walk only for a slot whose yield type is the
key and value pair record. Other yield types, such as
`IterableIterator<any>` or `<unknown>`, keep the checked adapter instead of
failing to compile.
This commit is contained in:
Chris Tate
2026-10-23 14:21:21 -05:00
parent 0e10a01386
commit c296362834
2 changed files with 78 additions and 1 deletions
@@ -84,6 +84,26 @@ function collectionIteratorSlotValue(
collT.kind === "map"
? { kind: "mapIntrinsic", method: m, receiver: coll, args: [index], type: t, loc }
: { kind: "setIntrinsic", method: "iterKey", receiver: coll, args: [index], type: t, loc };
// entries() answers [key, value] pairs: only a slot whose yield type is
// that pair as a record of the two fields takes the typed walk; a tuple
// or other pair type keeps the checked adapter.
if (method === "entries") {
const pair =
expected.yieldT.kind === "record" ? lowerer.shapes.get(expected.yieldT.shapeId) : undefined;
const field = (name: string): IrType | undefined =>
pair?.fields.find((f) => f.name === name)?.type;
const first = field("0");
const second = field("1");
if (
pair === undefined ||
pair.fields.length !== 2 ||
first === undefined ||
second === undefined ||
!typeEquals(first, keyT) ||
!typeEquals(second, valueT)
)
return null;
}
const entry = (): IrExpr =>
method === "keys" || (collT.kind === "set" && method === "values")
? read("iterKey", keyT)
@@ -99,9 +119,13 @@ function collectionIteratorSlotValue(
loc,
};
if (!typeEquals(entry().type, expected.yieldT)) return null;
// The entry is read once per step and kept: the parking yield of an
// early exit answers it again rather than rereading a slot the loop may
// have deleted.
const current = varRef("current.0", expected.yieldT, loc);
const suspend = (): IrStmt => ({
kind: "exprStmt",
expr: { kind: "yieldExpr", value: entry(), type: resumeType(expected.nextT), loc },
expr: { kind: "yieldExpr", value: current, type: resumeType(expected.nextT), loc },
loc,
});
const finish: IrStmt = {
@@ -120,6 +144,7 @@ function collectionIteratorSlotValue(
{ id: "native.0", name: "native", type: DYN, mutable: false },
{ id: "index.0", name: "index", type: F64, mutable: true },
{ id: "resumed.0", name: "resumed", type: BOOL, mutable: true },
{ id: "current.0", name: "current", type: expected.yieldT, mutable: false },
],
body: [
{
@@ -173,6 +198,7 @@ function collectionIteratorSlotValue(
loc,
},
setResumed(false, loc),
{ kind: "varDecl", localId: "current.0", init: entry(), loc },
{
kind: "tryCatch",
tryBody: [suspend(), setResumed(true, loc)],
@@ -56,3 +56,54 @@ const ordered = [...newest(listed)];
console.log(JSON.stringify(ordered), ordered[0] === listed[1]);
const step = newest(listed).next();
console.log(step.done, JSON.stringify(step.value));
// Deleting the current entry before leaving a loop over an iterator-typed
// method: the iterator parks after that entry and continues with the next.
class Store {
readonly rows = new Map<string, number>();
readonly tags = new Set<string>();
values(): IterableIterator<number> {
return this.rows.values();
}
keys(): IterableIterator<string> {
return this.rows.keys();
}
tagValues(): IterableIterator<string> {
return this.tags.values();
}
anyPairs(): IterableIterator<any> {
return this.rows.entries();
}
unknownPairs(): IterableIterator<unknown> {
return this.rows.entries();
}
tagAny(): IterableIterator<any> {
return this.tags.entries();
}
}
const store = new Store();
for (const [k, v] of [["a", 1], ["b", 2], ["c", 3]] as const) store.rows.set(k, v);
for (const t of ["x", "y", "z"]) store.tags.add(t);
console.log(JSON.stringify([...store.anyPairs()]), JSON.stringify([...store.unknownPairs()]));
console.log(JSON.stringify([...store.tagAny()]));
const values = store.values();
for (const v of values) {
if (v === 2) {
store.rows.delete("b");
break;
}
}
console.log([...values].join(), [...store.rows.keys()].join());
for (const k of store.keys()) {
store.rows.delete(k);
break;
}
console.log([...store.rows.keys()].join());
const tags = store.tagValues();
for (const t of tags) {
if (t === "y") {
store.tags.delete("y");
break;
}
}
console.log([...tags].join(), [...store.tags].join());