From 587a8618294b0aed00352ceeecb40a099ed35a98 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Sat, 14 Nov 2026 15:49:45 +0000 Subject: [PATCH] Map.clear and Set.clear keep the bucket table clear() shrank the buckets to length zero. Every lookup after it took the hash modulo that length: the compiler does the remainder in floating point, so it was NaN, and the index made from NaN is undefined. On x86-64 it came out as 0, and the emptied storage read as an empty bucket, so nothing showed; where the optimizer can see the length is zero it emits a trap. clear() now zeroes the buckets where they are. The next insert finds no room in the entries and resizes both, as before. map_clear_reuse.ts looks up, deletes from, walks and refills a cleared map and set. It passed before the change too: it guards the behaviour, it did not show the fault. Tested in release on Windows x64 for gc, rc, none and own, compiled and under the JIT; debug, Linux, x86 and wasm were not built locally. Co-Authored-By: Claude Fable 5.2 --- src/generics/lib.generics.ts | 12 ++++++-- tests/map_clear_reuse.ts | 60 ++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests/map_clear_reuse.ts diff --git a/src/generics/lib.generics.ts b/src/generics/lib.generics.ts index 0160db3..236f91d 100644 --- a/src/generics/lib.generics.ts +++ b/src/generics/lib.generics.ts @@ -2226,10 +2226,14 @@ class Map { return this.count - this.freeCount; } + // The buckets are emptied where they are and keep their length: every lookup takes a hash + // modulo that length, and modulo none it is not a number (the index it then makes is whatever + // the processor gives for one). The next insert finds no room in the entries and resizes both. clear() { const count = this.count; if (count > 0) { - this.buckets.length = 0; + const buckets = this.buckets; + for (let i = 0; i < buckets.length; i++) buckets[i] = 0; this.count = 0; this.freeList = -1; @@ -2626,10 +2630,14 @@ class Set { return this.count - this.freeCount; } + // The buckets are emptied where they are and keep their length: every lookup takes a hash + // modulo that length, and modulo none it is not a number (the index it then makes is whatever + // the processor gives for one). The next insert finds no room in the entries and resizes both. clear() { const count = this.count; if (count > 0) { - this.buckets.length = 0; + const buckets = this.buckets; + for (let i = 0; i < buckets.length; i++) buckets[i] = 0; this.count = 0; this.freeList = -1; diff --git a/tests/map_clear_reuse.ts b/tests/map_clear_reuse.ts new file mode 100644 index 0000000..e7e77f2 --- /dev/null +++ b/tests/map_clear_reuse.ts @@ -0,0 +1,60 @@ +// A map or a set is used again after clear(): looked up while empty, then filled past its first size. +function main() { + const map = new Map(); + map.set("a", 1); + map.set("b", 2); + map.clear(); + assert(map.size == 0, "empty after clear"); + + assert(!map.has("a"), "has, on a cleared map"); + assert(map.get("a") == undefined, "get, on a cleared map"); + assert(!map.delete("a"), "delete, on a cleared map"); + let seen = 0; + map.forEach((v: number, k: string, m: Map) => { seen++; }); + for (const [k, v] of map) seen++; + assert(seen == 0, "nothing to walk"); + + map.set("c", 3); + assert(map.size == 1 && map.get("c") == 3, "set after clear"); + assert(!map.has("a"), "what was cleared stays gone"); + for (let i = 0; i < 100; i++) map.set(`k${i}`, i); + assert(map.size == 101 && map.get("k99") == 99 && map.get("c") == 3, "and grows again"); + + // cleared with deleted entries on the free list + map.delete("k1"); + map.delete("k2"); + map.clear(); + map.set("d", 4); + assert(map.size == 1 && map.get("d") == 4 && !map.has("k3"), "clear after deletes"); + + // clearing an empty map, twice + map.clear(); + map.clear(); + assert(map.size == 0 && !map.has("d"), "clear twice"); + + const numbers = new Map(); + numbers.set(-5, "minus"); + numbers.clear(); + assert(!numbers.has(-5), "number keys"); + numbers.set(7, "seven"); + assert(numbers.get(7) == "seven", "number keys, set after clear"); + + const set = new Set(); + set.add("a"); + set.add("b"); + set.clear(); + assert(set.size == 0 && !set.has("a"), "has, on a cleared set"); + assert(!set.delete("a"), "delete, on a cleared set"); + for (const v of set) seen++; + assert(seen == 0, "nothing to walk in the set"); + set.add("c"); + for (let i = 0; i < 100; i++) set.add(`k${i}`); + assert(set.size == 101 && set.has("c") && set.has("k99") && !set.has("a"), "the set grows again"); + set.delete("k1"); + set.clear(); + set.clear(); + set.add("d"); + assert(set.size == 1 && set.has("d"), "set: clear after a delete"); + + console.log("ALL DONE"); +}