From c2963628343abb47685249cedaa2613bb612c888 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 14:21:21 -0500 Subject: [PATCH] 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` or ``, keep the checked adapter instead of failing to compile. --- .../frontend/lowering/iterator-adapters.ts | 28 +++++++++- .../7746-key-order-iterable-iterator.ts | 51 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index a8b4ea496..9f2b1e31f 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -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)], diff --git a/tests/corpus/7746-key-order-iterable-iterator.ts b/tests/corpus/7746-key-order-iterable-iterator.ts index 13f27ee83..d9d4ea15a 100644 --- a/tests/corpus/7746-key-order-iterable-iterator.ts +++ b/tests/corpus/7746-key-order-iterable-iterator.ts @@ -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(); + readonly tags = new Set(); + values(): IterableIterator { + return this.rows.values(); + } + keys(): IterableIterator { + return this.rows.keys(); + } + tagValues(): IterableIterator { + return this.tags.values(); + } + anyPairs(): IterableIterator { + return this.rows.entries(); + } + unknownPairs(): IterableIterator { + return this.rows.entries(); + } + tagAny(): IterableIterator { + 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());