Skip to content

Map/Set delete-during-iteration: size-gate misses delete+add in the same turn (#6158 follow-up) #6165

Closed

Description

@proggeramlug

Summary

The delete-during-iteration re-derive shipped in #6158 gates on a size
decrease
(size < last_seen). That misses a delete and add in the same
iteration turn: the delete compacts the buffer (shifting entries down), the add
restores the size, so the size-gate takes the fast cursor path and skips a
shifted entry.

Repro (conceptual)

const m = new Map([["A",1],["B",2],["C",3],["D",4]]);
for (const [k] of m) { if (k === "A") { m.delete("A"); m.set("E", 5); } /* … */ }
// size stays 4 across the turn → "B" can be skipped

(Flagged by CodeRabbit on #6158; documented there as a known limitation.)

Fix sketch

Gate the re-derive on a mutation/delete counter instead of size. Increment a
per-collection counter in delete_entry_at_index (the shifting op); the
iterator records the counter it last saw and re-derives via find whenever it
changed (adds don't shift, so they needn't trigger it — but a delete always
does, regardless of a compensating add). Applies to both iteration paths:

  • inlined for-of fast path — for_head::map_set_delete_safe_for_of
    (ls_id becomes last-seen-delete-count; new js_map_delete_count /
    js_set_delete_count runtime reads)
  • explicit iterator objects — dispatch_map_iterator_method /
    dispatch_set_iterator_method (field 3)

Counter storage needs GC-rekeying on collection move (mirror MAP_INDEX), or a
header field.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions