jz 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,12 @@
1
1
  /**
2
2
  * Collection module — Set, Map, HASH (dynamic string-keyed objects).
3
3
  *
4
- * Set: type=8, open addressing hash table. Entries: [hash:f64, key:f64] (16B each).
5
- * Map: type=9, same but entries: [hash:f64, key:f64, val:f64] (24B each).
4
+ * Set: type=8, open addressing hash table. Entries: [hash|seq:8, key:8] (16B each).
5
+ * Map: type=9, same but entries: [hash|seq:8, key:8, val:8] (24B each).
6
6
  * HASH: type=7, same layout as Map but uses content-based string hash + equality.
7
+ * Every table additionally carries an i32 HASH LANE (cap × 4 B) AFTER the entry
8
+ * region — the only thing probes walk (see probeStart) — so allocations are
9
+ * cap × (entrySize + 4) bytes; entry offsets and iteration are unchanged.
7
10
  *
8
11
  * @module collection
9
12
  */
@@ -229,46 +232,56 @@ const litKeyHash = (key) => {
229
232
  return null
230
233
  }
231
234
 
232
- // Equality expressions for probe templates
233
- const sameValueZeroEq = '(call $__same_value_zero (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
234
- const strEq = '(call $__str_eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
235
-
236
- // Hash-first probe guard: each slot stores the key's hash in its low 32 bits
237
- // (high 32 = insertion seq), so a collision step can reject on one i32 compare
238
- // instead of a full key compare. Equal keys always have equal stored hashes
239
- // (the insert wrote strHash/mapHash of that very key), so the guard never
240
- // skips a true match — it only skips the ~all-of-them unequal probes that were
241
- // walking shared-prefix bytes through __str_eq (31M unequal content-compares
242
- // per self-host bench run before this). If-expression for short-circuit:
243
- // i32.and would still evaluate the call.
244
- //
245
- // On a hash hit, an inline `storedKey == queryKey` (one i64.eq on the slot's key
246
- // word) decides the overwhelmingly-common identity case — interned/SSO literals and
247
- // the same heap pointer are bit-equal — WITHOUT the __str_eq / __same_value_zero call
248
- // frame. Sound for both: bit-equality implies string-equality and SameValueZero (the
249
- // only cross-bit-pattern equals — +0/-0, distinct NaN payloads — fall through to the
250
- // full compare, never the reverse). Only a content-equal-but-bit-distinct key (a heap
251
- // string vs a literal) still pays the call. This attacks the __str_eq call tax directly
252
- // and helps the runtimes that DON'T inline tiny callees (wasmtime, wasm2c).
253
- const hashGuard = (eqExpr) =>
254
- `(if (result i32) (i32.eq (i32.load (local.get $slot)) (local.get $h))
255
- (then (if (result i32)
235
+ // Key-equality expressions for probe templates — run only after a LANE hash hit
236
+ // (the probe skeleton compares hashes; these decide the hit). The inline
237
+ // `storedKey == queryKey` bit-eq decides the overwhelmingly-common identity case
238
+ // — interned/SSO literals and the same heap pointer are bit-equal — WITHOUT the
239
+ // __str_eq / __same_value_zero call frame. Sound for both: bit-equality implies
240
+ // string-equality and SameValueZero (the only cross-bit-pattern equals +0/-0,
241
+ // distinct NaN payloads fall through to the full compare, never the reverse).
242
+ const keyEq = (fullEq) =>
243
+ `(if (result i32)
256
244
  (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))
257
245
  (then (i32.const 1))
258
- (else ${eqExpr})))
259
- (else (i32.const 0)))`
260
- const strEqG = hashGuard(strEq)
261
- const sameValueZeroEqG = hashGuard(sameValueZeroEq)
262
-
263
- // Open-addressing probe walked additively by entrySize: avoids an i32.mul + mask per
264
- // step (vs recomputing slot = off + idx*entrySize). Needs $off/$cap/$h set and $end/$slot
265
- // locals declared. `idxExpr` is the first-slot index (defaults to h mod cap; cap is pow2).
246
+ (else ${fullEq}))`
247
+ const strEqG = keyEq('(call $__str_eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))')
248
+ const sameValueZeroEqG = keyEq('(call $__same_value_zero (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))')
249
+ const bitEq = '(i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))'
250
+
251
+ // HASH-LANE probe. Entries keep the classic layout ([hash|seq:8][key:8][val:8]
252
+ // iteration, heal, durable logs, delete-shift and clones are untouched), but a
253
+ // parallel i32 HASH LANE (cap × 4 B, zero-filled, AFTER the entry region) is what
254
+ // probes WALK: one 4-byte load per step, 16 hash checks per cache line where the
255
+ // 24-byte entry stride gave 2-3, and a miss chain touches an 8 kB lane instead of
256
+ // sweeping a 48 kB table through L1 (the wordcount-vs-C probe-footprint gap).
257
+ // Empty ⇔ lane word 0 (hash clamp keeps real hashes ≥ 2); healed zombies KEEP
258
+ // their stale hash in the lane and are passed by the key compare exactly as the
259
+ // entry-walk passed them. $ls walks the lane ($lb/$end its bounds); $slot (the
260
+ // entry address) derives only on a hash hit / at the insert slot. Every table
261
+ // alloc pays entrySize+4 per slot; the entry region offsets are unchanged.
262
+ const LANE = 4
266
263
  const probeStart = (entrySize, idxExpr = '(i32.and (local.get $h) (i32.sub (local.get $cap) (i32.const 1)))') =>
