jz 0.2.1 → 0.3.1

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.
package/module/number.js CHANGED
@@ -10,7 +10,8 @@
10
10
  */
11
11
 
12
12
  import { typed, asF64, asI32, asI64, toNumF64, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64 } from '../src/ir.js'
13
- import { emit, isReassigned } from '../src/emit.js'
13
+ import { emit } from '../src/emit.js'
14
+ import { isReassigned } from '../src/ast.js'
14
15
  import { valTypeOf, VAL } from '../src/analyze.js'
15
16
  import { inc, PTR } from '../src/ctx.js'
16
17
 
@@ -22,6 +23,7 @@ export default (ctx) => {
22
23
  __to_num: ['__char_at', '__str_byteLen', '__pow10', '__to_str'],
23
24
  __to_bigint: ['__char_at', '__str_byteLen'],
24
25
  __parseInt: ['__char_at', '__str_byteLen'],
26
+ __parseFloat: ['__char_at', '__str_byteLen', '__pow10', '__to_str'],
25
27
  })
26
28
 
27
29
 
@@ -346,7 +348,6 @@ export default (ctx) => {
346
348
  ['f64.eq', ['local.get', `$${t}`], ['f64.trunc', ['local.get', `$${t}`]]]], 'i32')
347
349
  }
348
350
 
349
- // parseInt(str, radix) — parse string to integer
350
351
  ctx.core.stdlib['__parseInt'] = `(func $__parseInt (param $str i64) (param $radix i32) (result f64)
351
352
  (local $off i32) (local $len i32) (local $i i32) (local $c i32) (local $neg i32)
352
353
  (local $result f64) (local $digit i32) (local $seen i32) (local $f f64)
@@ -592,14 +593,135 @@ export default (ctx) => {
592
593
  (then (i64.sub (i64.const 0) (local.get $result)))
593
594
  (else (local.get $result)))))`
594
595
 
