-
-
Notifications
You must be signed in to change notification settings - Fork 155
fix(runtime): Set/Map forEach — fused array-receiver reroute + delete-during-iteration #6215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| // `.forEach` on an unknown-typed receiver is statically fused to the ARRAY | ||
| // forEach entry point. When the receiver is actually a native Set or Map — | ||
| // e.g. a collection stored on an object and read back as a property — the | ||
| // fused call treated the SetHeader as an ArrayHeader, feeding hash-table | ||
| // internals to the callback as elements and segfaulting on the first property | ||
| // read. react-server-dom hits exactly this: `request.abortableTasks` is a Set | ||
| // it iterates via `.forEach`, reading `.status` off each task — this crashed | ||
| // every Next.js App Router dynamic route once the RSC flight started flowing | ||
| // (#5989). | ||
| // | ||
| // `forEach` is the only method name the fused array methods share with | ||
| // Set/Map, so the runtime reroute (mirroring the existing typed-array reroute) | ||
| // covers the hazard class. | ||
| // | ||
| // Validated byte-for-byte against `node --experimental-strip-types`. | ||
|
|
||
| // (1) the flight shape: Set stored on an object, read back, forEach'd | ||
| const req: any = { abortableTasks: new Set() }; | ||
| req.abortableTasks.add({ status: 10 }); | ||
| req.abortableTasks.add({ status: 11 }); | ||
| req.abortableTasks.add({ status: 12 }); | ||
| const got: any[] = []; | ||
| req.abortableTasks.forEach((t: any) => got.push(t.status)); | ||
| console.log(JSON.stringify(got), req.abortableTasks.size); | ||
|
|
||
| // (2) Map stored on an object — callback receives (value, key, map) | ||
| const holder: any = { cache: new Map() }; | ||
| holder.cache.set("a", { n: 1 }); | ||
| holder.cache.set("b", { n: 2 }); | ||
| const pairs: string[] = []; | ||
| holder.cache.forEach((v: any, k: any) => pairs.push(`${k}=${v.n}`)); | ||
| console.log(pairs.join(",")); | ||
|
|
||
| // (3) Set forEach argument order: (value, valueAgain, set) | ||
| const s: any = { s: new Set(["x"]) }; | ||
| s.s.forEach((v: any, v2: any, theSet: any) => | ||
| console.log(v === v2, theSet.has("x"), theSet.size), | ||
| ); | ||
|
|
||
| // (4) plain arrays through the same fused path stay correct | ||
| const arrHolder: any = { list: [7, 8] }; | ||
| const items: string[] = []; | ||
| arrHolder.list.forEach((v: any, i: any, a: any) => items.push(`${i}:${v}:${a.length}`)); | ||
| console.log(items.join(",")); | ||
|
|
||
| // (5) delete during Set.forEach (React deletes tasks while sweeping): the | ||
| // backing vector compacts on delete, so naive index advancement skipped the | ||
| // shifted-in next entry. | ||
| const req2: any = { tasks: new Set() }; | ||
| const t1 = { id: 1 }; | ||
| const t2 = { id: 2 }; | ||
| req2.tasks.add(t1); | ||
| req2.tasks.add(t2); | ||
| const seen: number[] = []; | ||
| req2.tasks.forEach((t: any) => { | ||
| seen.push(t.id); | ||
| req2.tasks.delete(t); | ||
| }); | ||
| console.log(JSON.stringify(seen), req2.tasks.size); | ||
|
|
||
| // (6) delete during Map.forEach — same compaction hazard | ||
| const m6: any = { m: new Map() }; | ||
| m6.m.set("a", 1); | ||
| m6.m.set("b", 2); | ||
| m6.m.set("c", 3); | ||
| const seen6: string[] = []; | ||
| m6.m.forEach((v: any, k: any) => { | ||
| seen6.push(`${k}:${v}`); | ||
| m6.m.delete(k); | ||
| }); | ||
| console.log(JSON.stringify(seen6), m6.m.size); | ||
|
|
||
| // (7) delete an EARLIER entry during iteration (must not skip or re-visit) | ||
| const s7 = new Set(["p", "q", "r"]); | ||
| const seen7: string[] = []; | ||
| s7.forEach((v: any) => { | ||
| seen7.push(v); | ||
| if (v === "q") s7.delete("p"); | ||
| }); | ||
| console.log(JSON.stringify(seen7), s7.size); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚖️ Poor tradeoff
Missing add-during-iteration regression test claimed in PR objectives.
The PR objectives state the test file covers "add-during-iteration regression cases," but no such test exists. The upstream
forEachimplementations (bothjs_map_foreach_implandjs_set_foreach_impl) explicitly re-read(*map).size/(*set).sizeeach iteration so that entries appended during the callback are visited per ECMA-262. Without a regression test, a future change that snapshots the initial size could silently break this contract.Suggested addition:
➕ Proposed add-during-iteration tests
// (8) add during Set.forEach — entries appended in the callback MUST be visited const s8 = new Set([1]); const seen8: number[] = []; s8.forEach((v: any) => { seen8.push(v); if (v < 3) s8.add(v + 1); }); console.log(JSON.stringify(seen8), s8.size); // Expected: [1,2,3] 3 // (9) add during Map.forEach — same contract const m9 = new Map([["a", 1]]); const seen9: string[] = []; m9.forEach((v: any, k: any) => { seen9.push(`${k}:${v}`); if (v < 3) m9.set(String.fromCharCode(k.charCodeAt(0) + 1), v + 1); }); console.log(JSON.stringify(seen9), m9.size); // Expected: ["a:1","b:2","c:3"] 3📝 Committable suggestion
🤖 Prompt for AI Agents