jz 0.3.1 → 0.5.0

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.
@@ -11,6 +11,7 @@
11
11
  import { typed, asF64, asI64, asI32, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64, allocPtr, undefExpr } from '../src/ir.js'
12
12
  import { emit, emitFlat } from '../src/emit.js'
13
13
  import { valTypeOf, lookupValType, VAL } from '../src/analyze.js'
14
+ import { hasOwnContinue } from '../src/analyze.js'
14
15
  import { inc, PTR, LAYOUT } from '../src/ctx.js'
15
16
 
16
17
  const SET_ENTRY = 16 // hash + key
@@ -93,19 +94,20 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
93
94
  }
94
95
 
95
96
  /** Generate lookup probe function.
96
- * wantValue=true: return slot value, missing => NULL_NAN sentinel.
97
+ * wantValue=true: return slot value, missing => `undefined` (UNDEF_NAN) — a
98
+ * missing Map entry / object property reads as `undefined` in JS, never null.
97
99
  * wantValue=false: return i32 0/1 existence flag.
98
100
  * hasExt: emit EXTERNAL fallthrough (delegate to __ext_prop/__ext_has). */
99
101
  function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, hasExt) {
100
102
  const rt = wantValue ? 'i64' : 'i32'
101
103
  const onEmpty = wantValue
102
- ? `(return (i64.const ${NULL_NAN}))`
104
+ ? `(return (i64.const ${UNDEF_NAN}))`
103
105
  : '(return (i32.const 0))'
104
106
  const onFound = wantValue
105
107
  ? '(return (i64.load (i32.add (local.get $slot) (i32.const 16))))'
106
108
  : '(return (i32.const 1))'
107
109
  const notFound = wantValue
108
- ? `(i64.const ${NULL_NAN})`
110
+ ? `(i64.const ${UNDEF_NAN})`
109
111
  : '(i32.const 0)'
110
112
  const tExpr = `(i32.wrap_i64 (i64.and (i64.shr_u (local.get $coll) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))`
111
113
  const typeGuard = hasExt
@@ -186,7 +188,7 @@ function genUpsertGrow(name, entrySize, hashFn, eqExpr, typeConst, strict = fals
186
188
  (if (i32.ge_s (i32.mul (local.get $size) (i32.const 4)) (i32.mul (local.get $cap) (i32.const 3)))
187
189
  (then
188
190
  (local.set $newcap (i32.shl (local.get $cap) (i32.const 1)))
189
- (local.set $newptr (call $__alloc_hdr (i32.const 0) (local.get $newcap) (i32.const ${entrySize})))
191
+ (local.set $newptr (call $__alloc_hdr_n (i32.const 0) (local.get $newcap) (i32.const ${entrySize})))
190
192
  (local.set $i (i32.const 0))
191
193
  (block $rd (loop $rl
192
194
  (br_if $rd (i32.ge_s (local.get $i) (local.get $cap)))
@@ -335,14 +337,14 @@ export default (ctx) => {
335
337
  __hash_has: () => ctx.features.external
336
338
  ? ['__str_hash', '__str_eq', '__ptr_type', '__ext_has']
337
339
  : ['__str_hash', '__str_eq', '__ptr_type'],
338
- __hash_new: ['__alloc_hdr'],
339
- __hash_new_small: ['__alloc_hdr', '__mkptr'],
340
+ __hash_new: ['__alloc_hdr_n'],
341
+ __hash_new_small: ['__alloc_hdr_n', '__mkptr'],
340
342
  __hash_get_local: ['__str_hash', '__str_eq'],
341
343
  __hash_get_local_h: ['__str_eq'],
342
344
  __hash_set_local_h: ['__str_eq'],
343
- __hash_set_local: ['__str_hash', '__str_eq'],
345
+ __hash_set_local: ['__str_hash', '__str_eq', '__alloc_hdr_n', '__mkptr'],
344
346
  __ihash_get_local: ['__map_hash'],
345
- __ihash_set_local: ['__map_hash', '__alloc_hdr', '__mkptr'],
347
+ __ihash_set_local: ['__map_hash', '__alloc_hdr_n', '__mkptr'],
346
348
  __dyn_get_t: ['__dyn_get_t_h', '__str_hash'],
347
349
  __dyn_get_t_h: ['__ihash_get_local', '__str_eq', '__is_nullish'],
348
350
  __dyn_get: ['__dyn_get_t', '__ptr_type'],
@@ -353,9 +355,15 @@ export default (ctx) => {
353
355
  __dyn_get_any_t: () => ctx.features.external
354
356
  ? ['__dyn_get_t', '__hash_get_local', '__ext_prop']
355
357
  : ['__dyn_get_t', '__hash_get_local'],
358
+ __dyn_get_any_t_h: () => ctx.features.external
359
+ ? ['__dyn_get_t_h', '__hash_get_local_h', '__ext_prop']
360
+ : ['__dyn_get_t_h', '__hash_get_local_h'],
356
361
  __dyn_get_or: ['__dyn_get'],
357
362
  __dyn_set: ['__hash_new', '__hash_new_small', '__ihash_get_local', '__ihash_set_local', '__hash_set_local', '__ptr_offset', '__is_nullish', '__str_eq'],
358
363
  __dyn_move: ['__ihash_get_local', '__ihash_set_local', '__is_nullish'],
364
+ __hash_del_local: ['__str_hash', '__str_eq', '__ptr_type'],
365
+ __dyn_del: ['__hash_del_local', '__ihash_get_local', '__is_nullish'],
366
+ __coll_clear: ['__ptr_type', '__ptr_offset'],
359
367
  })
360
368
 
361
369
  inc('__ptr_offset', '__cap')
@@ -437,14 +445,57 @@ export default (ctx) => {
437
445
  // __map_new() → f64 — allocate empty Map (for JSON.parse, runtime creation)
438
446
  ctx.core.stdlib['__map_new'] = `(func $__map_new (result f64)
439
447
  (call $__mkptr (i32.const ${PTR.MAP}) (i32.const 0)
440
- (call $__alloc_hdr (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY}))))`
448
+ (call $__alloc_hdr_n (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY}))))`
441
449
 
442
450
  // === Set ===
443
451
 
444
- ctx.core.emit['new.Set'] = () => {
452
+ ctx.core.emit['new.Set'] = (iterExpr) => {
445
453
  ctx.features.set = true
446
- const out = allocPtr({ type: PTR.SET, len: 0, cap: INIT_CAP, stride: SET_ENTRY, tag: 'set' })
447
- return typed(['block', ['result', 'f64'], out.init, out.ptr], 'f64')
454
+ if (iterExpr == null) {
455
+ const out = allocPtr({ type: PTR.SET, len: 0, cap: INIT_CAP, stride: SET_ENTRY, tag: 'set' })
456
+ return typed(['block', ['result', 'f64'], out.init, out.ptr], 'f64')
457
+ }
458
+ // new Set(iterable): seed from an array argument's elements. __set_add does
459
+ // SameValueZero dedup + −0 normalization. A non-array argument leaves the set
460
+ // empty — the ptr_type guard zeroes the length so the loop is skipped.
461
+ //
462
+ // __set_add is a FIXED-capacity probe (genUpsert never grows the table); a
463
+ // full table makes its probe loop spin forever. So the table is pre-sized to
464
+ // the smallest power of two greater than 2*len: distinct entries ≤ len, so the
465
+ // table stays ≤50% full and every insert finds a free slot. cap = 1 <<
466
+ // (32 − clz(m−1)) with m = 2*len + INIT_CAP rounds 2*len up to a power of two
467
+ // and floors at INIT_CAP for the empty/short case.
468
+ inc('__set_add', '__ptr_type', '__len', '__typed_idx')
469
+ const setL = temp('nss'), arrL = temp('nsa')
470
+ const iL = tempI32('nsi'), lenL = tempI32('nsl')
471
+ const capExpr = ['i32.shl', ['i32.const', 1],
472
+ ['i32.sub', ['i32.const', 32], ['i32.clz',
473
+ ['i32.sub',
474
+ ['i32.add', ['i32.shl', ['local.get', `$${lenL}`], ['i32.const', 1]], ['i32.const', INIT_CAP]],
475
+ ['i32.const', 1]]]]]
476
+ const out = allocPtr({ type: PTR.SET, len: 0, cap: capExpr, stride: SET_ENTRY, tag: 'set' })
477
+ return typed(['block', ['result', 'f64'],
478
+ ['local.set', `$${arrL}`, asF64(emit(iterExpr))],
479
+ ['local.set', `$${lenL}`, ['i32.const', 0]],
480
+ ['if', ['i32.eq',
481
+ ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${arrL}`]]],
482
+ ['i32.const', PTR.ARRAY]],
483
+ ['then', ['local.set', `$${lenL}`,
484
+ ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${arrL}`]]]]]],
485
+ out.init,
486
+ ['local.set', `$${setL}`, out.ptr],
487
+ ['local.set', `$${iL}`, ['i32.const', 0]],
488
+ ['block', `$d_${iL}`, ['loop', `$l_${iL}`,
489
+ ['br_if', `$d_${iL}`, ['i32.ge_s', ['local.get', `$${iL}`], ['local.get', `$${lenL}`]]],
490
+ ['local.set', `$${setL}`, ['f64.reinterpret_i64',
491
+ ['call', '$__set_add',
492
+ ['i64.reinterpret_f64', ['local.get', `$${setL}`]],
493
+ ['i64.reinterpret_f64', ['call', '$__typed_idx',
494
+ ['i64.reinterpret_f64', ['local.get', `$${arrL}`]],
495
+ ['local.get', `$${iL}`]]]]]],
496
+ ['local.set', `$${iL}`, ['i32.add', ['local.get', `$${iL}`], ['i32.const', 1]]],
497
+ ['br', `$l_${iL}`]]],
498
+ ['local.get', `$${setL}`]], 'f64')
448
499
  }
449
500
 
450
501
  ctx.core.emit['.add'] = (setExpr, val) => {
@@ -462,10 +513,55 @@ export default (ctx) => {
462
513
  return typed(['f64.convert_i32_s', ['call', '$__set_delete', asI64(emit(setExpr)), asI64(emit(val))]], 'f64')
463
514
  }
464
515
 
516
+ // Map.prototype.clear / Set.prototype.clear — drop every entry. `.clear` only
517
+ // exists on Map/Set in JS, so a single generic emitter is unambiguous; the
518
+ // stdlib reads the ptr tag to pick the entry stride.
519
+ ctx.core.emit['.clear'] = (collExpr) => {
520
+ inc('__coll_clear')
521
+ return typed(['call', '$__coll_clear', asI64(emit(collExpr))], 'f64')
522
+ }
523
+
524
+ // Zero every slot (so probes see empty) and reset the length header. Entry
525
+ // stride is 16 for a SET, 24 for a MAP — `(t == MAP) << 3` adds the 8-byte
526
+ // value column. Returns undefined; a non-collation arg is a guarded no-op.
527
+ ctx.core.stdlib['__coll_clear'] = `(func $__coll_clear (param $coll i64) (result f64)
528
+ (local $off i32) (local $cap i32) (local $t i32)
529
+ (local.set $t (call $__ptr_type (local.get $coll)))
530
+ (if (i32.or (i32.eq (local.get $t) (i32.const ${PTR.SET})) (i32.eq (local.get $t) (i32.const ${PTR.MAP})))
531
+ (then
532
+ (local.set $off (call $__ptr_offset (local.get $coll)))
533
+ (local.set $cap (i32.load (i32.sub (local.get $off) (i32.const 4))))
534
+ (memory.fill (local.get $off) (i32.const 0)
535
+ (i32.mul (local.get $cap)
536
+ (i32.add (i32.const ${SET_ENTRY})
537
+ (i32.shl (i32.eq (local.get $t) (i32.const ${PTR.MAP})) (i32.const 3)))))
538
+ (i32.store (i32.sub (local.get $off) (i32.const 8)) (i32.const 0))))
539
+ (f64.reinterpret_i64 (i64.const ${UNDEF_NAN})))`
540
+
465
541
  ctx.core.emit['.size'] = (expr) => {
466
542
  return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', asF64(emit(expr))]]], 'f64')
467
543
  }
468
544
 
545
+ // x instanceof Map / Set — typed-pointer predicates emitted by jzify. NaN-check
546
+ // first (non-pointer numbers must report false), then compare __ptr_type tag.
547
+ // Mirrors module/array.js's Array.isArray inline form. Result is i32 (boolean).
548
+ ctx.core.emit['__is_map'] = (x) => {
549
+ inc('__ptr_type')
550
+ const v = asF64(emit(x))
551
+ const t = temp('imap')
552
+ return typed(['i32.and',
553
+ ['f64.ne', ['local.tee', `$${t}`, v], ['local.get', `$${t}`]],
554
+ ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', PTR.MAP]]], 'i32')
555
+ }
556
+ ctx.core.emit['__is_set'] = (x) => {
557
+ inc('__ptr_type')
558
+ const v = asF64(emit(x))
559
+ const t = temp('iset')
560
+ return typed(['i32.and',
561
+ ['f64.ne', ['local.tee', `$${t}`, v], ['local.get', `$${t}`]],
562
+ ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', PTR.SET]]], 'i32')
563
+ }
564
+
469
565
  // Generated Set probe functions
470
566
  ctx.core.stdlib['__set_add'] = () => genUpsert('__set_add', SET_ENTRY, '$__map_hash', sameValueZeroEq, PTR.SET, false, ctx.features.external)
471
567
  ctx.core.stdlib['__set_has'] = () => genLookup('__set_has', SET_ENTRY, '$__map_hash', sameValueZeroEq, PTR.SET, false, ctx.features.external)
@@ -512,7 +608,7 @@ export default (ctx) => {
512
608
  // Generated Map probe functions
513
609
  ctx.core.stdlib['__map_set'] = () => genUpsert('__map_set', MAP_ENTRY, '$__map_hash', sameValueZeroEq, PTR.MAP, true, ctx.features.external)
514
610
  ctx.core.stdlib['__map_get'] = () => genLookup('__map_get', MAP_ENTRY, '$__map_hash', sameValueZeroEq, PTR.MAP, true, ctx.features.external)
515
- ctx.core.stdlib['__map_get_h'] = () => genLookupStrictPrehashed('__map_get_h', MAP_ENTRY, sameValueZeroEq, PTR.MAP, NULL_NAN, ctx.features.external)
611
+ ctx.core.stdlib['__map_get_h'] = () => genLookupStrictPrehashed('__map_get_h', MAP_ENTRY, sameValueZeroEq, PTR.MAP, UNDEF_NAN, ctx.features.external)
516
612
  ctx.core.stdlib['__map_has'] = () => genLookup('__map_has', MAP_ENTRY, '$__map_hash', sameValueZeroEq, PTR.MAP, false, ctx.features.external)
517
613
  ctx.core.stdlib['__map_delete'] = genDelete('__map_delete', MAP_ENTRY, '$__map_hash', sameValueZeroEq, PTR.MAP)
518
614
 
@@ -567,19 +663,25 @@ export default (ctx) => {
567
663
 
568
664
  ctx.core.stdlib['__hash_new'] = `(func $__hash_new (result f64)
569
665
  (call $__mkptr (i32.const ${PTR.HASH}) (i32.const 0)
570
- (call $__alloc_hdr (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY}))))`
666
+ (call $__alloc_hdr_n (i32.const 0) (i32.const ${INIT_CAP}) (i32.const ${MAP_ENTRY}))))`
571
667
 
572
668
  // Small initial capacity for propsPtr-style hashes (per-object dyn props).
573
669
  // Most receivers in real code carry 0-2 dyn props; paying 8-slot up-front
574
670
  // is wasted memory + probe-loop cache pressure. Grows to 4/8/... on demand.
671
+ // L3/'speed' opts into a larger initial cap (default 8) to skip 2→4→8 growth
672
+ // when AST-style nodes carry 3-5 props (watr.compile's profile).
673
+ const smallCap = Math.max(ctx.transform.optimize?.hashSmallInitCap | 0, 2)
575
674
  ctx.core.stdlib['__hash_new_small'] = `(func $__hash_new_small (result f64)
576
675
  (call $__mkptr (i32.const ${PTR.HASH}) (i32.const 0)
577
- (call $__alloc_hdr (i32.const 0) (i32.const 2) (i32.const ${MAP_ENTRY}))))`
676
+ (call $__alloc_hdr_n (i32.const 0) (i32.const ${smallCap}) (i32.const ${MAP_ENTRY}))))`
578
677
 
579
678
  ctx.core.stdlib['__hash_get_local'] = genLookupStrict('__hash_get_local', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH)
580
679
  ctx.core.stdlib['__hash_get_local_h'] = genLookupStrictPrehashed('__hash_get_local_h', MAP_ENTRY, strEq, PTR.HASH)
581
680
  ctx.core.stdlib['__hash_set_local_h'] = genUpsertStrictPrehashed('__hash_set_local_h', MAP_ENTRY, strEq, PTR.HASH)
582
681
  ctx.core.stdlib['__hash_set_local'] = genUpsertGrow('__hash_set_local', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH, true)
682
+ // Tombstones an entry in a HASH (string keys). Returns 1 if found+deleted, 0 otherwise.
683
+ // Used as the bucket-level primitive for __dyn_del.
684
+ ctx.core.stdlib['__hash_del_local'] = genDelete('__hash_del_local', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH)
583
685
  // Outer __dyn_props hash: keyed by object offset (i32 as f64 bits), value is per-object props hash.
584
686
  // Uses bit-hash + i64.eq — no string allocation for the unique integer key.
585
687
  ctx.core.stdlib['__ihash_get_local'] = genLookupStrict('__ihash_get_local', MAP_ENTRY, '$__map_hash', '(i64.eq (i64.load (i32.add (local.get $slot) (i32.const 8))) (local.get $key))', PTR.HASH)
@@ -709,6 +811,11 @@ export default (ctx) => {
709
811
  (br_if $haveProps (i32.eq
710
812
  (i32.wrap_i64 (i64.and (i64.shr_u (local.get $props) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
711
813
  (i32.const ${PTR.HASH})))
814
+ ;; Fresh array (header propsPtr=0, no shift, no props): skip the
815
+ ;; global hash probe — there's nothing to find. Shifted arrays read
816
+ ;; forwarding bytes here (low32=newOff, high32=-1) → non-zero, so
817
+ ;; this br_if doesn't fire and they fall through to __dyn_props.
818
+ (br_if $dynDone (i64.eqz (local.get $props)))
712
819
  (local.set $props (i64.const 0))))
713
820
  ;; OBJECT: heap-allocated (off >= __heap_start) carries propsPtr at
714
821
  ;; off-16 from __alloc_hdr. The slot is either 0 (no dyn props yet) or
@@ -829,6 +936,28 @@ export default (ctx) => {
829
936
  (else ${extArm})))))`