596
+ ctx.core.stdlib['__parseFloat'] = `(func $__parseFloat (param $v i64) (result f64)
597
+ (local $t i32) (local $len i32) (local $i i32) (local $c i32) (local $neg i32)
598
+ (local $seen i32) (local $exp i32) (local $expNeg i32) (local $expDigits i32)
599
+ (local $dot i32) (local $sigDigits i32) (local $decExp i32) (local $dropped i32) (local $round i32)
600
+ (local $result f64) (local $f f64)
601
+ (local.set $f (f64.reinterpret_i64 (local.get $v)))
602
+ (if (f64.eq (local.get $f) (local.get $f)) (then (return (local.get $f))))
603
+ (local.set $t (call $__ptr_type (local.get $v)))
604
+ ;; parseFloat first applies ToString, then parses the longest decimal prefix.
605
+ ;; Unlike Number(), empty strings and non-decimal prefixes produce NaN/0
606
+ ;; according to the consumed prefix rather than whole-string numeric coercion.
607
+ (if (i32.ne (local.get $t) (i32.const ${PTR.STRING}))
608
+ (then
609
+ (local.set $v (call $__to_str (local.get $v)))
610
+ (local.set $t (call $__ptr_type (local.get $v)))
611
+ (if (i32.ne (local.get $t) (i32.const ${PTR.STRING}))
612
+ (then (return (f64.const nan))))))
613
+ (local.set $len (call $__str_byteLen (local.get $v)))
614
+ ;; Skip leading whitespace.
615
+ (block $ws (loop $wsl
616
+ (br_if $ws (i32.ge_s (local.get $i) (local.get $len)))
617
+ (br_if $ws (i32.gt_s (call $__char_at (local.get $v) (local.get $i)) (i32.const 32)))
618
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
619
+ (br $wsl)))
620
+ ;; Sign.
621
+ (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
622
+ (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 45)))
623
+ (then (local.set $neg (i32.const 1)) (local.set $i (i32.add (local.get $i) (i32.const 1)))))
624
+ (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
625
+ (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 43)))
626
+ (then (local.set $i (i32.add (local.get $i) (i32.const 1)))))
627
+ ;; Decimal significand. Keep 17 significant decimal digits, track the
628
+ ;; base-10 exponent for skipped digits, and round once before pow10 scaling.
629
+ (block $numDone (loop $numLoop
630
+ (br_if $numDone (i32.ge_s (local.get $i) (local.get $len)))
631
+ (local.set $c (call $__char_at (local.get $v) (local.get $i)))
632
+ (if (i32.and (i32.eq (local.get $c) (i32.const 46)) (i32.eqz (local.get $dot)))
633
+ (then
634
+ (local.set $dot (i32.const 1))
635
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
636
+ (br $numLoop)))
637
+ (br_if $numDone
638
+ (i32.or
639
+ (i32.lt_s (local.get $c) (i32.const 48))
640
+ (i32.gt_s (local.get $c) (i32.const 57))))
641
+ (local.set $seen (i32.const 1))
642
+ (local.set $c (i32.sub (local.get $c) (i32.const 48)))
643
+ (if (i32.and (i32.eqz (local.get $sigDigits)) (i32.eqz (local.get $c)))
644
+ (then
645
+ (if (local.get $dot) (then (local.set $decExp (i32.sub (local.get $decExp) (i32.const 1)))))
646
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
647
+ (br $numLoop)))
648
+ (if (i32.lt_s (local.get $sigDigits)
649
+ (if (result i32) (local.get $dot) (then (i32.const 16)) (else (i32.const 17))))
650
+ (then
651
+ (local.set $result
652
+ (f64.add
653
+ (f64.mul (local.get $result) (f64.const 10))
654
+ (f64.convert_i32_s (local.get $c))))
655
+ (local.set $sigDigits (i32.add (local.get $sigDigits) (i32.const 1)))
656
+ (if (local.get $dot) (then (local.set $decExp (i32.sub (local.get $decExp) (i32.const 1))))))
657
+ (else
658
+ (if (i32.eqz (local.get $dropped))
659
+ (then (if (i32.ge_s (local.get $c) (i32.const 5)) (then (local.set $round (i32.const 1))))))
660
+ (local.set $dropped (i32.const 1))
661
+ (if (i32.eqz (local.get $dot)) (then (local.set $decExp (i32.add (local.get $decExp) (i32.const 1)))))))
662
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
663
+ (br $numLoop)))
664
+ (if (i32.eqz (local.get $seen)) (then (return (f64.const nan))))
665
+ (if (local.get $round) (then (local.set $result (f64.add (local.get $result) (f64.const 1)))))
666
+ ;; Scientific notation.
667
+ (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
668
+ (i32.or
669
+ (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 101))
670
+ (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 69))))
671
+ (then
672
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
673
+ (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
674
+ (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 45)))
675
+ (then (local.set $expNeg (i32.const 1)) (local.set $i (i32.add (local.get $i) (i32.const 1)))))
676
+ (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
677
+ (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 43)))
678
+ (then (local.set $i (i32.add (local.get $i) (i32.const 1)))))
679
+ (block $expDone (loop $expLoop
680
+ (br_if $expDone (i32.ge_s (local.get $i) (local.get $len)))
681
+ (local.set $c (call $__char_at (local.get $v) (local.get $i)))
682
+ (br_if $expDone
683
+ (i32.or
684
+ (i32.lt_s (local.get $c) (i32.const 48))
685
+ (i32.gt_s (local.get $c) (i32.const 57))))
686
+ (local.set $exp
687
+ (i32.add
688
+ (i32.mul (local.get $exp) (i32.const 10))
689
+ (i32.sub (local.get $c) (i32.const 48))))
690
+ (local.set $expDigits (i32.add (local.get $expDigits) (i32.const 1)))
691
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
692
+ (br $expLoop)))
693
+ (if (local.get $expDigits)
694
+ (then
695
+ (if (local.get $expNeg)
696
+ (then (local.set $decExp (i32.sub (local.get $decExp) (local.get $exp))))
697
+ (else (local.set $decExp (i32.add (local.get $decExp) (local.get $exp)))))))))
698
+ (if (i32.gt_s (local.get $decExp) (i32.const 0))
699
+ (then (local.set $result (f64.mul (local.get $result) (call $__pow10 (local.get $decExp))))))
700
+ (if (i32.lt_s (local.get $decExp) (i32.const 0))
701
+ (then (local.set $result (f64.div (local.get $result) (call $__pow10 (i32.sub (i32.const 0) (local.get $decExp)))))))
702
+ (if (result f64) (local.get $neg) (then (f64.neg (local.get $result))) (else (local.get $result))))`
703
+
595
704
  ctx.core.emit['Number.parseInt'] = (x, radix) => {
596
- inc('__parseInt')
705
+ needParseInt()
597
706
  return typed(['call', '$__parseInt', asI64(emit(x)), radix ? asI32(emit(radix)) : ['i32.const', 0]], 'f64')
598
707
  }
599
708
  ctx.core.emit['parseInt'] = ctx.core.emit['Number.parseInt']
709
+ const addImportOnce = (ctx, mod, name, fn) => {
710
+ if (ctx.module.imports.some(i => i[1] === `"${mod}"` && i[2] === `"${name}"`)) return
711
+ ctx.module.imports.push(['import', `"${mod}"`, `"${name}"`, fn])
712
+ }
713
+ const needParseFloat = () => addImportOnce(ctx, 'env', 'parseFloat',
714
+ ['func', '$__parseFloat', ['param', 'i64'], ['result', 'f64']])
715
+ const needParseInt = () => addImportOnce(ctx, 'env', 'parseInt',
716
+ ['func', '$__parseInt', ['param', 'i64'], ['param', 'i32'], ['result', 'f64']])
717
+
600
718
  ctx.core.emit['Number.parseFloat'] = (x) => {
601
- inc('__to_num')
602
- return typed(['call', '$__to_num', asI64(emit(x))], 'f64')
719
+ if (ctx.transform.host === 'wasi') {
720
+ inc('__parseFloat')
721
+ return typed(['call', '$__parseFloat', asI64(emit(x))], 'f64')
722
+ }
723
+ needParseFloat()
724
+ return typed(['call', '$__parseFloat', asI64(emit(x))], 'f64')
603
725
  }
604
726
  ctx.core.emit['parseFloat'] = ctx.core.emit['Number.parseFloat']
605
727
 
package/module/object.js CHANGED
@@ -134,8 +134,9 @@ export default (ctx) => {
134
134
  ctx.core.emit[`.${VAL.CLOSURE}:hasOwnProperty`] = ctx.core.emit['.hasOwnProperty']
135
135
 
136
136
  ctx.core.emit['Object.values'] = (obj) => {
137
+ if (isHashTyped(obj)) return emitHashValues(obj)
137
138
  const schema = resolveSchema(obj)
138
- if (!schema) err('Object.values requires object with known schema')
139
+ if (!schema) return emitRuntimeValues(obj)
139
140
  const va = asF64(emit(obj))
140
141
  const n = schema.length
141
142
  const t = temp('ov'), base = tempI32('vb')
@@ -225,6 +226,10 @@ export default (ctx) => {
225
226
  return typed(['block', ['result', 'f64'], ...body], 'f64')
226
227
  }
227
228
 
229
+ ctx.core.emit['Object.defineProperty'] = () => {
230
+ err('Object.defineProperty descriptor semantics are outside jz scope; jzify only folds static bundler export helpers')
231
+ }
232
+
228
233
  // Object.fromEntries(arr) → creates HASH from array of [key, value] pairs
229
234
  ctx.core.emit['Object.fromEntries'] = (arr) => {
230
235
  inc('__hash_new', '__hash_set')
@@ -450,6 +455,13 @@ function emitHashKeys(obj) {
450
455
  hashKeysFromTemp(t)], 'f64')
451
456
  }
452
457
 
458
+ function emitHashValues(obj) {
459
+ const t = temp('hv')
460
+ return typed(['block', ['result', 'f64'],
461
+ ['local.set', `$${t}`, asF64(emit(obj))],
462
+ hashValuesFromTemp(t)], 'f64')
463
+ }
464
+
453
465
  // Inline body of the HASH walk against an already-bound f64 local. Shared by
454
466
  // the static-HASH path and the runtime-dispatch path so both produce the same
455
467
  // IR shape from the same source — only difference is whether they enter from
@@ -481,6 +493,33 @@ function hashKeysFromTemp(t) {
481
493
  out.ptr]
482
494
  }
483
495
 
496
+ function hashValuesFromTemp(t) {
497
+ inc('__ptr_offset', '__cap', '__len')
498
+ const off = tempI32('hvo'), cap = tempI32('hvc'), n = tempI32('hvn')
499
+ const i = tempI32('hvi'), o = tempI32('hvj'), slot = tempI32('hvs')
500
+ const out = allocPtr({ type: PTR.ARRAY, len: ['local.get', `$${n}`], tag: 'hva' })
501
+ const id = ctx.func.uniq++
502
+ return ['block', ['result', 'f64'],
503
+ ['local.set', `$${n}`, ['call', '$__len', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
504
+ out.init,
505
+ ['local.set', `$${off}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
506
+ ['local.set', `$${cap}`, ['call', '$__cap', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
507
+ ['local.set', `$${i}`, ['i32.const', 0]],
508
+ ['local.set', `$${o}`, ['i32.const', 0]],
509
+ ['block', `$vbrk${id}`, ['loop', `$vloop${id}`,
510
+ ['br_if', `$vbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${cap}`]]],
511
+ ['local.set', `$${slot}`, ['i32.add', ['local.get', `$${off}`],
512
+ ['i32.mul', ['local.get', `$${i}`], ['i32.const', 24]]]],
513
+ ['if', ['f64.ne', ['f64.load', ['local.get', `$${slot}`]], ['f64.const', 0]],
514
+ ['then',
515
+ elemStore(out.local, o,
516
+ ['f64.load', ['i32.add', ['local.get', `$${slot}`], ['i32.const', 16]]]),
517
+ ['local.set', `$${o}`, ['i32.add', ['local.get', `$${o}`], ['i32.const', 1]]]]],
518
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
519
+ ['br', `$vloop${id}`]]],
520
+ out.ptr]
521
+ }
522
+
484
523
  // Type-unknown receiver: bind the value, branch on ptr-type. HASH walks the
485
524
  // probe table; OBJECT loads the schema's key array (registered statically at
486
525
  // compile time or lazily at runtime by JSON.parse via __jp_schema_get); other
@@ -509,6 +548,26 @@ function emitRuntimeKeys(obj) {
509
548
  ['else', ['block', ['result', 'f64'], empty.init, empty.ptr]]]]]], 'f64')
510
549
  }
511
550
 
551
+ function emitRuntimeValues(obj) {
552
+ inc('__ptr_type')
553
+ if (!ctx.scope.globals.has('__schema_tbl'))
554
+ ctx.scope.globals.set('__schema_tbl', '(global $__schema_tbl (mut i32) (i32.const 0))')
555
+ const t = temp('rv'), tt = tempI32('rvt')
556
+ const empty = allocPtr({ type: PTR.ARRAY, len: 0, tag: 'rve' })
557
+ return typed(['block', ['result', 'f64'],
558
+ ['local.set', `$${t}`, asF64(emit(obj))],
559
+ ['local.set', `$${tt}`, ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
560
+ ['if', ['result', 'f64'],
561
+ ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.HASH]],
562
+ ['then', hashValuesFromTemp(t)],
563
+ ['else', ['if', ['result', 'f64'],
564
+ ['i32.and',
565
+ ['i32.eq', ['local.get', `$${tt}`], ['i32.const', PTR.OBJECT]],
566
+ ['i32.ne', ['global.get', '$__schema_tbl'], ['i32.const', 0]]],
567
+ ['then', objectValuesFromTemp(t)],
568
+ ['else', ['block', ['result', 'f64'], empty.init, empty.ptr]]]]]], 'f64')
569
+ }
570
+
512
571
  // Schema-keyed Object.keys: copy the schema's keys array (a jz Array of
513
572
  // STRINGs registered in __schema_tbl[sid]) into a fresh ARRAY so callers can
514
573
  // mutate without aliasing the schema substrate.
@@ -537,3 +596,31 @@ function objectKeysFromTemp(t) {
537
596
  ['br', `$kloop${id}`]]],
538
597
  mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${out}`])]
539
598
  }
599
+
600
+ function objectValuesFromTemp(t) {
601
+ inc('__alloc_hdr', '__ptr_offset')
602
+ const sid = tempI32('ovs'), src = tempI32('ovsrc'), n = tempI32('ovn')
603
+ const base = tempI32('ovbase'), out = tempI32('ovo'), i = tempI32('ovi')
604
+ const id = ctx.func.uniq++
605
+ return ['block', ['result', 'f64'],
606
+ ['local.set', `$${sid}`, ['i32.wrap_i64', ['i64.and',
607
+ ['i64.shr_u', ['i64.reinterpret_f64', ['local.get', `$${t}`]], ['i64.const', LAYOUT.AUX_SHIFT]],
608
+ ['i64.const', LAYOUT.AUX_MASK]]]],
609
+ ['local.set', `$${src}`, ['i32.wrap_i64', ['i64.and',
610
+ ['i64.load', ['i32.add', ['global.get', '$__schema_tbl'], ['i32.shl', ['local.get', `$${sid}`], ['i32.const', 3]]]],
611
+ ['i64.const', LAYOUT.OFFSET_MASK]]]],
612
+ ['local.set', `$${n}`, ['i32.load', ['i32.sub', ['local.get', `$${src}`], ['i32.const', 8]]]],
613
+ ['local.set', `$${base}`, ['call', '$__ptr_offset', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
614
+ ['local.set', `$${out}`, ['call', '$__alloc_hdr',
615
+ ['local.get', `$${n}`], ['local.get', `$${n}`], ['i32.const', 8]]],
616
+ ['local.set', `$${i}`, ['i32.const', 0]],
617
+ ['block', `$ovbrk${id}`, ['loop', `$ovloop${id}`,
618
+ ['br_if', `$ovbrk${id}`, ['i32.ge_s', ['local.get', `$${i}`], ['local.get', `$${n}`]]],
619
+ ['f64.store',
620
+ ['i32.add', ['local.get', `$${out}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]],
621
+ ['f64.load',
622
+ ['i32.add', ['local.get', `$${base}`], ['i32.shl', ['local.get', `$${i}`], ['i32.const', 3]]]]],
623
+ ['local.set', `$${i}`, ['i32.add', ['local.get', `$${i}`], ['i32.const', 1]]],
624
+ ['br', `$ovloop${id}`]]],
625
+ mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${out}`])]
626
+ }
package/module/string.js CHANGED
@@ -883,11 +883,12 @@ export default (ctx) => {
883
883
 
884
884
  ctx.core.emit['.string:slice'] = (str, start, end) => {
885
885
  inc('__str_slice')
886
- if (end != null) return typed(['call', '$__str_slice', asI64(emit(str)), asI32(emit(start)), asI32(emit(end))], 'f64')
886
+ const startIR = start == null ? ['i32.const', 0] : asI32(emit(start))
887
+ if (end != null) return typed(['call', '$__str_slice', asI64(emit(str)), startIR, asI32(emit(end))], 'f64')
887
888
  const t = temp('t')
888
889
  return typed(['block', ['result', 'f64'],
889
890
  ['local.set', `$${t}`, asF64(emit(str))],
890
- ['call', '$__str_slice', ['i64.reinterpret_f64', ['local.get', `$${t}`]], asI32(emit(start)),
891
+ ['call', '$__str_slice', ['i64.reinterpret_f64', ['local.get', `$${t}`]], startIR,
891
892
  ['call', '$__str_byteLen', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]]], 'f64')
892
893
  }
893
894
 
@@ -587,8 +587,14 @@ export default (ctx) => {
587
587
  }
588
588
  return `(func $__typed_idx (param $ptr i64) (param $i i32) (result f64)
589
589
  (local $off i32) (local $et i32) (local $len i32) (local $aux i32)
590
- (local.set $aux (call $__ptr_aux (local.get $ptr)))
591
590
  (local.set $off (call $__ptr_offset (local.get $ptr)))
591
+ ;; ARRAY fast path: __ptr_offset already followed any forwarding — read header len + f64.load, no $__len call.
592
+ (if (i32.and (i32.eq (call $__ptr_type (local.get $ptr)) (i32.const ${PTR.ARRAY})) (i32.ge_u (local.get $off) (i32.const 8)))
593
+ (then (return (if (result f64)
594
+ (i32.and (i32.ge_s (local.get $i) (i32.const 0)) (i32.lt_u (local.get $i) (i32.load (i32.sub (local.get $off) (i32.const 8)))))
595
+ (then (f64.load (i32.add (local.get $off) (i32.shl (local.get $i) (i32.const 3)))))
596
+ (else (f64.const nan:${UNDEF_NAN}))))))
597
+ (local.set $aux (call $__ptr_aux (local.get $ptr)))
592
598
  (if
593
599
  (i32.and
594
600
  (i32.eq (call $__ptr_type (local.get $ptr)) (i32.const ${PTR.TYPED}))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jz",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Modern functional JS compiling to WASM",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -38,8 +38,8 @@
38
38
  "author": "Dmitry Iv",
39
39
  "license": "MIT",
40
40
  "dependencies": {
41
- "subscript": "^10.4.2",
42
- "watr": "^4.5.1"
41
+ "subscript": "^10.4.7",
42
+ "watr": "^4.5.3"
43
43
  },
44
44
  "keywords": [
45
45
  "javascript",
package/src/analyze.js CHANGED
@@ -33,11 +33,27 @@ export const STMT_OPS = new Set([';', 'let', 'const', 'return', 'if', 'for', 'fo
33
33
  '=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??=',
34
34
  'throw', 'try', 'catch', 'finally', '++', '--', '()'])
35
35
 
36
+ /** Assignment operators — shared across analyze, plan, emit. */
37
+ export const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
38
+
36
39
  /** Distinguish a function block body `{ ... }` from an expression-bodied object literal `({a:1})`.
37
40
  * Both share the `'{}'` op tag; blocks have a non-`':'` first child (object literals start with key:val pairs). */
38
41
  export const isBlockBody = (body) =>
39
42
  Array.isArray(body) && body[0] === '{}' && body[1]?.[0] !== ':'
40
43
 
44
+ /** Extract integer value from AST literal node. Returns null if not a 32-bit integer. */
45
+ export function intLiteralValue(expr) {
46
+ let v = null
47
+ if (typeof expr === 'number') v = expr
48
+ else if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number') v = expr[1]
49
+ else if (Array.isArray(expr) && expr[0] === 'u-' && typeof expr[1] === 'number') v = -expr[1]
50
+ else if (typeof expr === 'string') v = repOf(expr)?.intConst ?? ctx.scope.constInts?.get(expr) ?? null
51
+ return v != null && Number.isInteger(v) && v >= -2147483648 && v <= 2147483647 ? v : null
52
+ }
53
+
54
+ /** Non-negative integer literal — used for string/typed-array index bounds. */
55
+ export const nonNegIntLiteral = (node) => { const n = intLiteralValue(node); return n != null && n >= 0 ? n : null }
56
+
41
57
  /** Collect all `return X` expressions (X != null) from a function body, skipping nested arrow funcs.
42
58
  * Pushes into `out`. Non-returning paths are silently skipped — pair with `alwaysReturns` if total
43
59
  * coverage matters, or with `hasBareReturn` to detect `return;` (undef result). */
@@ -1892,8 +1908,6 @@ export function analyzePtrUnboxable(body, locals, boxed) {
1892
1908
  for (const a of args) collect(a)
1893
1909
  }
1894
1910
 
1895
- const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=',
1896
- '<<=', '>>=', '>>>=', '||=', '&&=', '??='])
1897
1911
  const NULL_CMP_OPS = new Set(['==', '!=', '===', '!=='])
1898
1912
 
1899
1913
  function check(node) {
@@ -2035,8 +2049,6 @@ export function findFreeVars(node, bound, free, scope) {
2035
2049
  for (const a of args) findFreeVars(a, bound, free, scope)
2036
2050
  }
2037
2051
 
2038
- const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
2039
-
2040
2052
  /** Check if any of the given variable names are assigned anywhere in the AST. */
2041
2053
  export function findMutations(node, names, mutated) {
2042
2054
  if (node == null || typeof node !== 'object' || !Array.isArray(node)) return