diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5712-map-number-overwrite-at-growth.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5712-map-number-overwrite-at-growth.ts.json new file mode 100644 index 000000000..5f8dcf6b4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5712-map-number-overwrite-at-growth.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5712-map-number-overwrite-at-growth.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_map.c b/packages/runtime/src/scr_map.c index 92c006416..aaedb535e 100644 --- a/packages/runtime/src/scr_map.c +++ b/packages/runtime/src/scr_map.c @@ -1003,10 +1003,19 @@ static inline bool scr_map_append_grows_index(const ScrMap *m) { /* Number keys arrive normalized (+0 for -0, canonical NaN). A directly * indexed map overwrites or appends through its table; a key the table - * cannot hold converts it to hashing first. */ + * cannot hold converts it to hashing first. When an append would grow a + * hashed map's index, the lookup that decides whether the key is new also + * overwrites a key that is already present. */ static void scr_map_set_f64_slot(ScrMap *m, uint64_t key, uint64_t val) { double k = scr_map_slot_to_f64(key); - if (!m->ndense && scr_map_append_grows_index(m) && scr_map_find_f64(m, k) == SCR_MAP_EMPTY) { + if (!m->ndense && scr_map_append_grows_index(m)) { + size_t e = scr_map_find_f64(m, k); + if (e != SCR_MAP_EMPTY) { + uint64_t old = m->entries[e].val; + m->entries[e].val = val; /* unlink before releasing (cycle collector) */ + scr_map_release_val(m, old); + return; + } scr_map_enter_dense(m, k); /* the append would grow the index */ } if (m->ndense) { diff --git a/tests/corpus/5712-map-number-overwrite-at-growth.ts b/tests/corpus/5712-map-number-overwrite-at-growth.ts new file mode 100644 index 000000000..acbdb06e7 --- /dev/null +++ b/tests/corpus/5712-map-number-overwrite-at-growth.ts @@ -0,0 +1,61 @@ +// Number-key Map.set and Set.add of present keys while the map is at its +// bucket-table growth boundary (the next new key grows the index): +// overwrites keep the entry, its position and the size, and release the +// value they replace. + +interface Box { + label: string; +} + +function sparse(i: number): number { + return 1000 + i * 7919; +} + +for (const size of [8, 64, 1024]) { + const nums = new Map(); + const boxes = new Map(); + const names = new Map(); + const seen = new Set(); + for (let i = 0; i < size; i++) { + nums.set(sparse(i), i); + boxes.set(sparse(i), { label: `b${i}` }); + names.set(sparse(i), `n${i}`); + seen.add(sparse(i)); + } + for (let round = 0; round < 3; round++) { + for (let i = size - 1; i >= 0; i--) { + nums.set(sparse(i), i * 10 + round); + boxes.set(sparse(i), { label: `b${i}.${round}` }); + names.set(sparse(i), `n${i}.${round}`.repeat(2)); + seen.add(sparse(i)); + } + } + let sum = 0; + for (const v of nums.values()) sum += v; + const firstKeys = [...nums.keys()].slice(0, 3).join(","); + console.log( + size, + nums.size, + boxes.size, + names.size, + seen.size, + sum, + firstKeys, + boxes.get(sparse(size - 1))?.label, + names.get(sparse(0)), + [...seen].slice(0, 2).join(","), + ); + // One more key grows the index; existing entries are unchanged. + nums.set(-1, -1); + boxes.set(-1, { label: "new" }); + console.log(size, "grown", nums.size, nums.get(sparse(size >> 1)), nums.get(-1), boxes.get(-1)?.label, boxes.get(sparse(1))?.label); +} + +// At the boundary after deletes: a delete frees no bucket, so re-adding +// the deleted key compacts instead of growing. +const churn = new Map(); +for (let i = 0; i < 64; i++) churn.set(sparse(i), i); +churn.delete(sparse(10)); +churn.set(sparse(11), 111); +churn.set(sparse(10), 110); +console.log("churn", churn.size, churn.get(sparse(10)), churn.get(sparse(11)), [...churn.keys()].indexOf(sparse(10)));