830
937
  }
831
938
 
939
+ // Prehashed variant of __dyn_get_any_t for constant string keys: the FNV hash
940
+ // is folded at compile time (strHashLiteral), so no __str_hash call at runtime.
941
+ // Hot for the layered-parser pattern — `parse.step`/`parse.space`/… reads a
942
+ // function-object property with a literal key on every parser step.
943
+ ctx.core.stdlib['__dyn_get_any_t_h'] = () => {
944
+ const extArm = ctx.features.external
945
+ ? `(if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.EXTERNAL}))
946
+ (then (call $__ext_prop (local.get $obj) (local.get $key)))
947
+ (else (i64.const ${NULL_NAN})))`
948
+ : `(i64.const ${NULL_NAN})`
949
+ return `(func $__dyn_get_any_t_h (param $obj i64) (param $key i64) (param $t i32) (param $h i32) (result i64)
950
+ (local $val i64)
951
+ (if (result i64) (i32.eq (local.get $t) (i32.const ${PTR.HASH}))
952
+ (then (call $__hash_get_local_h (local.get $obj) (local.get $key) (local.get $h)))
953
+ (else
954
+ (local.set $val (call $__dyn_get_t_h (local.get $obj) (local.get $key) (local.get $t) (local.get $h)))
955
+ (if (result i64)
956
+ (i64.ne (local.get $val) (i64.const ${UNDEF_NAN}))
957
+ (then (local.get $val))
958
+ (else ${extArm})))))`
959
+ }
960
+
832
961
  // Hot for `node.loc = pos` patterns (e.g. watr's parser tags every nested level).
