jz 0.6.0 → 0.8.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.
Files changed (63) hide show
  1. package/README.md +99 -72
  2. package/bench/README.md +253 -100
  3. package/bench/bench.svg +58 -81
  4. package/cli.js +85 -12
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6196 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +222 -35
  9. package/interop.js +240 -141
  10. package/layout.js +34 -18
  11. package/module/array.js +99 -111
  12. package/module/collection.js +124 -30
  13. package/module/console.js +1 -1
  14. package/module/core.js +163 -19
  15. package/module/json.js +3 -3
  16. package/module/math.js +162 -3
  17. package/module/number.js +268 -13
  18. package/module/object.js +37 -5
  19. package/module/regex.js +8 -7
  20. package/module/simd.js +37 -5
  21. package/module/string.js +203 -168
  22. package/module/typedarray.js +565 -112
  23. package/package.json +26 -7
  24. package/src/abi/string.js +29 -29
  25. package/src/ast.js +19 -2
  26. package/src/autoload.js +3 -0
  27. package/src/compile/analyze-scans.js +174 -11
  28. package/src/compile/analyze.js +101 -4
  29. package/src/compile/cse-load.js +200 -0
  30. package/src/compile/emit-assign.js +82 -20
  31. package/src/compile/emit.js +592 -51
  32. package/src/compile/index.js +504 -89
  33. package/src/compile/infer.js +36 -1
  34. package/src/compile/loop-divmod.js +109 -0
  35. package/src/compile/loop-model.js +91 -0
  36. package/src/compile/loop-square.js +102 -0
  37. package/src/compile/narrow.js +275 -41
  38. package/src/compile/peel-stencil.js +215 -0
  39. package/src/compile/plan/advise.js +55 -1
  40. package/src/compile/plan/common.js +29 -0
  41. package/src/compile/plan/index.js +8 -1
  42. package/src/compile/plan/inline.js +180 -22
  43. package/src/compile/plan/literals.js +313 -24
  44. package/src/compile/plan/scope.js +115 -39
  45. package/src/compile/program-facts.js +21 -2
  46. package/src/ctx.js +96 -19
  47. package/src/helper-counters.js +137 -0
  48. package/src/ir.js +157 -20
  49. package/src/kind-traits.js +34 -3
  50. package/src/kind.js +79 -4
  51. package/src/op-policy.js +5 -2
  52. package/src/ops.js +119 -0
  53. package/src/optimize/index.js +1274 -151
  54. package/src/optimize/recurse.js +182 -0
  55. package/src/optimize/vectorize.js +4187 -253
  56. package/src/prepare/index.js +75 -14
  57. package/src/prepare/lift-iife.js +149 -0
  58. package/src/reps.js +6 -2
  59. package/src/static.js +9 -0
  60. package/src/type.js +63 -51
  61. package/src/wat/assemble.js +286 -21
  62. package/src/widen.js +21 -0
  63. package/src/wat/optimize.js +0 -3760
