jz 0.4.0 → 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,7 +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/ast.js'
14
+ import { hasOwnContinue } from '../src/analyze.js'
15
15
  import { inc, PTR, LAYOUT } from '../src/ctx.js'
16
16
 
17
17
  const SET_ENTRY = 16 // hash + key
@@ -94,19 +94,20 @@ function genUpsert(name, entrySize, hashFn, eqExpr, expectedType, hasVal, hasExt
94
94
  }
95
95
 
96
96
  /** Generate lookup probe function.
97
- * 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.
98
99
  * wantValue=false: return i32 0/1 existence flag.
99
100
  * hasExt: emit EXTERNAL fallthrough (delegate to __ext_prop/__ext_has). */
100
101
  function genLookup(name, entrySize, hashFn, eqExpr, expectedType, wantValue, hasExt) {
101
102
  const rt = wantValue ? 'i64' : 'i32'
102
103
  const onEmpty = wantValue
103
- ? `(return (i64.const ${NULL_NAN}))`
104
+ ? `(return (i64.const ${UNDEF_NAN}))`
104
105
  : '(return (i32.const 0))'
105
106
  const onFound = wantValue
106
107
  ? '(return (i64.load (i32.add (local.get $slot) (i32.const 16))))'
107
108
  : '(return (i32.const 1))'
108
109
  const notFound = wantValue
109
- ? `(i64.const ${NULL_NAN})`
110
+ ? `(i64.const ${UNDEF_NAN})`
110
111
  : '(i32.const 0)'
111
112
  const tExpr = `(i32.wrap_i64 (i64.and (i64.shr_u (local.get $coll) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK})))`
112
113
  const typeGuard = hasExt
@@ -354,9 +355,15 @@ export default (ctx) => {
354
355
  __dyn_get_any_t: () => ctx.features.external
355
356
  ? ['__dyn_get_t', '__hash_get_local', '__ext_prop']
356
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'],
357
361
  __dyn_get_or: ['__dyn_get'],
358
362
  __dyn_set: ['__hash_new', '__hash_new_small', '__ihash_get_local', '__ihash_set_local', '__hash_set_local', '__ptr_offset', '__is_nullish', '__str_eq'],
359
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'],
360
367
  })
361
368
 
362
369
  inc('__ptr_offset', '__cap')
@@ -442,10 +449,53 @@ export default (ctx) => {
442
449
 
443
450
  // === Set ===
444
451
 
445
- ctx.core.emit['new.Set'] = () => {
452
+ ctx.core.emit['new.Set'] = (iterExpr) => {
446
453
  ctx.features.set = true
447
- const out = allocPtr({ type: PTR.SET, len: 0, cap: INIT_CAP, stride: SET_ENTRY, tag: 'set' })
448
- 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')
449
499
  }
450
500
 
451
501
  ctx.core.emit['.add'] = (setExpr, val) => {
@@ -463,10 +513,55 @@ export default (ctx) => {
463
513
  return typed(['f64.convert_i32_s', ['call', '$__set_delete', asI64(emit(setExpr)), asI64(emit(val))]], 'f64')
464
514
  }
465
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
+
466
541
  ctx.core.emit['.size'] = (expr) => {
467
542
  return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', asF64(emit(expr))]]], 'f64')
468
543
  }
469
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
+
470
565
  // Generated Set probe functions
471
566
  ctx.core.stdlib['__set_add'] = () => genUpsert('__set_add', SET_ENTRY, '$__map_hash', sameValueZeroEq, PTR.SET, false, ctx.features.external)
472
567
  ctx.core.stdlib['__set_has'] = () => genLookup('__set_has', SET_ENTRY, '$__map_hash', sameValueZeroEq, PTR.SET, false, ctx.features.external)
@@ -513,7 +608,7 @@ export default (ctx) => {
513
608
  // Generated Map probe functions
514
609
  ctx.core.stdlib['__map_set'] = () => genUpsert('__map_set', MAP_ENTRY, '$__map_hash', sameValueZeroEq, PTR.MAP, true, ctx.features.external)
515
610
  ctx.core.stdlib['__map_get'] = () => genLookup('__map_get', MAP_ENTRY, '$__map_hash', sameValueZeroEq, PTR.MAP, true, ctx.features.external)
516
- 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)
517
612
  ctx.core.stdlib['__map_has'] = () => genLookup('__map_has', MAP_ENTRY, '$__map_hash', sameValueZeroEq, PTR.MAP, false, ctx.features.external)
518
613
  ctx.core.stdlib['__map_delete'] = genDelete('__map_delete', MAP_ENTRY, '$__map_hash', sameValueZeroEq, PTR.MAP)
519
614
 
@@ -584,6 +679,9 @@ export default (ctx) => {
584
679
  ctx.core.stdlib['__hash_get_local_h'] = genLookupStrictPrehashed('__hash_get_local_h', MAP_ENTRY, strEq, PTR.HASH)
585
680
  ctx.core.stdlib['__hash_set_local_h'] = genUpsertStrictPrehashed('__hash_set_local_h', MAP_ENTRY, strEq, PTR.HASH)
586
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)
587
685
  // Outer __dyn_props hash: keyed by object offset (i32 as f64 bits), value is per-object props hash.
588
686
  // Uses bit-hash + i64.eq — no string allocation for the unique integer key.
589
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)
@@ -838,6 +936,28 @@ export default (ctx) => {
838
936
  (else ${extArm})))))`
839
937
  }
840
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
+
841
961
  // Hot for `node.loc = pos` patterns (e.g. watr's parser tags every nested level).
842
962
  // Defer the root insert to the end and gate it on props-ptr change: most calls hit
843
963
  // the no-grow case where the ptr is unchanged and the root slot already points to it.
@@ -934,6 +1054,91 @@ export default (ctx) => {
934
1054
  (then (global.set $__dyn_get_cache_props (f64.reinterpret_i64 (local.get $props)))))))
935
1055
  (local.get $val))`
936
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
+
937
1142
  ctx.core.stdlib['__dyn_move'] = `(func $__dyn_move (param $oldOff i32) (param $newOff i32)
938
1143
  (local $props i64) (local $root i64)
939
1144
  (if (f64.eq (global.get $__dyn_props) (f64.const 0)) (then (return)))
@@ -947,6 +1152,15 @@ export default (ctx) => {
947
1152
  ctx.core.stdlib['__hash_get'] = () => genLookup('__hash_get', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH, true, ctx.features.external)
948
1153
  ctx.core.stdlib['__hash_has'] = () => genLookup('__hash_has', MAP_ENTRY, '$__str_hash', strEq, PTR.HASH, false, ctx.features.external)
949
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
+
950
1164
  // === `in` operator: key in obj → HASH key existence check ===
951
1165
  ctx.core.emit['in'] = (key, obj) => {
952
1166
  const objType = typeof obj === 'string' ? lookupValType(obj) : valTypeOf(obj)
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.