267
- `(local.set $end (i32.add (local.get $off) (i32.mul (local.get $cap) (i32.const ${entrySize}))))
268
- (local.set $slot (i32.add (local.get $off) (i32.mul ${idxExpr} (i32.const ${entrySize}))))`
269
- const probeNext = (entrySize) =>
270
- `(local.set $slot (i32.add (local.get $slot) (i32.const ${entrySize})))
271
- (if (i32.ge_u (local.get $slot) (local.get $end)) (then (local.set $slot (local.get $off))))`
264
+ `(local.set $lb (i32.add (local.get $off) (i32.mul (local.get $cap) (i32.const ${entrySize}))))
265
+ (local.set $end (i32.add (local.get $lb) (i32.shl (local.get $cap) (i32.const 2))))
266
+ (local.set $ls (i32.add (local.get $lb) (i32.shl ${idxExpr} (i32.const 2))))`
267
+ const probeNext = () =>
268
+ `(local.set $ls (i32.add (local.get $ls) (i32.const 4)))
269
+ (if (i32.ge_u (local.get $ls) (local.get $end)) (then (local.set $ls (local.get $lb))))`
270
+ // entry address of the lane cursor's slot
271
+ const slotFromLane = (entrySize) =>
272
+ `(local.set $slot (i32.add (local.get $off)
273
+ (i32.mul (i32.shr_u (i32.sub (local.get $ls) (local.get $lb)) (i32.const 2)) (i32.const ${entrySize}))))`
274
+ // probe-loop locals shared by every template
275
+ const laneLocals = '(local $lb i32) (local $ls i32) (local $hw i32)'
276
+ // cap-tries exhausted with no remembered zombie: rescan for any TOMB key via
277
+ // the shared cold helper (an all-zombies-with-foreign-hashes table —
278
+ // durable-heal-heavy warm embedders only; the lane probe only notices zombies
279
+ // on a hash hit). $__zomb_scan falls back to slot 0 when the table is truly
280
+ // full of live keys, which the 75%-load grow makes unreachable.
281
+ const zombieRescan = (entrySize) => `(if (i32.eqz (local.get $zb)) (then
282
+ (local.set $zb (call $__zomb_scan (local.get $off) (local.get $cap) (i32.const ${entrySize})))
283
+ (local.set $zbl (i32.add (local.get $lb)
284
+ (i32.shl (i32.div_u (i32.sub (local.get $zb) (local.get $off)) (i32.const ${entrySize})) (i32.const 2))))))`
272
285
 
273
286
  // Store a fresh entry's hash word, packing a monotonic insertion sequence
274
287
  // (global $__seq) into its free high 32 bits. The hash itself only ever occupies
@@ -313,6 +326,7 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
313
326
  (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32)
314
327
  (local $size i32) (local $newptr i32) (local $newcap i32) (local $i i32)
315
328
  (local $oldslot i32) (local $newidx i32) (local $newslot i32) (local $zb i32) (local $ztr i32)
329
+ ${laneLocals} (local $zbl i32) (local $nlb i32)
316
330
  ${typeGuard}
317
331
  (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
318
332
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
@@ -327,7 +341,8 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
327
341
  (if (i32.ge_s (i32.mul (local.get $size) (i32.const 4)) (i32.mul (local.get $cap) (i32.const 3)))
328
342
  (then
329
343
  (local.set $newcap (i32.shl (local.get $cap) (i32.const 1)))
330
- (local.set $newptr (call $__alloc_hdr_n (i32.const 0) (local.get $newcap) (i32.const ${entrySize})))
344
+ (local.set $newptr (call $__alloc_hdr_n (i32.const 0) (local.get $newcap) (i32.const ${entrySize + LANE})))
345
+ (local.set $nlb (i32.add (local.get $newptr) (i32.mul (local.get $newcap) (i32.const ${entrySize}))))
331
346
  (i64.store (i32.sub (local.get $newptr) (i32.const 16)) (i64.load (i32.sub (local.get $off) (i32.const 16))))
332
347
  (local.set $i (i32.const 0))
333
348
  (block $rd (loop $rl
@@ -344,6 +359,7 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
344
359
  (br $probe2)))
345
360
  (i64.store (local.get $newslot) (i64.load (local.get $oldslot)))
346
361
  (i64.store (i32.add (local.get $newslot) (i32.const 8)) (i64.load (i32.add (local.get $oldslot) (i32.const 8))))${rehashVal}
362
+ (i32.store (i32.add (local.get $nlb) (i32.shl (local.get $newidx) (i32.const 2))) (local.get $h))
347
363
  (i32.store (i32.sub (local.get $newptr) (i32.const 8))
348
364
  (i32.add (i32.load (i32.sub (local.get $newptr) (i32.const 8))) (i32.const 1)))))
349
365
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
@@ -355,27 +371,39 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
355
371
  (local.set $cap (local.get $newcap))))
356
372
  (local.set $h (call ${hashFn} (local.get $key)))
357
373
  ${probeStart(entrySize)}
358
- ;; zombie-aware probe (durable-slot heal, TOMB_NAN keys): remember the first
359
- ;; zombie, keep probing to empty/match, insert into the zombie when the chain
360
- ;; ends (or when cap tries exhaust physically-full-of-zombies table).
374
+ ;; zombie-aware LANE probe (durable-slot heal, TOMB_NAN keys): a zombie keeps
375
+ ;; its stale hash in the lane, so it is only NOTICED on a hash hit (key reads
376
+ ;; TOMB) reuse still catches the dominant re-insert-same-key case, and the
377
+ ;; cap-tries fallback rescans for any zombie before giving up.
361
378
  (block $done (loop $probe
362
- (if (i64.eqz (i64.load (local.get $slot)))
379
+ (local.set $hw (i32.load (local.get $ls)))
380
+ (if (i32.eqz (local.get $hw))
363
381
  (then
364
- (if (local.get $zb) (then (local.set $slot (local.get $zb))))
382
+ (if (local.get $zb)
383
+ (then (local.set $slot (local.get $zb)) (local.set $ls (local.get $zbl)))
384
+ (else ${slotFromLane(entrySize)}))
365
385
  ${seqStore}
386
+ (i32.store (local.get $ls) (local.get $h))
366
387
  (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}${storeVal}
367
388
  (i32.store (i32.sub (local.get $off) (i32.const 8))
368
389
  (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
369
390
  (br $done)))
370
- (if (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN}))
371
- (then (if (i32.eqz (local.get $zb)) (then (local.set $zb (local.get $slot)))))
372
- (else (if ${eqExpr} ${onMatch})))
373
- ${probeNext(entrySize)}
391
+ (if (i32.eq (local.get $hw) (local.get $h))
392
+ (then
393
+ ${slotFromLane(entrySize)}
394
+ (if (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN}))
395
+ (then (if (i32.eqz (local.get $zb))
396
+ (then (local.set $zb (local.get $slot)) (local.set $zbl (local.get $ls)))))
397
+ (else (if ${eqExpr} ${onMatch})))))
398
+ ${probeNext()}
374
399
  (local.set $ztr (i32.add (local.get $ztr) (i32.const 1)))
375
400
  (if (i32.ge_s (local.get $ztr) (local.get $cap))
376
401
  (then
402
+ ${zombieRescan(entrySize)}
377
403
  (local.set $slot (local.get $zb))
404
+ (local.set $ls (local.get $zbl))
378
405
  ${seqStore}
406
+ (i32.store (local.get $ls) (local.get $h))
379
407
  (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}${storeVal}
380
408
  (i32.store (i32.sub (local.get $off) (i32.const 8))
381
409
  (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
@@ -414,15 +442,20 @@ function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, has
414
442
 
415
443
  return `(func $${name} (param $coll i64) (param $key i64) (result ${rt})
416
444
  (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32) (local $tries i32)
445
+ ${laneLocals}
417
446
  ${typeGuard}
418
447
  (local.set $off ${offExpr})
419
448
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
420
449
  (local.set $h (call ${hashFn} (local.get $key)))
421
450
  ${probeStart(entrySize)}
422
451
  (block $done (loop $probe
423
- (if (i64.eqz (i64.load (local.get $slot))) (then ${onEmpty}))
424
- (if ${eqExpr} (then ${onFound}))
425
- ${probeNext(entrySize)}
452
+ (local.set $hw (i32.load (local.get $ls)))
453
+ (if (i32.eqz (local.get $hw)) (then ${onEmpty}))
454
+ (if (i32.eq (local.get $hw) (local.get $h))
455
+ (then
456
+ ${slotFromLane(entrySize)}
457
+ (if ${eqExpr} (then ${onFound}))))
458
+ ${probeNext()}
426
459
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
427
460
  (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
428
461
  (br $probe)))
@@ -450,6 +483,7 @@ function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
450
483
  return `(func $${name} (param $coll i64) (param $key i64) (result i32)
451
484
  (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32) (local $tries i32)
452
485
  (local $i i32) (local $j i32) (local $k i32) (local $n i32)
486
+ ${laneLocals} (local $li i32) (local $lj i32)
453
487
  (if (i32.ne (call $__ptr_type (local.get $coll)) (i32.const ${expectedType})) (then (return (i32.const 0))))
454
488
  (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
455
489
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
@@ -463,20 +497,29 @@ function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
463
497
  ${probeStart(entrySize)}
464
498
  (block $found
465
499
  (block $absent (loop $probe
466
- (if (i64.eqz (i64.load (local.get $slot))) (then (br $absent)))
467
- (if ${eqExpr} (then (br $found)))
468
- ${probeNext(entrySize)}
500
+ (local.set $hw (i32.load (local.get $ls)))
501
+ (if (i32.eqz (local.get $hw)) (then (br $absent)))
502
+ (if (i32.eq (local.get $hw) (local.get $h))
503
+ (then
504
+ ${slotFromLane(entrySize)}
505
+ (if ${eqExpr} (then (br $found)))))
506
+ ${probeNext()}
469
507
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
470
508
  (br_if $absent (i32.ge_s (local.get $tries) (local.get $cap)))
471
509
  (br $probe)))
472
510
  (return (i32.const 0)))
473
- ;; $slot holds the entry to remove. Walk forward; move back any entry whose home
474
- ;; is not cyclically within (i, j], else it would become unreachable from its home.
511
+ ;; $slot holds the entry to remove ($ls its lane word). Walk forward; move back
512
+ ;; any entry whose home is not cyclically within (i, j], else it would become
513
+ ;; unreachable from its home. The lane word travels with each moved entry.
475
514
  (local.set $i (local.get $slot))
476
515
  (local.set $j (local.get $slot))
516
+ (local.set $li (local.get $ls))
517
+ (local.set $lj (local.get $ls))
477
518
  (block $stop (loop $shift
478
519
  (local.set $j (i32.add (local.get $j) (i32.const ${entrySize})))
479
- (if (i32.ge_u (local.get $j) (local.get $end)) (then (local.set $j (local.get $off))))
520
+ (local.set $lj (i32.add (local.get $lj) (i32.const 4)))
521
+ (if (i32.ge_u (local.get $lj) (local.get $end))
522
+ (then (local.set $j (local.get $off)) (local.set $lj (local.get $lb))))
480
523
  (br_if $stop (i64.eqz (i64.load (local.get $j))))
481
524
  ;; Empty slot ends the cluster (load < 100%). A 100%-full table has none — lookups
482
525
  ;; tolerate that via the $tries<cap bound, so delete must too: after $cap advances $j
@@ -489,10 +532,13 @@ function genDelete(name, entrySize, hashFn, eqExpr, expectedType) {
489
532
  (then (br_if $shift (i32.and (i32.lt_u (local.get $i) (local.get $k)) (i32.le_u (local.get $k) (local.get $j)))))
490
533
  (else (br_if $shift (i32.or (i32.lt_u (local.get $i) (local.get $k)) (i32.le_u (local.get $k) (local.get $j))))))
491
534
  (memory.copy (local.get $i) (local.get $j) (i32.const ${entrySize}))
535
+ (i32.store (local.get $li) (i32.load (local.get $lj)))
492
536
  (local.set $i (local.get $j))
537
+ (local.set $li (local.get $lj))
493
538
  (br $shift)))
494
539
  (i64.store (local.get $i) (i64.const 0))
495
540
  (i64.store (i32.add (local.get $i) (i32.const 8)) (i64.const 0))
541
+ (i32.store (local.get $li) (i32.const 0))
496
542
  ${enumcInval}(i32.store (i32.sub (local.get $off) (i32.const 8))
497
543
  (i32.sub (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
498
544
  (i32.const 1))`
@@ -520,6 +566,7 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
520
566
  (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32)
521
567
  (local $size i32) (local $newptr i32) (local $newcap i32) (local $i i32)
522
568
  (local $oldslot i32) (local $newidx i32) (local $newslot i32) (local $zb i32) (local $ztr i32)
569
+ ${laneLocals} (local $zbl i32) (local $nlb i32)
523
570
  ${typeGuard}
524
571
  (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
525
572
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
@@ -534,7 +581,8 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
534
581
  (if (i32.ge_s (i32.mul (local.get $size) (i32.const 4)) (i32.mul (local.get $cap) (i32.const 3)))
535
582
  (then
536
583
  (local.set $newcap (i32.shl (local.get $cap) (i32.const 1)))
537
- (local.set $newptr (call $__alloc_hdr_n (i32.const 0) (local.get $newcap) (i32.const ${entrySize})))
584
+ (local.set $newptr (call $__alloc_hdr_n (i32.const 0) (local.get $newcap) (i32.const ${entrySize + LANE})))
585
+ (local.set $nlb (i32.add (local.get $newptr) (i32.mul (local.get $newcap) (i32.const ${entrySize}))))
538
586
  (local.set $i (i32.const 0))
539
587
  (block $rd (loop $rl
540
588
  (br_if $rd (i32.ge_s (local.get $i) (local.get $cap)))
@@ -551,6 +599,7 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
551
599
  (i64.store (local.get $newslot) (i64.load (local.get $oldslot)))
552
600
  (i64.store (i32.add (local.get $newslot) (i32.const 8)) (i64.load (i32.add (local.get $oldslot) (i32.const 8))))
553
601
  (i64.store (i32.add (local.get $newslot) (i32.const 16)) (i64.load (i32.add (local.get $oldslot) (i32.const 16))))
602
+ (i32.store (i32.add (local.get $nlb) (i32.shl (local.get $newidx) (i32.const 2))) (local.get $h))
554
603
  (i32.store (i32.sub (local.get $newptr) (i32.const 8))
555
604
  (i32.add (i32.load (i32.sub (local.get $newptr) (i32.const 8))) (i32.const 1)))))
556
605
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
@@ -576,29 +625,40 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
576
625
  ;; Insert/update
577
626
  (local.set $h (call ${hashFn} (local.get $key)))
578
627
  ${probeStart(entrySize)}
579
- ;; zombie-aware probe (durable-slot heal, TOMB_NAN keys) — see genUpsert.
628
+ ;; zombie-aware LANE probe (durable-slot heal, TOMB_NAN keys) — see genUpsert.
580
629
  (block $done (loop $probe
581
- (if (i64.eqz (i64.load (local.get $slot)))
630
+ (local.set $hw (i32.load (local.get $ls)))
631
+ (if (i32.eqz (local.get $hw))
582
632
  (then
583
- (if (local.get $zb) (then (local.set $slot (local.get $zb))))
633
+ (if (local.get $zb)
634
+ (then (local.set $slot (local.get $zb)) (local.set $ls (local.get $zbl)))
635
+ (else ${slotFromLane(entrySize)}))
584
636
  ${seqStore}
637
+ (i32.store (local.get $ls) (local.get $h))
585
638
  (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
586
639
  (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
587
640
  (i32.store (i32.sub (local.get $off) (i32.const 8))
588
641
  (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
589
642
  (br $done)))
590
- (if (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN}))
591
- (then (if (i32.eqz (local.get $zb)) (then (local.set $zb (local.get $slot)))))
592
- (else (if ${eqExpr}
593
- (then
594
- (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
595
- (br $done)))))
596
- ${probeNext(entrySize)}
643
+ (if (i32.eq (local.get $hw) (local.get $h))
644
+ (then
645
+ ${slotFromLane(entrySize)}
646
+ (if (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN}))
647
+ (then (if (i32.eqz (local.get $zb))
648
+ (then (local.set $zb (local.get $slot)) (local.set $zbl (local.get $ls)))))
649
+ (else (if ${eqExpr}
650
+ (then
651
+ (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
652
+ (br $done)))))))
653
+ ${probeNext()}
597
654
  (local.set $ztr (i32.add (local.get $ztr) (i32.const 1)))
598
655
  (if (i32.ge_s (local.get $ztr) (local.get $cap))
599
656
  (then
657
+ ${zombieRescan(entrySize)}
600
658
  (local.set $slot (local.get $zb))
659
+ (local.set $ls (local.get $zbl))
601
660
  ${seqStore}
661
+ (i32.store (local.get $ls) (local.get $h))
602
662
  (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
603
663
  (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
604
664
  (i32.store (i32.sub (local.get $off) (i32.const 8))
@@ -624,6 +684,7 @@ function genSlotUpsert(name, entrySize, hashFn, eqExpr) {
624
684
  (local $size i32) (local $newptr i32) (local $newcap i32) (local $i i32)
625
685
  (local $oldslot i32) (local $newidx i32) (local $newslot i32) (local $zb i32) (local $ztr i32)
626
686
  (local $kaux i32) (local $koff i32)
687
+ ${laneLocals} (local $zbl i32) (local $nlb i32)
627
688
  (if (i32.ne (call $__ptr_type (local.get $obj)) (i32.const ${PTR.HASH}))
628
689
  (then (return (i32.const 0))))
629
690
  (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
@@ -638,7 +699,8 @@ function genSlotUpsert(name, entrySize, hashFn, eqExpr) {
638
699
  (if (i32.ge_s (i32.mul (local.get $size) (i32.const 4)) (i32.mul (local.get $cap) (i32.const 3)))
639
700
  (then
640
701
  (local.set $newcap (i32.shl (local.get $cap) (i32.const 1)))
641
- (local.set $newptr (call $__alloc_hdr_n (i32.const 0) (local.get $newcap) (i32.const ${entrySize})))
702
+ (local.set $newptr (call $__alloc_hdr_n (i32.const 0) (local.get $newcap) (i32.const ${entrySize + LANE})))
703
+ (local.set $nlb (i32.add (local.get $newptr) (i32.mul (local.get $newcap) (i32.const ${entrySize}))))
642
704
  (local.set $i (i32.const 0))
643
705
  (block $rd (loop $rl
644
706
  (br_if $rd (i32.ge_s (local.get $i) (local.get $cap)))
@@ -655,6 +717,7 @@ function genSlotUpsert(name, entrySize, hashFn, eqExpr) {
655
717
  (i64.store (local.get $newslot) (i64.load (local.get $oldslot)))
656
718
  (i64.store (i32.add (local.get $newslot) (i32.const 8)) (i64.load (i32.add (local.get $oldslot) (i32.const 8))))
657
719
  (i64.store (i32.add (local.get $newslot) (i32.const 16)) (i64.load (i32.add (local.get $oldslot) (i32.const 16))))
720
+ (i32.store (i32.add (local.get $nlb) (i32.shl (local.get $newidx) (i32.const 2))) (local.get $h))
658
721
  (i32.store (i32.sub (local.get $newptr) (i32.const 8))
659
722
  (i32.add (i32.load (i32.sub (local.get $newptr) (i32.const 8))) (i32.const 1)))))
660
723
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
@@ -694,24 +757,35 @@ function genSlotUpsert(name, entrySize, hashFn, eqExpr) {
694
757
  : `(local.set $h (call ${hashFn} (local.get $key)))`}
695
758
  ${probeStart(entrySize)}
696
759
  (block $done (loop $probe
697
- (if (i64.eqz (i64.load (local.get $slot)))
760
+ (local.set $hw (i32.load (local.get $ls)))
761
+ (if (i32.eqz (local.get $hw))
698
762
  (then
699
- (if (local.get $zb) (then (local.set $slot (local.get $zb))))
763
+ (if (local.get $zb)
764
+ (then (local.set $slot (local.get $zb)) (local.set $ls (local.get $zbl)))
765
+ (else ${slotFromLane(entrySize)}))
700
766
  ${seqStore}
767
+ (i32.store (local.get $ls) (local.get $h))
701
768
  (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
702
769
  (i64.store (i32.add (local.get $slot) (i32.const 16)) (i64.const ${UNDEF_NAN}))
703
770
  (i32.store (i32.sub (local.get $off) (i32.const 8))
704
771
  (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
705
772
  (br $done)))
706
- (if (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN}))
707
- (then (if (i32.eqz (local.get $zb)) (then (local.set $zb (local.get $slot)))))
708
- (else (if ${eqExpr} (then (br $done)))))
709
- ${probeNext(entrySize)}
773
+ (if (i32.eq (local.get $hw) (local.get $h))
774
+ (then
775
+ ${slotFromLane(entrySize)}
776
+ (if (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN}))
777
+ (then (if (i32.eqz (local.get $zb))
778
+ (then (local.set $zb (local.get $slot)) (local.set $zbl (local.get $ls)))))
779
+ (else (if ${eqExpr} (then (br $done)))))))
780
+ ${probeNext()}
710
781
  (local.set $ztr (i32.add (local.get $ztr) (i32.const 1)))
711
782
  (if (i32.ge_s (local.get $ztr) (local.get $cap))
712
783
  (then
784
+ ${zombieRescan(entrySize)}
713
785
  (local.set $slot (local.get $zb))
786
+ (local.set $ls (local.get $zbl))
714
787
  ${seqStore}
788
+ (i32.store (local.get $ls) (local.get $h))
715
789
  (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
716
790
  (i64.store (i32.add (local.get $slot) (i32.const 16)) (i64.const ${UNDEF_NAN}))
717
791
  (i32.store (i32.sub (local.get $off) (i32.const 8))
@@ -724,6 +798,7 @@ function genSlotUpsert(name, entrySize, hashFn, eqExpr) {
724
798
  function genLookupStrict(name, entrySize, hashFn, eqExpr, expectedType, missing = UNDEF_NAN) {
725
799
  return `(func $${name} (param $coll i64) (param $key i64) (result i64)
726
800
  (local $off i32) (local $cap i32) (local $h i32) (local $end i32) (local $slot i32) (local $tries i32)
801
+ ${laneLocals}
727
802
  (if (i32.ne
728
803
  (i32.wrap_i64 (i64.and (i64.shr_u (local.get $coll) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
729
804
  (i32.const ${expectedType}))
@@ -739,11 +814,15 @@ function genLookupStrict(name, entrySize, hashFn, eqExpr, expectedType, missing
739
814
  (local.set $h (call ${hashFn} (local.get $key)))
740
815
  ${probeStart(entrySize)}
741
816
  (block $done (loop $probe
742
- (if (i64.eqz (i64.load (local.get $slot)))
817
+ (local.set $hw (i32.load (local.get $ls)))
818
+ (if (i32.eqz (local.get $hw))
743
819
  (then (return (i64.const ${missing}))))
744
- (if ${eqExpr}
745
- (then (return (i64.load (i32.add (local.get $slot) (i32.const 16))))))
746
- ${probeNext(entrySize)}
820
+ (if (i32.eq (local.get $hw) (local.get $h))
821
+ (then
822
+ ${slotFromLane(entrySize)}
823
+ (if ${eqExpr}
824
+ (then (return (i64.load (i32.add (local.get $slot) (i32.const 16))))))))
825
+ ${probeNext()}
747
826
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
748
827
  (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
749
828
  (br $probe)))
@@ -769,6 +848,7 @@ function genLookupStrictPrehashed(name, entrySize, eqExpr, expectedType, missing
769
848
  (then ${onEmpty}))`
770
849
  return `(func $${name} (param $coll i64) (param $key i64) (param $h i32) (result ${rt})
771
850
  (local $off i32) (local $cap i32) (local $end i32) (local $slot i32) (local $tries i32)
851
+ ${laneLocals}
772
852
  ${typeGuard}
773
853
  (local.set $off (i32.wrap_i64 (i64.and (local.get $coll) (i64.const ${LAYOUT.OFFSET_MASK}))))
774
854
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
@@ -780,9 +860,13 @@ function genLookupStrictPrehashed(name, entrySize, eqExpr, expectedType, missing
780
860
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))))
781
861
  ${probeStart(entrySize)}
782
862
  (block $done (loop $probe
783
- (if (i64.eqz (i64.load (local.get $slot))) (then ${onEmpty}))
784
- (if ${eqExpr} (then ${onFound}))
785
- ${probeNext(entrySize)}
863
+ (local.set $hw (i32.load (local.get $ls)))
864
+ (if (i32.eqz (local.get $hw)) (then ${onEmpty}))
865
+ (if (i32.eq (local.get $hw) (local.get $h))
866
+ (then
867
+ ${slotFromLane(entrySize)}
868
+ (if ${eqExpr} (then ${onFound}))))
869
+ ${probeNext()}
786
870
  (local.set $tries (i32.add (local.get $tries) (i32.const 1)))
787
871
  (br_if $done (i32.ge_s (local.get $tries) (local.get $cap)))
788
872
  (br $probe)))
@@ -792,6 +876,7 @@ function genLookupStrictPrehashed(name, entrySize, eqExpr, expectedType, missing
792
876
  function genUpsertStrictPrehashed(name, entrySize, eqExpr, expectedType) {
793
877
  return `(func $${name} (param $obj i64) (param $key i64) (param $h i32) (param $val i64) (result i64)
794
878
  (local $off i32) (local $cap i32) (local $end i32) (local $slot i32) (local $zb i32) (local $ztr i32)
879
+ ${laneLocals} (local $zbl i32)
795
880
  (if (i32.ne
796
881
  (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
797
882
  (i32.const ${expectedType}))
@@ -805,29 +890,40 @@ function genUpsertStrictPrehashed(name, entrySize, eqExpr, expectedType) {
805
890
  (local.set $off (call $__ptr_offset_fwd (local.get $off)))
806
891
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))))
807
892
  ${probeStart(entrySize)}
808
- ;; zombie-aware probe (durable-slot heal, TOMB_NAN keys) — see genUpsert.
893
+ ;; zombie-aware LANE probe (durable-slot heal, TOMB_NAN keys) — see genUpsert.
809
894
  (block $done (loop $probe
810
- (if (i64.eqz (i64.load (local.get $slot)))
895
+ (local.set $hw (i32.load (local.get $ls)))
896
+ (if (i32.eqz (local.get $hw))
811
897
  (then
812
- (if (local.get $zb) (then (local.set $slot (local.get $zb))))
898
+ (if (local.get $zb)
899
+ (then (local.set $slot (local.get $zb)) (local.set $ls (local.get $zbl)))
900
+ (else ${slotFromLane(entrySize)}))
813
901
  ${seqStore}
902
+ (i32.store (local.get $ls) (local.get $h))
814
903
  (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
815
904
  (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
816
905
  (i32.store (i32.sub (local.get $off) (i32.const 8))
817
906
  (i32.add (i32.load (i32.sub (local.get $off) (i32.const 8))) (i32.const 1)))
818
907
  (br $done)))
819
- (if (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN}))
820
- (then (if (i32.eqz (local.get $zb)) (then (local.set $zb (local.get $slot)))))
821
- (else (if ${eqExpr}
822
- (then
823
- (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
824
- (br $done)))))
825
- ${probeNext(entrySize)}
908
+ (if (i32.eq (local.get $hw) (local.get $h))
909
+ (then
910
+ ${slotFromLane(entrySize)}
911
+ (if (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN}))
912
+ (then (if (i32.eqz (local.get $zb))
913
+ (then (local.set $zb (local.get $slot)) (local.set $zbl (local.get $ls)))))
914
+ (else (if ${eqExpr}
915
+ (then
916
+ (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
917
+ (br $done)))))))
918
+ ${probeNext()}
826
919
  (local.set $ztr (i32.add (local.get $ztr) (i32.const 1)))
827
920
  (if (i32.ge_s (local.get $ztr) (local.get $cap))
828
921
  (then
922
+ ${zombieRescan(entrySize)}
829
923
  (local.set $slot (local.get $zb))
924
+ (local.set $ls (local.get $zbl))
830
925
  ${seqStore}
926
+ (i32.store (local.get $ls) (local.get $h))
831
927
  (i64.store (i32.add (local.get $slot) (i32.const 8)) (local.get $key))${durableEntryLogIR('slot', 'off')}
832
928
  (i64.store (i32.add (local.get $slot) (i32.const 16)) (local.get $val))${durableSlotLogIR('slot', 16, 'val')}
833
929
  (i32.store (i32.sub (local.get $off) (i32.const 8))
@@ -865,7 +961,7 @@ export default (ctx) => {
865
961
  // log (durableEntryLogIR) still calls $__durable_slot_log — without the
866
962
  // explicit edge the kernel leg drops the helper (auto-scan divergence, the
867
963
  // selfhost-includes class) and every `new Set(...)` fails to compile there.
868
- __set_add: () => [...(ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr_n', '__ext_set'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr_n']), ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []), ...slotLogDeps()],
964
+ __set_add: () => [...(ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr_n', '__zomb_scan', '__ext_set'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr_n', '__zomb_scan']), ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []), ...slotLogDeps()],
869
965
  __set_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__ext_has'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd'],
870
966
  __set_delete: ['__map_hash', '__same_value_zero'],
871
967
  __set_add_all: ['__ptr_offset', '__ptr_offset_fwd', '__cap', '__len', '__coll_order', '__set_add'],
@@ -874,7 +970,7 @@ export default (ctx) => {
874
970
  __sclone: ['__sclone_rec', '__mkptr', '__alloc_hdr_n'],
875
971
  __sclone_rec: ['__ptr_type', '__ptr_offset', '__ptr_offset_fwd', '__ptr_aux', '__is_nullish', '__len', '__alloc', '__alloc_hdr_n', '__mkptr', '__map_get', '__map_set', '__set_add', '__coll_order', '__arr_from', '__obj_clone', '__sclone_hash_vals'],
876
972
  __sclone_hash_vals: ['__sclone_rec'],
877
- __map_set: () => [...(ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr_n', '__ext_set'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr_n']), ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []), ...slotLogDeps()],
973
+ __map_set: () => [...(ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr_n', '__zomb_scan', '__ext_set'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__alloc_hdr_n', '__zomb_scan']), ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []), ...slotLogDeps()],
878
974
  __map_get: () => ctx.features.external ? ['__ext_prop', '__map_set', '__ptr_offset', '__ptr_offset_fwd'] : ['__map_set', '__ptr_offset', '__ptr_offset_fwd'],
879
975
  __map_get_h: () => ctx.features.external ? ['__ext_prop', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd'] : ['__same_value_zero', '__ptr_offset', '__ptr_offset_fwd'],
880
976
  __map_has: () => ctx.features.external ? ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd', '__ext_has'] : ['__map_hash', '__same_value_zero', '__ptr_offset', '__ptr_offset_fwd'],
@@ -888,6 +984,7 @@ export default (ctx) => {
888
984
  __map_new: ['__alloc_hdr_n'],
889
985
  __hash_set: () => [
890
986
  ...(ctx.features.external ? ['__str_hash', '__str_eq', '__ptr_type', '__ext_set', '__dyn_set'] : ['__str_hash', '__str_eq', '__ptr_type', '__dyn_set']),
987
+ '__zomb_scan',
891
988
  ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []),
892
989
  ...slotLogDeps(),
893
990
  ],
@@ -901,12 +998,12 @@ export default (ctx) => {
901
998
  __hash_new_small: ['__alloc_hdr_n', '__mkptr'],
902
999
  __hash_get_local: ['__str_hash', '__str_eq'],
903
1000
  __hash_get_local_h: ['__str_eq'],
904
- __hash_set_local_h: () => ['__str_eq', ...slotLogDeps()],
905
- __hash_set_local: () => ['__str_hash', '__str_eq', '__alloc_hdr_n', '__mkptr', ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []), ...slotLogDeps()],
906
- __hash_slot: () => ['__str_hash', '__str_eq', '__alloc_hdr_n', '__ptr_type', '__ptr_offset', '__ptr_offset_fwd', ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []), ...slotLogDeps()],
1001
+ __hash_set_local_h: () => ['__str_eq', '__zomb_scan', ...slotLogDeps()],
1002
+ __hash_set_local: () => ['__str_hash', '__str_eq', '__alloc_hdr_n', '__mkptr', '__zomb_scan', ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []), ...slotLogDeps()],
1003
+ __hash_slot: () => ['__str_hash', '__str_eq', '__alloc_hdr_n', '__ptr_type', '__ptr_offset', '__ptr_offset_fwd', '__zomb_scan', ...(needsDurableFwdLog() ? ['__durable_fwd_log'] : []), ...slotLogDeps()],
907
1004
  __slot_write: () => slotLogDeps(),
908
1005
  __ihash_get_local: ['__map_hash'],
909
- __ihash_set_local: () => ['__map_hash', '__alloc_hdr_n', '__mkptr', ...slotLogDeps()],
1006
+ __ihash_set_local: () => ['__map_hash', '__alloc_hdr_n', '__mkptr', '__zomb_scan', ...slotLogDeps()],
910
1007
  __dyn_get_t: ['__dyn_get_t_h', '__str_hash', '__is_str_key', '__to_str'],
911
1008
  __dyn_get_t_h: ['__ihash_get_local', '__str_eq', '__is_nullish', '__hash_get_local_h', '__str_arr_idx', '__str_byteLen'],
912
1009
  __dyn_get: ['__dyn_get_t', '__ptr_type'],
@@ -1038,14 +1135,14 @@ export default (ctx) => {
1038
1135
  // __map_new() → f64 — allocate empty Map (for JSON.parse, runtime creation)
1039
1136
  ctx.core.stdlib['__map_new'] = `(func $__map_new (result f64)
1040
1137
  (call $__mkptr (i32.const ${PTR.MAP}) (i32.const 0)
1041
- (call $__alloc_hdr_n (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY}))))`
1138
+ (call $__alloc_hdr_n (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY + LANE}))))`
1042
1139
 
1043
1140
  // === Set ===
1044
1141
 
1045
1142
  ctx.core.emit['new.Set'] = (iterExpr) => {
1046
1143
  ctx.features.set = true
1047
1144
  if (iterExpr == null) {
1048
- const out = allocPtr({ type: PTR.SET, len: 0, cap: INIT_CAP, stride: SET_ENTRY, tag: 'set' })
1145
+ const out = allocPtr({ type: PTR.SET, len: 0, cap: INIT_CAP, stride: SET_ENTRY + LANE, tag: 'set' })
1049
1146
  return typed(['block', ['result', 'f64'], out.init, out.ptr], 'f64')
1050
1147
  }
1051
1148
  // new Set(iterable): __iter_arr normalizes any iterable to an index-iterable
@@ -1067,7 +1164,7 @@ export default (ctx) => {
1067
1164
  ['i32.sub',
1068
1165
  ['i32.add', ['i32.shl', ['local.get', `$${lenL}`], ['i32.const', 1]], ['i32.const', INIT_CAP]],
1069
1166
  ['i32.const', 1]]]]]
1070
- const out = allocPtr({ type: PTR.SET, len: 0, cap: capExpr, stride: SET_ENTRY, tag: 'set' })
1167
+ const out = allocPtr({ type: PTR.SET, len: 0, cap: capExpr, stride: SET_ENTRY + LANE, tag: 'set' })
1071
1168
  return typed(['block', ['result', 'f64'],
1072
1169
  ['local.set', `$${arrL}`, asF64(emit(['()', '__iter_arr', iterExpr]))],
1073
1170
  ['local.set', `$${lenL}`, ['i32.const', 0]],
@@ -1148,7 +1245,7 @@ export default (ctx) => {
1148
1245
  (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
1149
1246
  (memory.fill (local.get $off) (i32.const 0)
1150
1247
  (i32.mul (local.get $cap)
1151
- (i32.add (i32.const ${SET_ENTRY})
1248
+ (i32.add (i32.const ${SET_ENTRY + LANE})
1152
1249
  (i32.shl (i32.eq (local.get $t) (i32.const ${PTR.MAP})) (i32.const 3)))))
1153
1250
  (i32.store (i32.sub (local.get $off) (i32.const 8)) (i32.const 0))))
1154
1251
  (f64.reinterpret_i64 (i64.const ${UNDEF_NAN})))`
@@ -1271,7 +1368,7 @@ export default (ctx) => {
1271
1368
  ctx.core.emit['new.Map'] = (iterExpr) => {
1272
1369
  ctx.features.map = true
1273
1370
  if (iterExpr == null) {
1274
- const out = allocPtr({ type: PTR.MAP, len: 0, cap: INIT_CAP, stride: MAP_ENTRY, tag: 'map' })
1371
+ const out = allocPtr({ type: PTR.MAP, len: 0, cap: INIT_CAP, stride: MAP_ENTRY + LANE, tag: 'map' })
1275
1372
  return typed(['block', ['result', 'f64'], out.init, out.ptr], 'f64')
1276
1373
  }
1277
1374
  // new Map(iterable): seed from another Map or an array of [key, value] pairs.
@@ -1415,7 +1512,7 @@ export default (ctx) => {
1415
1512
  }
1416
1513
  // Build the fresh-dst + threaded-walker-call sequence, returning the dst temp.
1417
1514
  const buildSet = (steps, tag) => {
1418
- const dst = allocPtr({ type: PTR.SET, len: 0, cap: INIT_CAP, stride: SET_ENTRY, tag })
1515
+ const dst = allocPtr({ type: PTR.SET, len: 0, cap: INIT_CAP, stride: SET_ENTRY + LANE, tag })
1419
1516
  const dstT = temp('sopd')
1420
1517
  const dI = () => ['i64.reinterpret_f64', ['local.get', `$${dstT}`]]
1421
1518
  const seq = ['block', ['result', 'f64'], dst.init, ['local.set', `$${dstT}`, dst.ptr]]
@@ -1486,7 +1583,7 @@ export default (ctx) => {
1486
1583
  const resI64 = ['i64.reinterpret_f64', ['local.get', `$${result}`]]
1487
1584
  const nb = allocPtr({ type: PTR.ARRAY, len: 0, cap: 0, tag: 'gbn' })
1488
1585
  const initResult = isMap
1489
- ? (() => { const out = allocPtr({ type: PTR.MAP, len: 0, cap: INIT_CAP, stride: MAP_ENTRY, tag: 'gbm' })
1586
+ ? (() => { const out = allocPtr({ type: PTR.MAP, len: 0, cap: INIT_CAP, stride: MAP_ENTRY + LANE, tag: 'gbm' })
1490
1587
  return ['block', ['result', 'f64'], out.init, out.ptr] })()
1491
1588
  : ['call', '$__hash_new']
1492
1589
  const keyOf = (cbResult) => isMap ? asI64(cbResult) : ['call', '$__to_str', asI64(cbResult)]
@@ -1546,7 +1643,7 @@ export default (ctx) => {
1546
1643
  ctx.core.stdlib['__sclone'] = `(func $__sclone (param $v f64) (result f64)
1547
1644
  (call $__sclone_rec (local.get $v)
1548
1645
  (i64.reinterpret_f64 (call $__mkptr (i32.const ${PTR.MAP}) (i32.const 0)
1549
- (call $__alloc_hdr_n (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY}))))))`
1646
+ (call $__alloc_hdr_n (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY + LANE}))))))`
1550
1647
 
1551
1648
  // Deep-clone the values of a freshly copied HASH table, in place.
1552
1649
  ctx.core.stdlib['__sclone_hash_vals'] = `(func $__sclone_hash_vals (param $off i32) (param $memo i64)
@@ -1639,7 +1736,7 @@ export default (ctx) => {
1639
1736
  (local.set $src (call $__ptr_offset (local.get $bits)))
1640
1737
  (local.set $cap (i32.load (i32.sub (local.get $src) (i32.const 4))))
1641
1738
  (local.set $out (call $__mkptr (local.get $t) (i32.const 0)
1642
- (call $__alloc_hdr_n (i32.const 0) (local.get $cap) (local.get $stride))))
1739
+ (call $__alloc_hdr_n (i32.const 0) (local.get $cap) (i32.add (local.get $stride) (i32.const ${LANE})))))
1643
1740
  (drop (call $__map_set (local.get $memo) (local.get $bits) (i64.reinterpret_f64 (local.get $out))))
1644
1741
  ;; walk the source in insertion order; ≤len inserts into cap slots never grow,
1645
1742
  ;; so $out's bits stay canonical (the memo entry above remains the pointer)
@@ -1739,7 +1836,7 @@ export default (ctx) => {
1739
1836
  (i32.sub (i32.const 32) (i32.clz
1740
1837
  (i32.sub (i32.add (i32.shl (local.get $n) (i32.const 1)) (i32.const ${INIT_CAP})) (i32.const 1))))))
1741
1838
  (local.set $map (i64.reinterpret_f64 (call $__mkptr (i32.const ${PTR.MAP}) (i32.const 0)
1742
- (call $__alloc_hdr_n (i32.const 0) (local.get $newcap) (i32.const ${MAP_ENTRY})))))
1839
+ (call $__alloc_hdr_n (i32.const 0) (local.get $newcap) (i32.const ${MAP_ENTRY + LANE})))))
1743
1840
  (if (i32.eq (local.get $t) (i32.const ${PTR.MAP}))
1744
1841
  (then
1745
1842
  ;; Copy in source insertion order so the new map enumerates identically.
@@ -1834,9 +1931,23 @@ export default (ctx) => {
1834
1931
  (if (local.get $cs) (then (i32.store (local.get $cs) (local.get $h))))
1835
1932
  (local.get $h))`
1836
1933
 
1934
+ // Cold shared rescan for the upsert templates' cap-tries fallback: first
1935
+ // TOMB-keyed (zombied) slot of the table, or slot 0 when none exists (a
1936
+ // truly-full-of-live-keys table — unreachable behind the 75%-load grow).
1937
+ ctx.core.stdlib['__zomb_scan'] = `(func $__zomb_scan (param $off i32) (param $cap i32) (param $es i32) (result i32)
1938
+ (local $slot i32) (local $end i32)
1939
+ (local.set $slot (local.get $off))
1940
+ (local.set $end (i32.add (local.get $off) (i32.mul (local.get $cap) (local.get $es))))
1941
+ (block $d (loop $l
1942
+ (br_if $d (i32.ge_u (local.get $slot) (local.get $end)))
1943
+ (br_if $d (i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (i64.const ${TOMB_NAN})))
1944
+ (local.set $slot (i32.add (local.get $slot) (local.get $es)))
1945
+ (br $l)))
1946
+ (select (local.get $off) (local.get $slot) (i32.ge_u (local.get $slot) (local.get $end))))`
1947
+
1837
1948
  ctx.core.stdlib['__hash_new'] = `(func $__hash_new (result f64)
1838
1949
  (call $__mkptr (i32.const ${PTR.HASH}) (i32.const 0)
1839
- (call $__alloc_hdr_n (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY}))))`
1950
+ (call $__alloc_hdr_n (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY + LANE}))))`
1840
1951
 
1841
1952
  // Small initial capacity for propsPtr-style hashes (per-object dyn props).
1842
1953
  // Most receivers in real code carry 0-2 dyn props; paying 8-slot up-front
@@ -1846,7 +1957,7 @@ export default (ctx) => {
1846
1957
  const smallCap = Math.max(ctx.transform.optimize?.hashSmallInitCap | 0, 2)
1847
1958
  ctx.core.stdlib['__hash_new_small'] = `(func $__hash_new_small (result f64)
1848
1959
  (call $__mkptr (i32.const ${PTR.HASH}) (i32.const 0)
1849
- (call $__alloc_hdr_n (i32.const 0) (i32.const ${smallCap}) (i32.const ${MAP_ENTRY}))))`
1960
+ (call $__alloc_hdr_n (i32.const 0) (i32.const ${smallCap}) (i32.const ${MAP_ENTRY + LANE}))))`
1850
1961
 
1851
1962
  ctx.core.stdlib['__hash_get_local'] = genLookupStrict('__hash_get_local', MAP_ENTRY, '$__str_hash', strEqG, PTR.HASH)
1852
1963
  ctx.core.stdlib['__hash_get_local_h'] = genLookupStrictPrehashed('__hash_get_local_h', MAP_ENTRY, strEqG, PTR.HASH)