package/module/math.js CHANGED
@@ -129,7 +129,11 @@ export default (ctx) => {
129
129
  // undefined→NaN, and strings get parsed. Without this, raw NaN-boxed pointers
130
130
  // (null/undefined/strings) would propagate through math.log etc. and surface
131
131
  // as the original null/undefined sentinel after decode.
132
- const fn = (name, ...args) => typed(['call', `$${name}`, ...args.map(a => toNumF64(a, emit(a)))], 'f64')
132
+ // A canon'd operand feeding a math call is redundant: the callee (log/sin/exp/…)
133
+ // propagates a non-canonical NaN identically and re-canon-izes its own result.
134
+ // `Math.log(Math.log(x))` thus sheds the inner per-call select + f64.ne.
135
+ const stripCanon = (v) => (v && v.canonOf != null) ? typed(v.canonOf, 'f64') : v
136
+ const fn = (name, ...args) => typed(['call', `$${name}`, ...args.map(a => stripCanon(toNumF64(a, emit(a))))], 'f64')
133
137
 
134
138
  // Canonicalize a possibly-NaN f64 result. A wasm arithmetic op that mints a
135
139
  // fresh NaN (f64.sqrt of a negative, f64.min/max with a NaN operand) leaves
@@ -139,12 +143,18 @@ export default (ctx) => {
139
143
  // untyped === / typeof. So fold any NaN back to canonical where one is born.
140
144
  const canon = (node) => {
141
145
  const t = temp('cn')
142
- return typed(['block', ['result', 'f64'],
146
+ const ir = typed(['block', ['result', 'f64'],
143
147
  ['local.set', `$${t}`, node],
144
148
  ['select',
145
149
  ['f64.const', 'nan'],
146
150
  ['local.get', `$${t}`],
147
151
  ['f64.ne', ['local.get', `$${t}`], ['local.get', `$${t}`]]]], 'f64')
152
+ // Tag the wrapper so a NaN-propagating f64 consumer (`f64.add`/`mul`/… that
153
+ // itself canon-izes on escape) can strip the redundant inner canon: the raw
154
+ // op result `node` propagates a freshly-minted NaN identically through the
155
+ // consumer, and only the OUTERMOST escaping value needs the canonical form.
156
+ ir.canonOf = node
157
+ return ir
148
158
  }
149
159
 
150
160
  // sqrt(x) needs no NaN-canon when its argument is provably ≥ 0 with no spurious NaN:
@@ -166,7 +176,9 @@ export default (ctx) => {
166
176
  n[0] === 'f64.const' ? (typeof n[1] === 'number' && n[1] >= 0) : false
167
177
  const sqrtIR = (a) => { const ir = f('f64.sqrt', a); return nonNegF64(ir[1]) ? ir : canon(ir) }
168
178
 
169
- // Constants
179
+ // Constants — each folds to its `(f64.const …)` inline (no stdlib dep, hence
180
+ // direct emit rather than reg). Written out (not a `Math[name]` loop) because the
181
+ // self-host subset can't resolve dynamic access on the `Math` compile-time namespace.
170
182
  ctx.core.emit['math.PI'] = () => typed(['f64.const', Math.PI], 'f64')
171
183
  ctx.core.emit['math.E'] = () => typed(['f64.const', Math.E], 'f64')
172
184
  ctx.core.emit['math.LN2'] = () => typed(['f64.const', Math.LN2], 'f64')
@@ -348,6 +360,22 @@ export default (ctx) => {
348
360
  // We emit, check, and if not foldable, the emitted IR is used by the fallthrough paths.
349
361
  const irA = toNumF64(a, emit(a)), irB = toNumF64(b, emit(b))
350
362
  if (isLit(irA) && isLit(irB)) return typed(['f64.const', Math.pow(litVal(irA), litVal(irB))], 'f64')
363
+ // Constant non-integer exponent c: inline Math.pow(x,c) as exp(c·log(x)) — that IS $math.pow's
364
+ // own non-integer tail (the final line of $math.pow below), so it is BIT-IDENTICAL to the call
365
+ // for every finite x and for x ∈ {±0, +∞, NaN}: log+exp carry the edges (log(NaN)=NaN,
366
+ // log(0)=−∞, log(<0)=NaN, log(+∞)=+∞; exp(±∞)=∞/0). Skipping the ~15-branch pow special-case
367
+ // ladder + the call frame is a per-pixel win on the gamma curves (v**0.45, a**(1/2.4)) that
368
+ // dominate tone-mapping, and a program whose only pow is folded this way never pulls $math.pow.
369
+ // The ONE divergence is x=−∞: this yields NaN where Math.pow gives ±∞ — the same deliberate
370
+ // boundary trade jz already makes for `(−∞)**0.5` (see the sqrt fold above); −∞ is never a real
371
+ // tone-map/gamma base. Integers stay on $math.pow (its square-and-multiply path is bit-different
372
+ // from exp/log); ±0.5 stays sqrt / the exact pow path (handled above).
373
+ if (isLit(irB)) {
374
+ const c = litVal(irB)
375
+ if (Number.isFinite(c) && !Number.isInteger(c) && c !== 0.5 && c !== -0.5)
376
+ return (inc('math.exp'), inc('math.log'),
377
+ typed(['call', '$math.exp', ['f64.mul', irB, ['call', '$math.log', irA]]], 'f64'))
378
+ }
351
379
  // base 2 → dedicated 2^y (exp2 is exact for integer y, and skips exp's ×ln2/÷ln2).
352
380
  // Every other literal base keeps $math.pow: `exp(y·ln base)` would lose ulps and,
353
381
  // worse, miss Math.pow's integer-exponent semantics — e.g. `16 ** flen` with a
@@ -499,6 +527,137 @@ export default (ctx) => {
499
527
  (if (f64.eq (f64.abs (local.get $x)) (f64.const inf)) (then (return (f64.const nan))))
500
528
  (f64.div (call $math.sin (local.get $x)) (call $math.cos (local.get $x))))`)
501
529
 
530
+ // ── f64x2 SIMD sin/cos — both lanes through one polynomial ───────────────────
531
+ // The scalar sin_core/cos_core algorithm lifted to two f64 lanes: same
532
+ // round-to-nearest π reduction, same minimax poly (SIN_C/COS_C), same quadrant
533
+ // parity — but every branch becomes branchless so two independent angles cost one
534
+ // evaluation. A kernel computing sin and cos of distinct args (rotations, de Jong /
535
+ // Clifford maps, oscillator banks) packs them two-per-vector and ≈halves trig cost.
536
+ // • Both reduction passes run unconditionally: for an in-range r the second pass'
537
+ // q2 = nearest(r/π) = 0, so it's an exact no-op — no per-lane branch needed, and
538
+ // it still rescues |x| up to ~1e15 just like the scalar's gated pass.
539
+ // • NaN and ±∞ fall out as NaN through the arithmetic (∞ − ∞·π = NaN); a v128 lane
540
+ // is raw f64, not a NaN-box, so the canonical-NaN guard the scalar needs is moot.
541
+ // • Sign flip for odd quadrants is `r XOR (mask & −0.0)` (mask = |q|>0.5); final
542
+ // min/max clamps the ~1e-8 poly overshoot to [−1,1], same as scalar.
543
+ const splat = (c) => `(f64x2.splat (f64.const ${c}))`
544
+ const horner2 = (cs, v = '$r2') => cs.reduceRight((acc, c, i) =>
545
+ i === cs.length - 1 ? splat(c)
546
+ : `(f64x2.add ${splat(c)} (f64x2.mul (local.get ${v}) ${acc}))`, '')
547
+ // fdlibm log's even/odd split, as f64x2 coefficient arrays (the scalar $math.log inlines them):
548
+ // t1 = w·(L3 + w·(L5 + w·L7)), t2 = z·(L2 + w·(L4 + w·(L6 + w·L8))). Vectorized log_v reuses these.
549
+ // STRING coefficients (not numbers): horner2 only ever interpolates them into the WAT
550
+ // (`(f64.const ${c})`), and under self-host that `${number}` goes through jz's shortest-repr
551
+ // __ftoa, which truncates to ~9 sig figs — so the kernel's log_v poly diverged from scalar
552
+ // $math.log (which inlines the exact literals). As strings, the full-precision decimal is
553
+ // embedded verbatim and watr parses it exactly, in both the host and the self-host kernel.
554
+ const LOG_T1 = ['0.3999999999940941908', '0.2222219843214978396', '0.1531383769920937332']
555
+ const LOG_T2 = ['0.6666666666666735130', '0.2857142874366239149', '0.1818357216161805012', '0.1479819860511658591']
556
+ // Shared reduce → r ∈ [−π/2,π/2] in $r, quadrant parity in $q (branchless, 2 passes).
557
+ const reduce2 = `
558
+ (local.set $q (f64x2.nearest (f64x2.mul (local.get $x) ${splat(INV_PI)})))
559
+ (local.set $r (f64x2.sub (local.get $x) (f64x2.mul (local.get $q) ${splat(PI)})))
560
+ (local.set $q2 (f64x2.nearest (f64x2.mul (local.get $r) ${splat(INV_PI)})))
561
+ (local.set $r (f64x2.sub (local.get $r) (f64x2.mul (local.get $q2) ${splat(PI)})))
562
+ (local.set $q (f64x2.add (local.get $q) (local.get $q2)))
563
+ (local.set $q (f64x2.sub (local.get $q) (f64x2.mul ${splat(2)} (f64x2.nearest (f64x2.mul (local.get $q) ${splat(0.5)})))))
564
+ (local.set $r2 (f64x2.mul (local.get $r) (local.get $r)))`
565
+ // r XOR (|q|>0.5 ? −0.0 : 0), then clamp to [−1,1].
566
+ const signClamp = `
567
+ (local.set $r (v128.xor (local.get $r)
568
+ (v128.and (f64x2.gt (f64x2.abs (local.get $q)) ${splat(0.5)}) ${splat('-0.0')})))
569
+ (f64x2.min (f64x2.max (local.get $r) ${splat(-1)}) ${splat(1)})`
570
+ wat('math.sin2', `(func $math.sin2 (param $x v128) (result v128)
571
+ (local $q v128) (local $q2 v128) (local $r v128) (local $r2 v128)${reduce2}
572
+ (local.set $r (f64x2.mul (local.get $r) ${horner2(SIN_C)}))${signClamp})`)
573
+ wat('math.cos2', `(func $math.cos2 (param $x v128) (result v128)
574
+ (local $q v128) (local $q2 v128) (local $r v128) (local $r2 v128)${reduce2}
575
+ (local.set $r ${horner2(COS_C)})${signClamp})`)
576
+ // pow has no cheap 2-lane polynomial (it is exp(y·ln x) with cancellation-sensitive reductions),
577
+ // so the f64x2 mirror computes each lane with the scalar $math.pow and repacks — BIT-EXACT by
578
+ // construction. No transcendental speedup, but it keeps a pow-bearing pixel kernel's surrounding
579
+ // f64x2 arithmetic vectorized (the per-pixel-color pass only emits this when a truly-2-wide op —
580
+ // sin2/cos2/sqrt — already justifies the pair, so the extract/repack never makes a kernel slower).
581
+ wat('math.pow2', `(func $math.pow2 (param $x v128) (param $y v128) (result v128)
582
+ (f64x2.replace_lane 1
583
+ (f64x2.splat (call $math.pow (f64x2.extract_lane 0 (local.get $x)) (f64x2.extract_lane 0 (local.get $y))))
584
+ (call $math.pow (f64x2.extract_lane 1 (local.get $x)) (f64x2.extract_lane 1 (local.get $y)))))`, ['math.pow'])
585
+
586
+ // atan2/hypot/log have no cheap 2-lane polynomial (multi-`return` fdlibm bodies), so — like pow2 —
587
+ // each f64x2 mirror computes both lanes with the SCALAR helper and repacks: BIT-EXACT by
588
+ // construction. The per-pixel-color pass only emits these when a truly-2-wide op (sin2/cos2/sqrt)
589
+ // already justifies the f64x2 pair, so the extract/repack never makes a kernel slower.
590
+ // NOTE: names avoid the $math.log2/$math.exp2 collision (those are log-/exp-BASE-2).
591
+ wat('math.atan2_2', `(func $math.atan2_2 (param $y v128) (param $x v128) (result v128)
592
+ (f64x2.replace_lane 1
593
+ (f64x2.splat (call $math.atan2 (f64x2.extract_lane 0 (local.get $y)) (f64x2.extract_lane 0 (local.get $x))))
594
+ (call $math.atan2 (f64x2.extract_lane 1 (local.get $y)) (f64x2.extract_lane 1 (local.get $x)))))`, ['math.atan2'])
595
+ wat('math.hypot_2', `(func $math.hypot_2 (param $x v128) (param $y v128) (result v128)
596
+ (f64x2.replace_lane 1
597
+ (f64x2.splat (call $math.hypot (f64x2.extract_lane 0 (local.get $x)) (f64x2.extract_lane 0 (local.get $y))))
598
+ (call $math.hypot (f64x2.extract_lane 1 (local.get $x)) (f64x2.extract_lane 1 (local.get $y)))))`, ['math.hypot'])
599
+ // True f64x2 log — both lanes through one fdlibm poly (≈2× over two scalar calls). The HOT path
600
+ // (both lanes a normal finite x>0) mirrors $math.log's normal branch op-for-op: bit-exact (the
601
+ // sqrt2-center conditional becomes a per-lane bitselect; the i32 exponent k becomes an f64 via the
602
+ // 2^52 magic-add, identical to convert_i32_s for |k|≤1075). Any other lane (≤0/∞/NaN/denormal)
603
+ // routes BOTH lanes to the scalar fallback → bit-exact by construction, edges never lose precision.
604
+ wat('math.log_v', `(func $math.log_v (param $x v128) (result v128)
605
+ (local $k v128) (local $m v128) (local $mask v128) (local $f v128) (local $s v128) (local $z v128) (local $w v128) (local $hfsq v128)
606
+ (if (result v128)
607
+ (i64x2.all_true (v128.and
608
+ (f64x2.ge (local.get $x) (f64x2.splat (f64.const 0x1p-1022)))
609
+ (f64x2.lt (local.get $x) (f64x2.splat (f64.const inf)))))
610
+ (then
611
+ (local.set $k (f64x2.sub
612
+ (v128.or (v128.and (i64x2.shr_u (local.get $x) (i32.const 52)) (i64x2.splat (i64.const 0x7ff)))
613
+ (i64x2.splat (i64.const 0x4330000000000000)))
614
+ (f64x2.splat (f64.const 4503599627371519))))
615
+ (local.set $m (v128.or (v128.and (local.get $x) (i64x2.splat (i64.const 0x000fffffffffffff))) (i64x2.splat (i64.const 0x3ff0000000000000))))
616
+ (local.set $mask (f64x2.ge (local.get $m) (f64x2.splat (f64.const 1.4142135623730951))))
617
+ (local.set $m (v128.bitselect (f64x2.mul (local.get $m) (f64x2.splat (f64.const 0.5))) (local.get $m) (local.get $mask)))
618
+ (local.set $k (f64x2.add (local.get $k) (v128.and (local.get $mask) (f64x2.splat (f64.const 1.0)))))
619
+ (local.set $f (f64x2.sub (local.get $m) (f64x2.splat (f64.const 1.0))))
620
+ (local.set $s (f64x2.div (local.get $f) (f64x2.add (local.get $f) (f64x2.splat (f64.const 2.0)))))
621
+ (local.set $z (f64x2.mul (local.get $s) (local.get $s)))
622
+ (local.set $w (f64x2.mul (local.get $z) (local.get $z)))
623
+ (local.set $hfsq (f64x2.mul (f64x2.splat (f64.const 0.5)) (f64x2.mul (local.get $f) (local.get $f))))
624
+ (f64x2.add
625
+ (f64x2.mul (local.get $k) (f64x2.splat (f64.const ${Math.LN2})))
626
+ (f64x2.add (f64x2.sub (local.get $f) (local.get $hfsq))
627
+ (f64x2.mul (local.get $s) (f64x2.add (local.get $hfsq)
628
+ (f64x2.add
629
+ (f64x2.mul (local.get $w) ${horner2(LOG_T1, '$w')})
630
+ (f64x2.mul (local.get $z) ${horner2(LOG_T2, '$w')})))))))
631
+ (else
632
+ (f64x2.replace_lane 1
633
+ (f64x2.splat (call $math.log (f64x2.extract_lane 0 (local.get $x))))
634
+ (call $math.log (f64x2.extract_lane 1 (local.get $x)))))))`, ['math.log'])
635
+
636
+ // True f64x2 exp2 — hot path (round(y) ∈ (−1023,1024), the normal-result range) mirrors $math.exp2's
637
+ // single-IEEE-build branch op-for-op (Horner over f=y−round(y), 2^k via (k+1023)<<52); edges
638
+ // (overflow/underflow/denormal/NaN) route both lanes to the scalar fallback → bit-exact.
639
+ wat('math.exp2_v', `(func $math.exp2_v (param $y v128) (result v128)
640
+ (local $k v128) (local $f v128)
641
+ (local.set $k (f64x2.nearest (local.get $y)))
642
+ (if (result v128)
643
+ (i64x2.all_true (v128.and
644
+ (f64x2.gt (local.get $k) (f64x2.splat (f64.const -1023)))
645
+ (f64x2.lt (local.get $k) (f64x2.splat (f64.const 1024)))))
646
+ (then
647
+ (local.set $f (f64x2.sub (local.get $y) (local.get $k)))
648
+ (f64x2.mul ${horner2(EXP2_C, '$f')}
649
+ (i64x2.shl (i64x2.add
650
+ (i64x2.extend_low_i32x4_s (i32x4.trunc_sat_f64x2_s_zero (local.get $k)))
651
+ (i64x2.splat (i64.const 1023))) (i32.const 52))))
652
+ (else
653
+ (f64x2.replace_lane 1
654
+ (f64x2.splat (call $math.exp2 (f64x2.extract_lane 0 (local.get $y))))
655
+ (call $math.exp2 (f64x2.extract_lane 1 (local.get $y)))))))`, ['math.exp2'])
656
+
657
+ // e^x = 2^(x·log2e) — defers to exp2_v exactly as scalar $math.exp defers to $math.exp2. Bit-exact.
658
+ wat('math.exp_v', `(func $math.exp_v (param $x v128) (result v128)
659
+ (call $math.exp2_v (f64x2.mul (local.get $x) (f64x2.splat (f64.const ${Math.LOG2E})))))`, ['math.exp2_v'])
660
+
502
661
  // e^x = 2^(x·log2 e) — defer to the faster $math.exp2 (one multiply, no division, and
503
662
  // exp2's NaN/overflow/underflow guards cover exp's). Accurate to exp2's ~6e-9, better
504
663
  // than the old 7-term Taylor, and it shares one code path with `2**`.
package/module/number.js CHANGED
@@ -145,24 +145,237 @@ const sciExponent = (tail) => `
145
145
  ${tail}))`
146
146
 
147
147
  // Apply the accumulated base-10 exponent to $result via __pow10.
148
+ // Used as fallback when EL cannot determine a unique rounding.
148
149
  const POW10_SCALE = `
149
150
  (if (i32.gt_s (local.get $decExp) (i32.const 0))
150
151
  (then (local.set $result (f64.mul (local.get $result) (call $__pow10 (local.get $decExp))))))
151
152
  (if (i32.lt_s (local.get $decExp) (i32.const 0))
152
153
  (then (local.set $result (f64.div (local.get $result) (call $__pow10 (i32.sub (i32.const 0) (local.get $decExp)))))))`
153
154
 
155
+ // Eisel-Lemire correctly-rounded decimal-to-f64.
156
+ // Used in place of FINISH_SIGNIFICAND + POW10_SCALE. Keeps $mant as i64 until after
157
+ // sciExponent finalizes $decExp, then calls $__dec_to_f64 with both.
158
+ // Falls back to f64.convert_i64_u + POW10_SCALE when EL returns NaN (ambiguous).
159
+ // The caller handles sign INSIDE this fragment (so the final return is the signed result).
160
+ const EL_SCALE = `
161
+ (if (i32.eqz (local.get $seen)) (then (return (f64.const nan))))
162
+ (if (local.get $round) (then (local.set $mant (i64.add (local.get $mant) (i64.const 1)))))
163
+ (local.set $result (call $__dec_to_f64 (local.get $mant) (local.get $decExp)))
164
+ (if (f64.ne (local.get $result) (local.get $result))
165
+ (then
166
+ (local.set $result (f64.convert_i64_u (local.get $mant)))
167
+ ${POW10_SCALE}))
168
+ (if (local.get $neg) (then (local.set $result (f64.neg (local.get $result)))))`
169
+
170
+ // $__dec_to_f64: Eisel-Lemire correctly-rounded f64 from (mant × 10^exp10).
171
+ // Returns NaN (as sentinel) for ambiguous cases — caller falls back to POW10_SCALE.
172
+ // Reads 651-entry 128-bit power-of-ten table from global $__el_tbl (injected at compile time).
173
+ // Table layout: entry[i] = lo_i64_LE || hi_i64_LE, i = exp10 + 342.
174
+ //
175
+ // Algorithm: normalize mant to 64 bits, multiply by 128-bit table entry to get 128-bit
176
+ // product (prodHi:prodLo), extract 52-bit IEEE mantissa with correct rounding.
177
+ // Handles subnormals, overflow to Infinity, and early-exit for 0/trivial ranges.
178
+ const DEC_TO_F64_WAT = `(func $__dec_to_f64
179
+ (param $mant i64) (param $exp10 i32)
180
+ (result f64)
181
+ (local $mBits i32)
182
+ (local $w i64)
183
+ (local $tbl i32)
184
+ (local $tblHi i64) (local $tblLo i64)
185
+ ;; 128-bit product: prodHi × 2^64 + prodLo
186
+ (local $p1hi i64) (local $p1lo i64)
187
+ (local $p2hi i64) (local $p2lo i64)
188
+ (local $prodHi i64) (local $prodLo i64)
189
+ (local $carry i64)
190
+ (local $lz i32)
191
+ (local $log2floor i32)
192
+ (local $exp2 i32) (local $biased i32)
193
+ (local $roundShift i32)
194
+ (local $roundBit i64)
195
+ (local $sticky i64)
196
+ (local $mant52 i64)
197
+ (local $subnShift i32) (local $totalShift i32)
198
+ (local $snRound i64) (local $snSticky i64) (local $snMant i64)
199
+ ;; 32-bit limb temps for mul64
200
+ (local $a0 i32) (local $a1 i32) (local $b0 i32) (local $b1 i32)
201
+ (local $t00 i64) (local $t01 i64) (local $t10 i64) (local $t11 i64)
202
+ (local $mid i64) (local $mid_carry i64)
203
+ ;; Zero check
204
+ (if (i64.eqz (local.get $mant)) (then (return (f64.const 0))))
205
+ ;; Compute bit length of mant (1..64) by normalizing to 64 bits via clz
206
+ ;; mBits = 64 - clz64(mant)
207
+ (local.set $mBits (i32.sub (i32.const 64) (i32.wrap_i64 (i64.clz (local.get $mant)))))
208
+ ;; Normalize mant to 64-bit: w = mant << (64 - mBits)
209
+ (local.set $w (i64.shl (local.get $mant) (i64.extend_i32_u (i32.sub (i32.const 64) (local.get $mBits)))))
210
+ ;; Range checks: the table is TRIMMED to exp10 in [-65..65] (covers every
211
+ ;; real-world / source / JSON constant; fft/synth's coefficients are ~10^-23).
212
+ ;; Outside that, return NaN → caller falls back to POW10_SCALE (the pre-EL path,
213
+ ;; ~1 ULP for moderate exponents, exact only near the f64 limits no real literal
214
+ ;; reaches). This keeps the data segment ~2KB instead of ~10KB on every program
215
+ ;; that does an untyped numeric coercion (which pulls __to_num).
216
+ (if (i32.or (i32.lt_s (local.get $exp10) (i32.const -65))
217
+ (i32.gt_s (local.get $exp10) (i32.const 65)))
218
+ (then (return (f64.const nan))))
219
+ ;; Load 128-bit table entry for exp10: tbl = $__el_tbl + (exp10 + 65) * 16
220
+ (local.set $tbl (i32.add (global.get $__el_tbl) (i32.shl (i32.add (local.get $exp10) (i32.const 65)) (i32.const 4))))
221
+ (local.set $tblLo (i64.load (local.get $tbl)))
222
+ (local.set $tblHi (i64.load (i32.add (local.get $tbl) (i32.const 8))))
223
+ ;; ─── Two-product: (prodHi:prodLo) = w × tblHi + (w × tblLo >> 64) ──────────
224
+ ;; mul64(w, tblHi) → p1hi:p1lo
225
+ ;; Split w into 32-bit halves
226
+ (local.set $a0 (i32.wrap_i64 (i64.and (local.get $w) (i64.const 0xFFFFFFFF))))
227
+ (local.set $a1 (i32.wrap_i64 (i64.shr_u (local.get $w) (i64.const 32))))
228
+ (local.set $b0 (i32.wrap_i64 (i64.and (local.get $tblHi) (i64.const 0xFFFFFFFF))))
229
+ (local.set $b1 (i32.wrap_i64 (i64.shr_u (local.get $tblHi) (i64.const 32))))
230
+ (local.set $t00 (i64.mul (i64.extend_i32_u (local.get $a0)) (i64.extend_i32_u (local.get $b0))))
231
+ (local.set $t01 (i64.mul (i64.extend_i32_u (local.get $a0)) (i64.extend_i32_u (local.get $b1))))
232
+ (local.set $t10 (i64.mul (i64.extend_i32_u (local.get $a1)) (i64.extend_i32_u (local.get $b0))))
233
+ (local.set $t11 (i64.mul (i64.extend_i32_u (local.get $a1)) (i64.extend_i32_u (local.get $b1))))
234
+ ;; mid = t01 + (t00>>32), track carry; then mid += t10, track carry
235
+ ;; Each addition can carry at most 1 bit; total carry ≤ 2 → hi += carry<<32
236
+ (local.set $mid (i64.add (local.get $t01) (i64.shr_u (local.get $t00) (i64.const 32))))
237
+ (local.set $mid_carry (i64.extend_i32_u (i64.lt_u (local.get $mid) (local.get $t01))))
238
+ (local.set $mid (i64.add (local.get $mid) (local.get $t10)))
239
+ (local.set $mid_carry (i64.add (local.get $mid_carry)
240
+ (i64.extend_i32_u (i64.lt_u (local.get $mid) (local.get $t10)))))
241
+ (local.set $p1hi (i64.add
242
+ (i64.add (local.get $t11) (i64.shr_u (local.get $mid) (i64.const 32)))
243
+ (i64.shl (local.get $mid_carry) (i64.const 32))))
244
+ (local.set $p1lo (i64.or
245
+ (i64.and (local.get $t00) (i64.const 0xFFFFFFFF))
246
+ (i64.shl (i64.and (local.get $mid) (i64.const 0xFFFFFFFF)) (i64.const 32))))
247
+ ;; mul64(w, tblLo) → p2hi:p2lo
248
+ (local.set $b0 (i32.wrap_i64 (i64.and (local.get $tblLo) (i64.const 0xFFFFFFFF))))
249
+ (local.set $b1 (i32.wrap_i64 (i64.shr_u (local.get $tblLo) (i64.const 32))))
250
+ (local.set $t00 (i64.mul (i64.extend_i32_u (local.get $a0)) (i64.extend_i32_u (local.get $b0))))
251
+ (local.set $t01 (i64.mul (i64.extend_i32_u (local.get $a0)) (i64.extend_i32_u (local.get $b1))))
252
+ (local.set $t10 (i64.mul (i64.extend_i32_u (local.get $a1)) (i64.extend_i32_u (local.get $b0))))
253
+ (local.set $t11 (i64.mul (i64.extend_i32_u (local.get $a1)) (i64.extend_i32_u (local.get $b1))))
254
+ (local.set $mid (i64.add (local.get $t01) (i64.shr_u (local.get $t00) (i64.const 32))))
255
+ (local.set $mid_carry (i64.extend_i32_u (i64.lt_u (local.get $mid) (local.get $t01))))
256
+ (local.set $mid (i64.add (local.get $mid) (local.get $t10)))
257
+ (local.set $mid_carry (i64.add (local.get $mid_carry)
258
+ (i64.extend_i32_u (i64.lt_u (local.get $mid) (local.get $t10)))))
259
+ (local.set $p2hi (i64.add
260
+ (i64.add (local.get $t11) (i64.shr_u (local.get $mid) (i64.const 32)))
261
+ (i64.shl (local.get $mid_carry) (i64.const 32))))
262
+ (local.set $p2lo (i64.or
263
+ (i64.and (local.get $t00) (i64.const 0xFFFFFFFF))
264
+ (i64.shl (i64.and (local.get $mid) (i64.const 0xFFFFFFFF)) (i64.const 32))))
265
+ ;; Combine: prodHi:prodLo = p1hi:(p1lo + p2hi) with carry propagation
266
+ (local.set $carry (i64.add (local.get $p1lo) (local.get $p2hi)))
267
+ ;; Check if carry propagated (carry < p1lo → overflow into prodHi)
268
+ (local.set $prodHi (i64.add (local.get $p1hi)
269
+ (i64.extend_i32_u (i64.lt_u (local.get $carry) (local.get $p1lo)))))
270
+ (local.set $prodLo (local.get $carry))
271
+ ;; ─── Determine lz (leading-zero flag): 1 if MSB of prodHi is 0 ──────────────
272
+ (local.set $lz (i32.wrap_i64 (i64.xor (i64.shr_u (local.get $prodHi) (i64.const 63)) (i64.const 1))))
273
+ ;; ─── Compute biased exponent ─────────────────────────────────────────────────
274
+ ;; floor(exp10 * log2(10)) via fixed-point: (exp10 * 14267572527) >> 32
275
+ ;; 14267572527 = floor(log2(10) * 2^32) — correct for all exp10 in -342..308.
276
+ (local.set $log2floor (i32.wrap_i64 (i64.shr_s
277
+ (i64.mul (i64.extend_i32_s (local.get $exp10)) (i64.const 14267572527))
278
+ (i64.const 32))))
279
+ ;; exp2 = mBits + floor(exp10 * log2(10)) - lz
280
+ (local.set $exp2 (i32.sub (i32.add (local.get $mBits) (local.get $log2floor)) (local.get $lz)))
281
+ (local.set $biased (i32.add (local.get $exp2) (i32.const 1023)))
282
+ ;; Overflow → Infinity
283
+ (if (i32.ge_s (local.get $biased) (i32.const 2047)) (then (return (f64.const inf))))
284
+ ;; ─── Subnormal path (biased ≤ 0) ────────────────────────────────────────────
285
+ (if (i32.le_s (local.get $biased) (i32.const 0))
286
+ (then
287
+ ;; total_shift = 11 - lz + (1 - biased) = 12 - lz - biased
288
+ (local.set $totalShift (i32.sub (i32.sub (i32.const 12) (local.get $lz)) (local.get $biased)))
289
+ ;; If totalShift >= 64: prodHi >> totalShift == 0 in BigInt, but here:
290
+ ;; For totalShift in [64..127]: snMant = (prodHi >> (totalShift-64)) >> 64... = 0
291
+ ;; We just use BigInt-style: clamp to 0 if >=64 (prodHi is 64-bit)
292
+ (if (i32.ge_u (local.get $totalShift) (i32.const 64))
293
+ (then
294
+ ;; All mantissa bits are 0; only rounding could give min subnormal.
295
+ ;; round bit: at position (totalShift-1) of prodHi → always 0 for totalShift>=65
296
+ ;; For totalShift==64: round bit = bit 63 of prodHi = MSB
297
+ (if (i32.eq (local.get $totalShift) (i32.const 64))
298
+ (then
299
+ (local.set $snRound (i64.shr_u (local.get $prodHi) (i64.const 63)))
300
+ (local.set $snSticky (i64.or (local.get $prodLo) (local.get $p2lo)))
301
+ ;; Return min-subnormal if snRound=1 AND snSticky!=0 (boolean AND, not bitwise)
302
+ (if (i32.and
303
+ (i32.wrap_i64 (local.get $snRound))
304
+ (i64.ne (local.get $snSticky) (i64.const 0)))
305
+ (then (return (f64.reinterpret_i64 (i64.const 1)))))
306
+ ))
307
+ (return (f64.const 0))))
308
+ ;; totalShift in [1..63]: extract directly from prodHi
309
+ (local.set $snMant (i64.and
310
+ (i64.shr_u (local.get $prodHi) (i64.extend_i32_u (local.get $totalShift)))
311
+ (i64.const 0x000FFFFFFFFFFFFF)))
312
+ (local.set $snRound
313
+ (i64.and (i64.shr_u (local.get $prodHi)
314
+ (i64.extend_i32_u (i32.sub (local.get $totalShift) (i32.const 1))))
315
+ (i64.const 1)))
316
+ (local.set $snSticky (i64.or
317
+ (i64.and (local.get $prodHi)
318
+ (i64.sub (i64.shl (i64.const 1) (i64.extend_i32_u (i32.sub (local.get $totalShift) (i32.const 1))))
319
+ (i64.const 1)))
320
+ (i64.or (local.get $prodLo) (local.get $p2lo))))
321
+ ;; Round: snMant++ if roundBit && (sticky > 0 || snMant is odd)
322
+ (if (i64.ne (i64.and (local.get $snRound)
323
+ (i64.or (i64.extend_i32_u (i64.ne (local.get $snSticky) (i64.const 0)))
324
+ (i64.and (local.get $snMant) (i64.const 1))))
325
+ (i64.const 0))
326
+ (then (local.set $snMant (i64.add (local.get $snMant) (i64.const 1)))))
327
+ ;; Overflow of subnormal mantissa → minimum normal (biased=1, mant=0)
328
+ (if (i64.ge_u (local.get $snMant) (i64.const 0x0010000000000000))
329
+ (then (return (f64.reinterpret_i64 (i64.const 0x0010000000000000)))))
330
+ (return (f64.reinterpret_i64 (local.get $snMant)))))
331
+ ;; ─── Normal path ─────────────────────────────────────────────────────────────
332
+ ;; roundShift = 10 - lz (bit position of round bit in prodHi)
333
+ (local.set $roundShift (i32.sub (i32.const 10) (local.get $lz)))
334
+ (local.set $roundBit (i64.and
335
+ (i64.shr_u (local.get $prodHi) (i64.extend_i32_u (local.get $roundShift)))
336
+ (i64.const 1)))
337
+ ;; sticky = bits below roundBit in prodHi, plus all of prodLo and p2lo
338
+ (local.set $sticky (i64.or
339
+ (i64.and (local.get $prodHi)
340
+ (i64.sub (i64.shl (i64.const 1) (i64.extend_i32_u (local.get $roundShift)))
341
+ (i64.const 1)))
342
+ (i64.or (local.get $prodLo) (local.get $p2lo))))
343
+ ;; mant52 = (prodHi >> (11 - lz)) & MASK52
344
+ (local.set $mant52 (i64.and
345
+ (i64.shr_u (local.get $prodHi) (i64.extend_i32_u (i32.sub (i32.const 11) (local.get $lz))))
346
+ (i64.const 0x000FFFFFFFFFFFFF)))
347
+ ;; Round: mant52++ if roundBit && (sticky > 0 || mant52 is odd)
348
+ (if (i64.ne (i64.and (local.get $roundBit)
349
+ (i64.or (i64.extend_i32_u (i64.ne (local.get $sticky) (i64.const 0)))
350
+ (i64.and (local.get $mant52) (i64.const 1))))
351
+ (i64.const 0))
352
+ (then (local.set $mant52 (i64.add (local.get $mant52) (i64.const 1)))))
353
+ ;; Mantissa overflow: carry into exponent
354
+ (if (i64.ge_u (local.get $mant52) (i64.const 0x0010000000000000))
355
+ (then
356
+ (local.set $mant52 (i64.const 0))
357
+ (local.set $biased (i32.add (local.get $biased) (i32.const 1)))))
358
+ ;; Final overflow check after rounding
359
+ (if (i32.ge_s (local.get $biased) (i32.const 2047)) (then (return (f64.const inf))))
360
+ ;; Assemble IEEE 754 bits: biased_exp << 52 | mant52
361
+ (f64.reinterpret_i64
362
+ (i64.or
363
+ (i64.shl (i64.extend_i32_u (local.get $biased)) (i64.const 52))
364
+ (local.get $mant52))))`
365
+
154
366
  export default (ctx) => {
155
367
  deps({
156
368
  __mkstr: ['__alloc'],
157
369
  __ftoa: ['__itoa', '__pow10', '__mkstr', '__static_str', '__toExp'],
370
+ __i32_to_str: ['__itoa', '__mkstr'],
158
371
  __toExp: ['__itoa', '__pow10', '__mkstr', '__static_str'],
159
372
  __radix_str: ['__mkstr'],
160
373
  __num_radix: ['__ftoa', '__mkstr'],
161
- __to_num: ['__char_at', '__str_byteLen', '__pow10', '__to_str', '__skipws', '__ptr_aux'],
374
+ __to_num: ['__char_at', '__str_byteLen', '__pow10', '__dec_to_f64', '__to_str', '__skipws', '__ptr_aux'],
162
375
  __skipws: ['__char_at', '__strws'],
163
376
  __to_bigint: ['__char_at', '__str_byteLen', '__num_to_bigint'],
164
377
  __parseInt: ['__char_at', '__str_byteLen'],
165
- __parseFloat: ['__char_at', '__str_byteLen', '__pow10', '__to_str'],
378
+ __parseFloat: ['__char_at', '__str_byteLen', '__pow10', '__dec_to_f64', '__to_str'],
166
379
  })
167
380
 
168
381
 
@@ -215,6 +428,26 @@ export default (ctx) => {
215
428
  ${reverseBytesWat()}
216
429
  (local.get $len))`
217
430
 
431
+ // __i32_to_str(val: i32) → f64 (NaN-boxed string) — ToString for a value the
432
+ // compiler proved is a signed i32. The whole point is to bypass __ftoa's float
433
+ // machinery (shortest-repr search, __toExp, __pow10): a known integer renders with
434
+ // just __itoa over its magnitude plus a sign byte. Lets `"id " + (n|0)` and integer
435
+ // templates skip the ~2 KB float formatter the generic ToString hard-pulls.
436
+ ctx.core.stdlib['__i32_to_str'] = `(func $__i32_to_str (param $val i32) (result f64)
437
+ (local $buf i32) (local $len i32) (local $u i32)
438
+ (local.set $buf (call $__alloc (i32.const 12)))
439
+ (if (i32.lt_s (local.get $val) (i32.const 0))
440
+ (then
441
+ (i32.store8 (local.get $buf) (i32.const 45)) ;; '-'
442
+ ;; magnitude as unsigned: negate via 0 - val (INT_MIN maps to itself, read u below)
443
+ (local.set $u (i32.sub (i32.const 0) (local.get $val)))
444
+ (local.set $len (call $__itoa (local.get $u) (i32.add (local.get $buf) (i32.const 1))))
445
+ (return (call $__mkstr (local.get $buf) (i32.add (local.get $len) (i32.const 1)))))
446
+ (else
447
+ (local.set $len (call $__itoa (local.get $val) (local.get $buf)))
448
+ (return (call $__mkstr (local.get $buf) (local.get $len)))))
449
+ (f64.const 0))`
450
+
218
451
  // __radix_str(val: i64, radix: i32) → f64 (NaN-boxed string)
219
452
  // Signed integer → radix string for BigInt.prototype.toString(radix). Digits go
220
453
  // 0-9 then a-z (lowercase, per spec); magnitude is taken unsigned so i64.MIN
@@ -333,10 +566,11 @@ export default (ctx) => {
333
566
  (then
334
567
  (local.set $b (i32.load8_u (i32.add (local.get $buf) (local.get $i))))
335
568
  (br_if $heap (i32.ge_u (local.get $b) (i32.const 0x80)))
336
- (local.set $packed (i32.or (local.get $packed) (i32.shl (local.get $b) (i32.shl (local.get $i) (i32.const 3)))))
569
+ ;; 7-bit ASCII SSO: char i at bit i*7 (chars fit the offset for len ≤4); len at aux bits 10-12.
570
+ (local.set $packed (i32.or (local.get $packed) (i32.shl (local.get $b) (i32.mul (local.get $i) (i32.const 7)))))
337
571
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
338
572
  (br $pk))))
339
- (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.or (i32.const ${LAYOUT.SSO_BIT}) (local.get $len)) (local.get $packed))))))
573
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.or (i32.const ${LAYOUT.SSO_BIT}) (i32.shl (local.get $len) (i32.const 10))) (local.get $packed))))))
340
574
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $len))))
341
575
  (i32.store (local.get $off) (local.get $len))
342
576
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
@@ -623,8 +857,31 @@ export default (ctx) => {
623
857
  ctx.runtime.staticDataLen = staticStr.length
624
858
  ctx.runtime.data = (ctx.runtime.data || '') + staticStr
625
859
 
860
+ // Eisel-Lemire power-of-10 table: 131 entries × 16 bytes = 2096 bytes,
861
+ // staticStr.length = 57, pad 7 → table starts at byte 64 (always constant).
862
+ // The table bytes are stashed in ctx.runtime.elTable. src/compile/index.js appends
863
+ // them to ctx.runtime.data (padded to 8-byte boundary) only when $__dec_to_f64 is
864
+ // actually pulled in, then declares global $__el_tbl = that offset. This keeps the
865
+ // 2096-byte data segment out of modules that don't do decimal→f64 conversion.
866
+ // TRIMMED to exp10 in [-65..65] (see __dec_to_f64 range check). Each entry: 8
867
+ // bytes tblLo (LE) + 8 bytes tblHi (LE), for exp10 = -65 + i.
868
+ const EL_TABLE_HEX = 'e9b4c29f1247e998eaba94ea52bbcc862462b347d798233fa5e939a527ea7fa8ad3aa0190d7fec8e0e64888eb1e49fd2ac24043068cf5319893e15f9eeeea383d72d053c42c3a85f2b8e5ab7aaea8ca44d7906cb12f49237b63131655525b0cdd00be4be8bd8bbe211bf3e5f55178e80c40e9daeaece6a5bd66e0eb72a9db1a07552445a5a8245f28b0ad2647504dec81267d5f0f0e2d6ee2e8d06be928515fb6b608596d64d46553d18c4b67b73ed9c86b8263c4ce197aa4c1e75a45ad028c4a866304b9fd93dd5df65924d710433f52940fe8e03a846e5ab7f7bd0c6e23f9933d0bd72045298de965f9a8478db8fbf40446d8f85663e967cf7c0a556d273efa84aa4791300e7ddad9a98277663a895525d0d5818c0605559c17eb1537c12bba6b4106e1ef0b8aaaf71de9d681bd7e9e870ca041396b3ca0d07ab6221712692220dfdc5977b603dd1c855bb690db0b66a507cb77d9ab88c053b2b2ac4105ce442b2ad928e60f377e3045b9a7a8ab98ed31e5937b238f0551cc6f14019ed67b288662fc5de466c6ba3372e915fe801df15a03d3b4bac2323c6e2bcba3b31618b1a080d0a5e97ecab771b6ca98a7d39ae214a908c35bde7965522c753eddcc7d9542eda7741d6507e75755c5414ea1c88e9b9d0d5d10be5ddd2927369992424aa64e8444bc64e5e958777d0c3bf2dadd43e110bef3bf15abdb44a62da973cec848ed5cdea8aadb1ec61ddfad0bd4b27a6f24a81a5ed18de67ba943945ad1eb1cfd7ce708794cfea80f4fc434b2cb3ce818d024da9798325a131fc145ef75f42a23043a01358e46e093e3b9a35f5f7d2cafc5388186e9dca8b0dca0083f2b587fd7d3455cf64a25e77487ee091b7d1749e9d812a03fe4a3695da9d5876250612c60422f583bddd833a51c5eed3ae8796f742357972966a92c4523b7544cd14be9a9382170f3c05b775278a9295009a6dc13863dd128bc62453b12cf7ba8000c9f1035ecaeb16fcf6d3ee7bda7450a01d9784f5bca61cbbf488ea1a11926408e5bce5326cd0e3e9312ba56195b67d4a1eeccf9f43622e32ff3a075d1d928eee9293c287d4fab9febe0949b4a43632aa77b8b3a9897968be2e4c5be14dc4be9495e6100af64b01379d0fd9acb03af77c1d90948cf39ec18484530fd85c0935dc24b4b96fb006f2a56528130eb44b42132ee1d3452e44b7873ff9cb88506f09ccbc8c48d73915a5698ff7feaa24cb0bffebaf1b4d885a0e4473b5bed5edbdcefee6db303095f8880a683197a5b436415f70893d7cba362b0dc2fdfcce61841177ccab4c1b69047690323dbc427ae5d594bfd60fb1c1c2499a3fa6b5696caf05bd3786531d7233dc80cf0f2384471b47acc5a7a8a44e401361c3d32b6519e25817b7d1e9263108ac1c5a643bdf4f8d976e1283a3703d0ad7a3703d0ad7a3703d0ad7a3cccccccccccccccccccccccccccccccc00000000000000000000000000000080000000000000000000000000000000a0000000000000000000000000000000c8000000000000000000000000000000fa0000000000000000000000000000409c000000000000000000000000000050c3000000000000000000000000000024f4000000000000000000000000008096980000000000000000000000000020bcbe00000000000000000000000000286bee00000000000000000000000000f9029500000000000000000000000040b743ba00000000000000000000000010a5d4e80000000000000000000000002ae78491000000000000000000000080f420e6b50000000000000000000000a031a95fe3000000000000000000000004bfc91b8e0000000000000000000000c52ebca2b10000000000000000000040763a6b0bde00000000000000000000e8890423c78a0000000000000000000062acc5eb78ad000000000000000000807a17b726d7d800000000000000000090ac6e32788687000000000000000000b4570a3f1668a9000000000000000000a1edccce1bc2d30000000000000000a0841440615159840000000000000000c8a51990b9a56fa500000000000000003a0f20f4278fcbce0000000000000040840994f878393f810000000000000050e50bb936d7078fa100000000000000a4de4e6704cdc9f2c9000000000000004d96228145407c6ffc00000000000020f09db5702ba8adc59d000000000000286c05e34c36121937c500000000000032c7c61be0c356df84f60000000000407f3c5c116c3a960b139a0000000000109f4bb31507c97bce97c00000000000d4861e20db48bb1ac2bdf00000000080441413f4880db55099769600000000a055d91731eb50e2a43f14bc0000000008abcf5dfd25e51a8e4f19eb00000000e5caa15abe37cfd0b8d1ef92000000409e3d4af1ad05030527c6abb7000000d005cd9c6d19c743c6b0b796e5000000a2230082e46f5cea7bce327e8f0000808a2c80a2dd8bf3e41a82bf5db3000020ad37200bd56e309ea1622f35e0000034cc22f4264545de02a59d3d218c0000417f2bb17096d695430e058d29af0040115f76dd0c3c4c7bd45146f0f3da00c86afb690a88a50fcd24f32b76d888007a457a040dea8e5300eeefb6930eab80d8d6984590a4726880e9aba438d2d55047867f2bdaa64741f071eb6663a38524d9675fb6909099516c4ea6403c0ca76dcf41f7e3b4f4ff6507e2cf504bcfd0a421897a0ef1f8bf9f44ed81128f81820d6a2b19522df7afc7956822d7f221a39044769fa6f8f49b39bb02eb8c6feacbb4d55347d036f202086ac325700be5fe9065942c4262d70145229a1726274f9ff57eb9b7d23a4d42d6aa809deff022c7b2dea7658789e0d28bd5e0842badebf82feb889ff455cc6377850c333b4c939bfb256bc7716bbf3cd5a6cfff491f78c27aef45394e46ef8b8a90c37f1c2716f3'
869
+ // Pre-decode the EL table bytes once and stash in ctx.runtime.
870
+ // src/compile/index.js appends elTable to ctx.runtime.data only when
871
+ // __dec_to_f64 is actually pulled in via deps (lazy, keeps small modules clean).
872
+ let elTableBytes = ''
873
+ for (let i = 0; i < EL_TABLE_HEX.length; i += 2)
874
+ elTableBytes += String.fromCharCode(parseInt(EL_TABLE_HEX.slice(i, i + 2), 16))
875
+ ctx.runtime.elTable = elTableBytes
876
+
877
+ // Register the stdlib function (no data appended here — see compile/index.js hook)
878
+ ctx.core.stdlib['__dec_to_f64'] = DEC_TO_F64_WAT
879
+
626
880
  // === Number constants ===
627
881
 
882
+ // Each folds to inline (f64.const …), no stdlib dep. Written out (not a table
883
+ // loop) to stay within the self-host subset. `NaN` uses the `nan` token (not raw
884
+ // NaN) so it survives self-host IR marshalling — see emitNum.
628
885
  ctx.core.emit['Number.MAX_SAFE_INTEGER'] = () => typed(['f64.const', 9007199254740991], 'f64')
629
886
  ctx.core.emit['Number.MIN_SAFE_INTEGER'] = () => typed(['f64.const', -9007199254740991], 'f64')
630
887
  ctx.core.emit['Number.EPSILON'] = () => typed(['f64.const', 2.220446049250313e-16], 'f64')
@@ -632,7 +889,7 @@ export default (ctx) => {
632
889
  ctx.core.emit['Number.MIN_VALUE'] = () => typed(['f64.const', 5e-324], 'f64')
633
890
  ctx.core.emit['Number.POSITIVE_INFINITY'] = () => typed(['f64.const', Infinity], 'f64')
634
891
  ctx.core.emit['Number.NEGATIVE_INFINITY'] = () => typed(['f64.const', -Infinity], 'f64')
635
- ctx.core.emit['Number.NaN'] = () => typed(['f64.const', 'nan'], 'f64') // `nan` token, not raw NaN (survives self-host IR marshalling — see emitNum)
892
+ ctx.core.emit['Number.NaN'] = () => typed(['f64.const', 'nan'], 'f64')
636
893
 
637
894
  // === Number static methods ===
638
895
 
@@ -929,9 +1186,6 @@ export default (ctx) => {
929
1186
  ;; Decimal significand. Keep 18 significant decimal digits, track the
930
1187
  ;; base-10 exponent for skipped digits, and round once before pow10 scaling.
931
1188
  ${DEC_SIGNIFICAND}
932
- ;; No digits — the literal was a bare sign or stray text ("abc", "+") → NaN.
933
- ;; (Empty / all-whitespace strings already returned +0 above.)
934
- ${FINISH_SIGNIFICAND}
935
1189
  ;; Scientific notation. 'e'/'E' commits to an ExponentPart — at least one
936
1190
  ;; digit must follow ("1e", "5e+" are NaN).
937
1191
  ${sciExponent(`(if (i32.eqz (local.get $expDigits)) (then (return (f64.const nan))))
@@ -941,8 +1195,9 @@ export default (ctx) => {
941
1195
  ;; Reject trailing non-whitespace ("5px", numeric separators "1_0", …).
942
1196
  (local.set $i (call $__skipws (local.get $v) (local.get $i) (local.get $len)))
943
1197
  (if (i32.lt_s (local.get $i) (local.get $len)) (then (return (f64.const nan))))
944
- ${POW10_SCALE}
945
- (if (result f64) (local.get $neg) (then (f64.neg (local.get $result))) (else (local.get $result))))`
1198
+ ;; Eisel-Lemire exact rounding; fallback to __pow10 for ambiguous cases.
1199
+ ${EL_SCALE}
1200
+ (local.get $result))`
946
1201
 
947
1202
  // NumberToBigInt: a RangeError unless n is an integral Number — finite and
948
1203
  // equal to its own truncation. NaN fails the f64.eq integrality test;
@@ -1068,15 +1323,15 @@ export default (ctx) => {
1068
1323
  ;; Decimal significand. Keep 18 significant decimal digits, track the
1069
1324
  ;; base-10 exponent for skipped digits, and round once before pow10 scaling.
1070
1325
  ${DEC_SIGNIFICAND}
1071
- ${FINISH_SIGNIFICAND}
1072
1326
  ;; Scientific notation.
1073
1327
  ${sciExponent(`(if (local.get $expDigits)
1074
1328
  (then
1075
1329
  (if (local.get $expNeg)
1076
1330
  (then (local.set $decExp (i32.sub (local.get $decExp) (local.get $exp))))
1077
1331
  (else (local.set $decExp (i32.add (local.get $decExp) (local.get $exp)))))))`)}
1078
- ${POW10_SCALE}
1079
- (if (result f64) (local.get $neg) (then (f64.neg (local.get $result))) (else (local.get $result))))`
1332
+ ;; Eisel-Lemire exact rounding; fallback to __pow10 for ambiguous cases.
1333
+ ${EL_SCALE}
1334
+ (local.get $result))`
1080
1335
 
1081
1336
  // ToString(arg) for the string-input builtins. A statically-known boolean must
1082
1337
  // render as "true"/"false" (spec step 1: ToString) before parsing — otherwise
package/module/object.js CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { typed, asF64, asI64, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64, block64, ptrTypeEq, dispatchByPtrType, allocPtr, needsDynShadow, mkPtrIR, extractF64Bits, appendStaticSlots, slotAddr, elemLoad, elemStore, boolBoxIR } from '../src/ir.js'
11
11
  import { emit } from '../src/bridge.js'
12
+ import { staticArrayPtr } from './array.js'
12
13
  import { valTypeOf, shapeOf } from '../src/kind.js'
13
14
  import { VAL, lookupValType, repOf, updateRep } from '../src/reps.js'
14
15
  import { ctx, err, inc, PTR, LAYOUT, declGlobal } from '../src/ctx.js'
@@ -246,6 +247,31 @@ export default (ctx) => {
246
247
  }
247
248
  ctx.core.emit['Object.getOwnPropertyNames'] = ctx.core.emit['Object.keys']
248
249
 
250
+ // for-in's read-only key enumeration (src/prepare for…in lowering). Identical to
251
+ // Object.keys EXCEPT: when the receiver is a bare variable with a complete static
252
+ // schema, the key list is a compile-time constant, so it pools ONE static-data
253
+ // array (no per-evaluation alloc) — eliminating for-in's hot-loop heap-growth
254
+ // cliff and its unbounded-allocation OOM. The pooled array is shared/read-only,
255
+ // which is sound because for-in only reads ks[i]/ks.length (Object.keys can't pool:
256
+ // user code may `.sort()`/`.reverse()` the result in place). Anything not a static
257
+ // schema bare-var — arrays/strings/HASH/dyn-props/expressions — delegates to
258
+ // Object.keys (evaluates the receiver, full runtime enumeration).
259
+ ctx.core.emit['__keys_ro'] = (obj) => {
260
+ // Pool only when the receiver's enumerable key set is provably the static
261
+ // schema: a bare var with NO computed-key writes (`o[k]=v`) — those are the
262
+ // only writes that add enumerable keys (computed reads / dot-adds don't).
263
+ // `mayHaveDynProps` is too coarse here — it also flags computed-READ receivers,
264
+ // and for-in's own `o[k]` read would otherwise veto its own pooling.
265
+ if (typeof obj === 'string' && !ctx.types.dynWriteVars?.has(obj) && !isHashTyped(obj) && !arrayValType(obj) && !stringValType(obj)) {
266
+ const schema = resolveSchema(obj)
267
+ if (schema) {
268
+ const slots = schema.map(name => extractF64Bits(asF64(emit(['str', name]))))
269
+ if (slots.every(b => b !== null)) return staticArrayPtr(slots)
270
+ }
271
+ }
272
+ return ctx.core.emit['Object.keys'](obj)
273
+ }
274
+
249
275
  // Object.prototype.hasOwnProperty(key) — own-property presence check.
250
276
  // Compile-time fold for literal keys against object literals or variables
251
277
  // with known schemas; runtime path delegates to the `in` operator (same
@@ -764,11 +790,17 @@ function emitObjectSpread(props, spreadTarget = takeLiteralTarget()) {
764
790
  else allKnown = false
765
791
  } else if (Array.isArray(p) && p[0] === ':') addName(p[1])
766
792
  }
767
- // Single unknown spread `{ ...opts }` → alias the source directly. Safe for
768
- // read-only use; mutation would affect the source (a true clone takes the
769
- // dynamic-merge path below).
770
- if (!allKnown && props.length === 1 && Array.isArray(props[0]) && props[0][0] === '...')
771
- return typed(asF64(emit(props[0][1])), 'f64')
793
+ // Single unknown spread `{ ...src }` → shallow-clone src at runtime, preserving
794
+ // its type (OBJECT→OBJECT, HASH→HASH). Aliasing src (the old shortcut) leaked
795
+ // every later write to the result back into the source — a real correctness bug
796
+ // (jz's own narrow.js had to hand-route around it). __obj_clone keys off the
797
+ // box's runtime schemaId, so it copies static-segment sources too; the schema
798
+ // table it reads must exist, so declare + force it (assemble.js).
799
+ if (!allKnown && props.length === 1 && Array.isArray(props[0]) && props[0][0] === '...') {
800
+ inc('__obj_clone')
801
+ if (!ctx.scope.globals.has('__schema_tbl')) declGlobal('__schema_tbl', 'i32')
802
+ return typed(['call', '$__obj_clone', asF64(emit(props[0][1]))], 'f64')
803
+ }
772
804
  if (!allKnown) return emitDynamicSpread(props)
773
805
 
774
806
  const schemaId = ctx.schema.register(allNames)