833
962
  // Defer the root insert to the end and gate it on props-ptr change: most calls hit
834
963
  // the no-grow case where the ptr is unchanged and the root slot already points to it.
@@ -925,6 +1054,91 @@ export default (ctx) => {
925
1054
  (then (global.set $__dyn_get_cache_props (f64.reinterpret_i64 (local.get $props)))))))
926
1055
  (local.get $val))`
927
1056
 
1057
+ // Tag-dispatched delete (mirrors __dyn_set's dispatch). Returns 1 if a slot was
1058
+ // found+tombstoned, 0 otherwise. Header types (ARRAY non-shifted, OBJECT heap-only,
1059
+ // TYPED/HASH/SET/MAP) carry propsPtr at off-16; others fall back to the global
1060
+ // __dyn_props hash keyed by offset.
1061
+ // Schema-aware delete arm: when the receiver is an OBJECT with a known schema and
1062
+ // the key matches a static slot, overwrite that slot with UNDEF_NAN so subsequent
1063
+ // reads see "absent" (matches `delete obj.a; obj.a → undefined`). Without this,
1064
+ // the shadow-store delete alone would leave the structural slot intact and a later
1065
+ // ctx[k] read would re-surface the original value.
1066
+ const buildObjectSchemaDelArm = () => (ctx.schema.list.length > 0 || ctx.core.includes.has('__jp_obj')) ? `
1067
+ (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
1068
+ (i32.ne (global.get $__schema_tbl) (i32.const 0)))
1069
+ (then
1070
+ (local.set $sid (i32.wrap_i64 (i64.and (i64.shr_u
1071
+ (local.get $obj) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK}))))
1072
+ (local.set $kbits
1073
+ (i64.load (i32.add (global.get $__schema_tbl) (i32.shl (local.get $sid) (i32.const 3)))))
1074
+ (local.set $koff (i32.wrap_i64 (i64.and (local.get $kbits) (i64.const ${LAYOUT.OFFSET_MASK}))))
1075
+ (local.set $nkeys (i32.load (i32.sub (local.get $koff) (i32.const 8))))
1076
+ (local.set $idx (i32.const 0))
1077
+ (block $schemaDelDone (loop $schemaDelLoop
1078
+ (br_if $schemaDelDone (i32.ge_s (local.get $idx) (local.get $nkeys)))
1079
+ (if (call $__str_eq
1080
+ (i64.load (i32.add (local.get $koff) (i32.shl (local.get $idx) (i32.const 3))))
1081
+ (local.get $key))
1082
+ (then
1083
+ (i64.store (i32.add (local.get $off) (i32.shl (local.get $idx) (i32.const 3))) (i64.const ${UNDEF_NAN}))
1084
+ (local.set $hit (i32.const 1))
1085
+ (br $schemaDelDone)))
1086
+ (local.set $idx (i32.add (local.get $idx) (i32.const 1)))
1087
+ (br $schemaDelLoop)))))` : ''
1088
+
1089
+ ctx.core.stdlib['__dyn_del'] = () => `(func $__dyn_del (param $obj i64) (param $key i64) (result i32)
1090
+ (local $root i64) (local $props i64) (local $oldProps i64)
1091
+ (local $off i32) (local $type i32) (local $hit i32) ${buildObjectSchemaSetLocals()}
1092
+ (local.set $off (i32.wrap_i64 (i64.and (local.get $obj) (i64.const ${LAYOUT.OFFSET_MASK}))))
1093
+ (local.set $type (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))
1094
+ ${buildObjectSchemaDelArm()}
1095
+ ;; CLOSURE with no env: rekey to function table index (parallels __dyn_set / __dyn_get_t_h).
1096
+ (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.CLOSURE})) (i32.eqz (local.get $off)))
1097
+ (then (local.set $off (i32.sub (i32.const -1)
1098
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $obj) (i64.const ${LAYOUT.AUX_SHIFT})) (i64.const ${LAYOUT.AUX_MASK})))))))
1099
+ ;; ARRAY: follow forwarding chain to landed base.
1100
+ (if (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
1101
+ (then
1102
+ (block $done
1103
+ (loop $follow
1104
+ (br_if $done (i32.lt_u (local.get $off) (i32.const 16)))
1105
+ (br_if $done (i32.gt_u (local.get $off) (i32.shl (memory.size) (i32.const 16))))
1106
+ (br_if $done (i32.ne (i32.load (i32.sub (local.get $off) (i32.const 4))) (i32.const -1)))
1107
+ (local.set $off (i32.load (i32.sub (local.get $off) (i32.const 8))))
1108
+ (br $follow)))))
1109
+ ;; ARRAY landed propsPtr (HASH-tagged means real sidecar; else fall through to global).
1110
+ (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.ARRAY}))
1111
+ (i32.ge_u (local.get $off) (i32.const 16)))
1112
+ (then
1113
+ (local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
1114
+ (if (i32.eq
1115
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $oldProps) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))
1116
+ (i32.const ${PTR.HASH}))
1117
+ (then (return (i32.or (local.get $hit) (call $__hash_del_local (local.get $oldProps) (local.get $key))))))))
1118
+ ;; OBJECT heap: propsPtr directly at off-16.
1119
+ (if (i32.and (i32.eq (local.get $type) (i32.const ${PTR.OBJECT}))
1120
+ (i32.ge_u (local.get $off) (global.get $__heap_start)))
1121
+ (then
1122
+ (local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
1123
+ (if (i64.eqz (local.get $oldProps)) (then (return (local.get $hit))))
1124
+ (return (i32.or (local.get $hit) (call $__hash_del_local (local.get $oldProps) (local.get $key))))))
1125
+ ;; Other header types (TYPED/HASH/SET/MAP).
1126
+ (if (i32.and (i32.ge_u (local.get $off) (i32.const 16))
1127
+ (i32.or (i32.eq (local.get $type) (i32.const ${PTR.TYPED}))
1128
+ (i32.or (i32.eq (local.get $type) (i32.const ${PTR.HASH}))
1129
+ (i32.or (i32.eq (local.get $type) (i32.const ${PTR.SET}))
1130
+ (i32.eq (local.get $type) (i32.const ${PTR.MAP}))))))
1131
+ (then
1132
+ (local.set $oldProps (i64.load (i32.sub (local.get $off) (i32.const 16))))
1133
+ (if (i64.eqz (local.get $oldProps)) (then (return (local.get $hit))))
1134
+ (return (i32.or (local.get $hit) (call $__hash_del_local (local.get $oldProps) (local.get $key))))))
1135
+ ;; Fallback: global __dyn_props keyed by offset.
1136
+ (local.set $root (i64.reinterpret_f64 (global.get $__dyn_props)))
1137
+ (if (i64.eqz (local.get $root)) (then (return (local.get $hit))))
1138
+ (local.set $props (call $__ihash_get_local (local.get $root) (i64.reinterpret_f64 (f64.convert_i32_s (local.get $off)))))
1139
+ (if (call $__is_nullish (local.get $props)) (then (return (local.get $hit))))
1140
+ (i32.or (local.get $hit) (call $__hash_del_local (local.get $props) (local.get $key))))`
1141
+
928
1142
  ctx.core.stdlib['__dyn_move'] = `(func $__dyn_move (param $oldOff i32) (param $newOff i32)
929
1143
  (local $props i64) (local $root i64)
930
1144
  (if (f64.eq (global.get $__dyn_props) (f64.const 0)) (then (return)))
@@ -938,6 +1152,15 @@ export default (ctx) => {
938
1152
  ctx.core.stdlib['__hash_get'] = () => genLookup('__hash_get', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH, true, ctx.features.external)
939
1153
  ctx.core.stdlib['__hash_has'] = () => genLookup('__hash_has', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH, false, ctx.features.external)
940
1154
 
1155
+ // === `delete obj[k]`: lift from prepare for computed-key removal ===
1156
+ // Static-key `delete obj.x` / `delete obj["x"]` is rejected in prepare (fixed schema);
1157
+ // only the runtime-dispatched form reaches here. JS returns `true` on success — we
1158
+ // surface the actual found/not-found bit as i32 (`true`/`false` ↔ 1/0 in jz NaN-box).
1159
+ ctx.core.emit['delete'] = (obj, key) => {
1160
+ inc('__dyn_del')
1161
+ return typed(['call', '$__dyn_del', asI64(emit(obj)), asI64(emit(key))], 'i32')
1162
+ }
1163
+
941
1164
  // === `in` operator: key in obj → HASH key existence check ===
942
1165
  ctx.core.emit['in'] = (key, obj) => {
943
1166
  const objType = typeof obj === 'string' ? lookupValType(obj) : valTypeOf(obj)
@@ -1032,8 +1255,14 @@ export default (ctx) => {
1032
1255
  const ptrI64 = tempI64('hp'), srcOff = tempI32('hso'), srcType = tempI32('hst')
1033
1256
  if (!ctx.func.locals.has(varName)) ctx.func.locals.set(varName, 'f64')
1034
1257
  const id = ctx.func.uniq++
1258
+ const brk = `$brk${id}`, loop = `$loop${id}`, cont = `$cont${id}`
1035
1259
  const va = asF64(emit(src))
1036
- const bodyFlat = emitFlat(body)
1260
+ const needsCont = hasOwnContinue(body)
1261
+ ctx.func.stack.push({ brk, loop: needsCont ? cont : loop })
1262
+ let bodyFlat
1263
+ try { bodyFlat = emitFlat(body) }
1264
+ finally { ctx.func.stack.pop() }
1265
+ const bodyBlock = needsCont ? [['block', cont, ...bodyFlat]] : bodyFlat
1037
1266
  inc('__ptr_type')
1038
1267
  return [
1039
1268
  // Save source ptr as i64
@@ -1054,16 +1283,16 @@ export default (ctx) => {
1054
1283
  ['local.set', `$${off}`, ['call', '$__ptr_offset', ['local.get', `$${ptrI64}`]]],
1055
1284
  ['local.set', `$${cap}`, ['call', '$__cap', ['local.get', `$${ptrI64}`]]],
1056
1285
  ['local.set', `$${i}`, ['i32.const', 0]],
1057
- ['block', `$brk${id}`, ['loop', `$loop${id}`,
1058
- ['br_if', `$brk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${cap}`]]],
1286
+ ['block', brk, ['loop', loop,
1287
+ ['br_if', brk, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${cap}`]]],
1059
1288
  ['local.set', `$${slot}`, ['i32.add', ['local.get', `$${off}`],
1060
1289
  ['i32.mul', ['local.get', `$${i}`], ['i32.const', MAP_ENTRY]]]],
1061
1290
  ['if', ['i64.ne', ['i64.load', ['local.get', `$${slot}`]], ['i64.const', 0]],
1062
1291
  ['then',
1063
1292
  ['local.set', `$${varName}`, ['f64.reinterpret_i64', ['i64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 8]]]]],
1064
- ...bodyFlat]],
1293
+ ...bodyBlock]],
1065
1294
  ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
1066
- ['br', `$loop${id}`]]]]]
1295
+ ['br', loop]]]]]
1067
1296
  ]
1068
1297
  }
1069
1298
  }
package/module/console.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Console + clocks module — two host-mode lowerings.
3
3
  *
4
4
  * `host: 'js'` (default): emit `env.print(val: i64, fd: i32, sep: i32)` and
5
- * `env.now(clock: i32) -> f64`. The JS host (src/host.js) wires both
5
+ * `env.now(clock: i32) -> f64`. The JS host (interop.js) wires both
6
6
  * automatically — `print` reads the NaN-boxed value via `mem.read`, so
7
7
  * stringification happens host-side (no __ftoa / __write_str / __write_val
8
8
  * stdlib in the binary). `sep`: 10=newline, 32=space, 0=no separator.