jz 0.5.0 → 0.6.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 (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +281 -142
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +461 -185
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +591 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +600 -205
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -2997
  77. package/src/jzify.js +0 -1553
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /package/src/{codegen.js → wat/codegen.js} +0 -0
package/module/number.js CHANGED
@@ -9,10 +9,13 @@
9
9
  * @module number
10
10
  */
11
11
 
12
- import { typed, asF64, asI32, asI64, toI32, toNumF64, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64 } from '../src/ir.js'
13
- import { emit, emitBoolStr } from '../src/emit.js'
14
- import { isReassigned, valTypeOf, VAL } from '../src/analyze.js'
15
- import { inc, PTR } from '../src/ctx.js'
12
+ import { typed, asF64, asI32, asI64, toI32, toNumF64, NULL_NAN, UNDEF_NAN, FALSE_NAN, TRUE_NAN, temp, tempI32, tempI64, ptrTypeEq, truthyIR } from '../src/ir.js'
13
+ import { ssoBitI64Hex } from '../layout.js'
14
+ import { emit, bool, deps, reg, hostImport } from '../src/bridge.js'
15
+ import { isReassigned } from '../src/ast.js'
16
+ import { valTypeOf } from '../src/kind.js'
17
+ import { VAL } from '../src/reps.js'
18
+ import { inc, PTR, LAYOUT } from '../src/ctx.js'
16
19
 
17
20
  // ─── Shared decimal-number parsing fragments ────────────────────────────────
18
21
  // `__to_num` (Number coercion) and `__parseFloat` both scan a StrDecimalLiteral
@@ -34,9 +37,23 @@ import { inc, PTR } from '../src/ctx.js'
34
37
  // Callers MUST declare `$sbase i32`, set it once after `$v` is final and a
35
38
  // confirmed string, and only pass indices proven `< $len` (`__char_at` would
36
39
  // otherwise return its OOB 0; the inline load has no such guard).
40
+ const SSO_BIT_I64 = ssoBitI64Hex()
37
41
  const SBASE_INIT = '(local.set $sbase (i32.wrap_i64 (i64.and (local.get $v) (i64.const 4294967295))))'
42
+
43
+ /** In-place byte reversal of buf[i..j] (WAT fragment). __itoa/__radix_str emit the
44
+ * least-significant digit first, then flip the run. Caller pre-sets `j` to the last
45
+ * index and leaves `i` at 0; `tmp` is scratch. Labels $rev/$revl are block-local. */
46
+ const reverseBytesWat = (buf = '$buf', i = '$i', j = '$j', tmp = '$tmp') =>
47
+ `(block $rev (loop $revl
48
+ (br_if $rev (i32.ge_s (local.get ${i}) (local.get ${j})))
49
+ (local.set ${tmp} (i32.load8_u (i32.add (local.get ${buf}) (local.get ${i}))))
50
+ (i32.store8 (i32.add (local.get ${buf}) (local.get ${i})) (i32.load8_u (i32.add (local.get ${buf}) (local.get ${j}))))
51
+ (i32.store8 (i32.add (local.get ${buf}) (local.get ${j})) (local.get ${tmp}))
52
+ (local.set ${i} (i32.add (local.get ${i}) (i32.const 1)))
53
+ (local.set ${j} (i32.sub (local.get ${j}) (i32.const 1)))
54
+ (br $revl)))`
38
55
  const chAt = idx => `(if (result i32)
39
- (i64.eqz (i64.and (local.get $v) (i64.const 0x0000400000000000)))
56
+ (i64.eqz (i64.and (local.get $v) (i64.const ${SSO_BIT_I64})))
40
57
  (then (i32.load8_u (i32.add (local.get $sbase) ${idx})))
41
58
  (else (call $__char_at (local.get $v) ${idx})))`
42
59
 
@@ -135,10 +152,12 @@ const POW10_SCALE = `
135
152
  (then (local.set $result (f64.div (local.get $result) (call $__pow10 (i32.sub (i32.const 0) (local.get $decExp)))))))`
136
153
 
137
154
  export default (ctx) => {
138
- Object.assign(ctx.core.stdlibDeps, {
155
+ deps({
139
156
  __mkstr: ['__alloc'],
140
157
  __ftoa: ['__itoa', '__pow10', '__mkstr', '__static_str', '__toExp'],
141
158
  __toExp: ['__itoa', '__pow10', '__mkstr', '__static_str'],
159
+ __radix_str: ['__mkstr'],
160
+ __num_radix: ['__ftoa', '__mkstr'],
142
161
  __to_num: ['__char_at', '__str_byteLen', '__pow10', '__to_str', '__skipws', '__ptr_aux'],
143
162
  __skipws: ['__char_at', '__strws'],
144
163
  __to_bigint: ['__char_at', '__str_byteLen', '__num_to_bigint'],
@@ -193,22 +212,131 @@ export default (ctx) => {
193
212
  (br $l)))
194
213
  ;; Reverse
195
214
  (local.set $j (i32.sub (local.get $len) (i32.const 1)))
196
- (block $rd (loop $rl
197
- (br_if $rd (i32.ge_s (local.get $i) (local.get $j)))
215
+ ${reverseBytesWat()}
216
+ (local.get $len))`
217
+
218
+ // __radix_str(val: i64, radix: i32) → f64 (NaN-boxed string)
219
+ // Signed integer → radix string for BigInt.prototype.toString(radix). Digits go
220
+ // 0-9 then a-z (lowercase, per spec); magnitude is taken unsigned so i64.MIN
221
+ // (whose two's-complement negation is itself) formats correctly via div_u/rem_u.
222
+ ctx.core.stdlib['__radix_str'] = `(func $__radix_str (param $val i64) (param $radix i32) (result f64)
223
+ (local $buf i32) (local $pos i32) (local $neg i32) (local $mag i64) (local $r i64)
224
+ (local $dg i32) (local $i i32) (local $j i32) (local $tmp i32)
225
+ (local.set $buf (call $__alloc (i32.const 72)))
226
+ (local.set $r (i64.extend_i32_s (local.get $radix)))
227
+ (if (i64.eqz (local.get $val))
228
+ (then (i32.store8 (local.get $buf) (i32.const 48)) (return (call $__mkstr (local.get $buf) (i32.const 1)))))
229
+ (local.set $mag (local.get $val))
230
+ (if (i64.lt_s (local.get $val) (i64.const 0))
231
+ (then (local.set $neg (i32.const 1)) (local.set $mag (i64.sub (i64.const 0) (local.get $val)))))
232
+ (block $mb (loop $ml
233
+ (br_if $mb (i64.eqz (local.get $mag)))
234
+ (local.set $dg (i32.wrap_i64 (i64.rem_u (local.get $mag) (local.get $r))))
235
+ (i32.store8 (i32.add (local.get $buf) (local.get $pos))
236
+ (select (i32.add (local.get $dg) (i32.const 48)) (i32.add (local.get $dg) (i32.const 87)) (i32.lt_s (local.get $dg) (i32.const 10))))
237
+ (local.set $mag (i64.div_u (local.get $mag) (local.get $r)))
238
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
239
+ (br $ml)))
240
+ (if (local.get $neg)
241
+ (then (i32.store8 (i32.add (local.get $buf) (local.get $pos)) (i32.const 45))
242
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))))
243
+ (local.set $j (i32.sub (local.get $pos) (i32.const 1)))
244
+ ${reverseBytesWat()}
245
+ (call $__mkstr (local.get $buf) (local.get $pos)))`
246
+
247
+ // __num_radix(val: f64, radix: i32) → f64 (NaN-boxed string)
248
+ // Number.prototype.toString(radix) for radix != 10. Non-finite values defer to
249
+ // __ftoa ("NaN"/"Infinity"/"-Infinity"). Integer part uses exact i64 division
250
+ // (magnitude bounded by jz's i64 BigInt domain); the fraction multiplies out in
251
+ // f64, capped at 100 digits (radix fractions are implementation-defined precision).
252
+ //
253
+ // Per 21.1.3.6: radix must be an integer in [2, 36] — otherwise RangeError.
254
+ // The check sits at the canonical entry point so the const-radix fold path
255
+ // (which still calls here for any non-10 constant) and the dynamic-radix
256
+ // branch are validated uniformly. `(throw $__jz_err …)` is picked up by
257
+ // ensureThrowRuntime via stdlib scan, so callers do not need to flag throws.
258
+ ctx.core.stdlib['__num_radix'] = `(func $__num_radix (param $val f64) (param $radix i32) (result f64)
259
+ (local $buf i32) (local $pos i32) (local $neg i32) (local $iv i64) (local $r i64) (local $rf f64)
260
+ (local $int f64) (local $frac f64) (local $dg i32) (local $i i32) (local $j i32) (local $tmp i32) (local $fn i32) (local $rv f64)
261
+ (if (i32.or (i32.lt_s (local.get $radix) (i32.const 2)) (i32.gt_s (local.get $radix) (i32.const 36)))
262
+ (then (throw $__jz_err (f64.const 0))))
263
+ (if (i32.or (f64.ne (local.get $val) (local.get $val)) (f64.eq (f64.abs (local.get $val)) (f64.const inf)))
264
+ (then (return (call $__ftoa (local.get $val) (i32.const 0) (i32.const 0)))))
265
+ (local.set $buf (call $__alloc (i32.const 180)))
266
+ (local.set $r (i64.extend_i32_s (local.get $radix)))
267
+ (local.set $rf (f64.convert_i32_s (local.get $radix)))
268
+ (if (f64.lt (local.get $val) (f64.const 0))
269
+ (then (local.set $neg (i32.const 1)) (local.set $val (f64.neg (local.get $val)))))
270
+ (local.set $int (f64.floor (local.get $val)))
271
+ (local.set $frac (f64.sub (local.get $val) (local.get $int)))
272
+ (local.set $iv (i64.trunc_sat_f64_u (local.get $int)))
273
+ (if (i64.eqz (local.get $iv))
274
+ (then (i32.store8 (local.get $buf) (i32.const 48)) (local.set $pos (i32.const 1)))
275
+ (else
276
+ (block $ib (loop $il
277
+ (br_if $ib (i64.eqz (local.get $iv)))
278
+ (local.set $dg (i32.wrap_i64 (i64.rem_u (local.get $iv) (local.get $r))))
279
+ (i32.store8 (i32.add (local.get $buf) (local.get $pos))
280
+ (select (i32.add (local.get $dg) (i32.const 48)) (i32.add (local.get $dg) (i32.const 87)) (i32.lt_s (local.get $dg) (i32.const 10))))
281
+ (local.set $iv (i64.div_u (local.get $iv) (local.get $r)))
282
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
283
+ (br $il)))))
284
+ (if (local.get $neg)
285
+ (then (i32.store8 (i32.add (local.get $buf) (local.get $pos)) (i32.const 45))
286
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))))
287
+ (local.set $j (i32.sub (local.get $pos) (i32.const 1)))
288
+ (block $rb (loop $rl
289
+ (br_if $rb (i32.ge_s (local.get $i) (local.get $j)))
198
290
  (local.set $tmp (i32.load8_u (i32.add (local.get $buf) (local.get $i))))
199
- (i32.store8 (i32.add (local.get $buf) (local.get $i))
200
- (i32.load8_u (i32.add (local.get $buf) (local.get $j))))
291
+ (i32.store8 (i32.add (local.get $buf) (local.get $i)) (i32.load8_u (i32.add (local.get $buf) (local.get $j))))
201
292
  (i32.store8 (i32.add (local.get $buf) (local.get $j)) (local.get $tmp))
202
293
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
203
294
  (local.set $j (i32.sub (local.get $j) (i32.const 1)))
204
295
  (br $rl)))
205
- (local.get $len))`
296
+ (if (f64.gt (local.get $frac) (f64.const 0))
297
+ (then
298
+ (i32.store8 (i32.add (local.get $buf) (local.get $pos)) (i32.const 46))
299
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
300
+ (block $fb (loop $fl
301
+ (br_if $fb (f64.le (local.get $frac) (f64.const 0)))
302
+ (br_if $fb (i32.ge_s (local.get $fn) (i32.const 100)))
303
+ (local.set $frac (f64.mul (local.get $frac) (local.get $rf)))
304
+ (local.set $dg (i32.trunc_f64_s (f64.floor (local.get $frac))))
305
+ (i32.store8 (i32.add (local.get $buf) (local.get $pos))
306
+ (select (i32.add (local.get $dg) (i32.const 48)) (i32.add (local.get $dg) (i32.const 87)) (i32.lt_s (local.get $dg) (i32.const 10))))
307
+ (local.set $frac (f64.sub (local.get $frac) (f64.floor (local.get $frac))))
308
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
309
+ (local.set $fn (i32.add (local.get $fn) (i32.const 1)))
310
+ (br $fl)))))
311
+ ${!ctx.memory.shared ? `
312
+ ;; When __mkstr packed an SSO result it allocated nothing, so the digit scratch
313
+ ;; ($buf, at heap top) is dead — reclaim it so a heap-top accumulator stays on
314
+ ;; top and \`s += n.toString(r)\` bump-extends instead of reallocating (O(n)).
315
+ (local.set $rv (call $__mkstr (local.get $buf) (local.get $pos)))
316
+ (if (i32.and (i32.wrap_i64 (i64.shr_u (i64.reinterpret_f64 (local.get $rv)) (i64.const ${LAYOUT.AUX_SHIFT}))) (i32.const ${LAYOUT.SSO_BIT}))
317
+ (then (global.set $__heap (local.get $buf))))
318
+ (local.get $rv)` : `(call $__mkstr (local.get $buf) (local.get $pos))`})`
206
319
 
207
320
  // __mkstr(buf: i32, len: i32) → f64 — copy scratch buffer to heap string.
208
321
  // Hot (~60M calls in watr self-host via __ftoa). bulk memory.copy is ~10× faster than
209
322
  // a hand-rolled byte loop (wasm2c lowers it to memcpy under PGO+LTO).
210
323
  ctx.core.stdlib['__mkstr'] = `(func $__mkstr (param $buf i32) (param $len i32) (result f64)
211
- (local $off i32)
324
+ (local $off i32) (local $i i32) (local $packed i32) (local $b i32)
325
+ ;; SSO fast path: ≤4 ASCII bytes pack into the pointer with no allocation, so a
326
+ ;; number-format result doesn't displace a heap-top accumulator — keeping the
327
+ ;; canonical \`s += n.toString(r)\` builder O(n) via the bump-extend path.
328
+ (if (i32.le_u (local.get $len) (i32.const 4))
329
+ (then
330
+ (block $heap
331
+ (loop $pk
332
+ (if (i32.lt_u (local.get $i) (local.get $len))
333
+ (then
334
+ (local.set $b (i32.load8_u (i32.add (local.get $buf) (local.get $i))))
335
+ (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)))))
337
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
338
+ (br $pk))))
339
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.or (i32.const ${LAYOUT.SSO_BIT}) (local.get $len)) (local.get $packed))))))
212
340
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $len))))
213
341
  (i32.store (local.get $off) (local.get $len))
214
342
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
@@ -251,7 +379,12 @@ export default (ctx) => {
251
379
  ;; Default mode: auto-select precision (up to 9 digits, must fit i32 when scaled)
252
380
  (if (i32.eqz (local.get $mode))
253
381
  (then (local.set $prec (i32.const 9))))
254
- ;; Round and scale to integer: scaled = nearest(val * 10^prec)
382
+ ;; Round and scale to integer: scaled = nearest(val * 10^prec).
383
+ ;; NOTE: toFixed/toPrecision round ties-to-even here (f64.nearest), which differs from
384
+ ;; JS's round-half-away-from-zero on exact halves like (2.5).toFixed(0) → '2' vs '3'.
385
+ ;; A naive floor(x+0.5) "fixes" those but breaks values like 1.45 (whose ×10 rounds up
386
+ ;; to 14.5 in f64, giving '1.5' vs JS '1.4'); bit-exact toFixed needs the exact-decimal
387
+ ;; algorithm. Documented as a known difference rather than trading one error for another.
255
388
  (local.set $scale (call $__pow10 (local.get $prec)))
256
389
  (local.set $scaled (f64.nearest (f64.mul (local.get $val) (local.get $scale))))
257
390
  ;; If scaled doesn't fit i32, reduce precision until it does (min prec=0)
@@ -267,19 +400,40 @@ export default (ctx) => {
267
400
  (then
268
401
  (local.set $int (i32.trunc_f64_u (f64.div (local.get $scaled) (local.get $scale))))
269
402
  (local.set $frac (i32.trunc_f64_u (f64.sub (local.get $scaled)
270
- (f64.mul (f64.convert_i32_u (local.get $int)) (local.get $scale))))))
403
+ (f64.mul (f64.convert_i32_u (local.get $int)) (local.get $scale)))))
404
+ ;; Default mode, fit loop reduced prec to 0: the rounded integer is ready, but the
405
+ ;; original val may still have a fractional part that was discarded. Recover it:
406
+ ;; frac_f = val - trunc(val); since frac_f ∈ [0,1), frac_f*10^9 < 10^9 < 2^31 — safe.
407
+ (if (i32.and (i32.eqz (local.get $mode)) (i32.eqz (local.get $prec)))
408
+ (then
409
+ (local.set $abs (f64.sub (local.get $val) (f64.trunc (local.get $val))))
410
+ (if (f64.gt (local.get $abs) (f64.const 0))
411
+ (then
412
+ ;; $int was taken from f64.nearest(val), which rounds .5+ UP (999999999.9 → 1e9),
413
+ ;; but $abs/$frac below derive from f64.trunc(val). Re-derive $int from the same
414
+ ;; trunc so integer and fraction agree — else String(999999999.9) → "1000000000.9".
415
+ (local.set $int (i32.trunc_f64_u (f64.trunc (local.get $val))))
416
+ (local.set $prec (i32.const 9))
417
+ (local.set $scale (call $__pow10 (i32.const 9)))
418
+ ;; round: trunc_u(x+0.5) == floor(x+0.5) for the positive frac scale
419
+ (local.set $frac (i32.trunc_f64_u (f64.add
420
+ (f64.mul (local.get $abs) (f64.const 1000000000))
421
+ (f64.const 0.5)))))))))
271
422
  (else
272
423
  (local.set $int (i32.const 0))
273
424
  (local.set $frac (i32.const 0))
274
425
  (local.set $prec (i32.const 0))
275
426
  (local.set $abs (f64.trunc (local.get $val)))
276
- ;; Write large integer digits reversed
427
+ ;; Write large integer digits reversed.
428
+ ;; Clamp digit to [0,9]: f64 precision loss for large values can make the naive
429
+ ;; subtraction (abs - trunc(abs/10)*10) go slightly negative → i32.trunc_f64_u trap.
277
430
  (local.set $ilen (local.get $pos))
278
431
  (block $ld (loop $ll
279
432
  (br_if $ld (f64.lt (local.get $abs) (f64.const 1)))
280
433
  (i32.store8 (i32.add (local.get $buf) (local.get $pos))
281
- (i32.add (i32.const 48) (i32.trunc_f64_u (f64.sub (local.get $abs)
282
- (f64.mul (f64.trunc (f64.div (local.get $abs) (f64.const 10))) (f64.const 10))))))
434
+ (i32.add (i32.const 48) (i32.trunc_f64_u (f64.max (f64.const 0) (f64.min (f64.const 9)
435
+ (f64.nearest (f64.sub (local.get $abs)
436
+ (f64.mul (f64.trunc (f64.div (local.get $abs) (f64.const 10))) (f64.const 10)))))))))
283
437
  (local.set $abs (f64.trunc (f64.div (local.get $abs) (f64.const 10))))
284
438
  (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
285
439
  (br $ll)))
@@ -294,6 +448,43 @@ export default (ctx) => {
294
448
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
295
449
  (local.set $j (i32.sub (local.get $j) (i32.const 1)))
296
450
  (br $rl)))
451
+ ;; Default mode: emit fractional part if val has one (large-int path skipped it before).
452
+ ;; frac_f = val - trunc(val); since frac_f ∈ [0,1), frac_f*10^9 < 10^9 < 2^31 — safe.
453
+ (if (i32.eqz (local.get $mode))
454
+ (then
455
+ (local.set $abs (f64.sub (local.get $val) (f64.trunc (local.get $val))))
456
+ (if (f64.gt (local.get $abs) (f64.const 0))
457
+ (then
458
+ ;; round: trunc_u(x+0.5) == floor(x+0.5) for the positive frac scale
459
+ (local.set $frac (i32.trunc_f64_u (f64.add
460
+ (f64.mul (local.get $abs) (f64.const 1000000000))
461
+ (f64.const 0.5))))
462
+ (i32.store8 (i32.add (local.get $buf) (local.get $pos)) (i32.const 46))
463
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
464
+ ;; 9 fractional digits from $frac, high-to-low
465
+ (local.set $i (i32.const 8))
466
+ (block $fd2 (loop $fl2
467
+ (br_if $fd2 (i32.lt_s (local.get $i) (i32.const 0)))
468
+ (local.set $j (i32.div_u (local.get $frac) (i32.trunc_f64_u (call $__pow10 (local.get $i)))))
469
+ (i32.store8 (i32.add (local.get $buf) (local.get $pos))
470
+ (i32.add (i32.const 48) (i32.rem_u (local.get $j) (i32.const 10))))
471
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
472
+ (local.set $i (i32.sub (local.get $i) (i32.const 1)))
473
+ (br $fl2)))
474
+ ;; Strip trailing zeros
475
+ (block $sz2 (loop $sl2
476
+ (br_if $sz2 (i32.le_s (local.get $pos) (i32.const 0)))
477
+ (br_if $sz2 (i32.ne
478
+ (i32.load8_u (i32.add (local.get $buf) (i32.sub (local.get $pos) (i32.const 1))))
479
+ (i32.const 48)))
480
+ (local.set $pos (i32.sub (local.get $pos) (i32.const 1)))
481
+ (br $sl2)))
482
+ ;; Strip trailing dot
483
+ (if (i32.and (i32.gt_s (local.get $pos) (i32.const 0))
484
+ (i32.eq
485
+ (i32.load8_u (i32.add (local.get $buf) (i32.sub (local.get $pos) (i32.const 1))))
486
+ (i32.const 46)))
487
+ (then (local.set $pos (i32.sub (local.get $pos) (i32.const 1)))))))))
297
488
  (return (call $__mkstr (local.get $buf) (local.get $pos)))))
298
489
  ;; Write integer part
299
490
  (local.set $ilen (call $__itoa (local.get $int) (i32.add (local.get $buf) (local.get $pos))))
@@ -365,7 +556,7 @@ export default (ctx) => {
365
556
  (local.set $val (f64.mul (local.get $val) (f64.const 10)))
366
557
  (local.set $exp (i32.sub (local.get $exp) (i32.const 1)))
367
558
  (br $l2)))))
368
- ;; Scale to integer mantissa: nearest(val * 10^prec)
559
+ ;; Scale to integer mantissa: nearest(val * 10^prec). Ties-to-even (see __ftoa note).
369
560
  (local.set $scale (call $__pow10 (local.get $prec)))
370
561
  (local.set $mantissa (f64.nearest (f64.mul (local.get $val) (local.get $scale))))
371
562
  ;; Rounding overflow (e.g. 9.95 → 1000 when prec=1, scale=10)
@@ -441,7 +632,7 @@ export default (ctx) => {
441
632
  ctx.core.emit['Number.MIN_VALUE'] = () => typed(['f64.const', 5e-324], 'f64')
442
633
  ctx.core.emit['Number.POSITIVE_INFINITY'] = () => typed(['f64.const', Infinity], 'f64')
443
634
  ctx.core.emit['Number.NEGATIVE_INFINITY'] = () => typed(['f64.const', -Infinity], 'f64')
444
- ctx.core.emit['Number.NaN'] = () => typed(['f64.const', NaN], 'f64')
635
+ ctx.core.emit['Number.NaN'] = () => typed(['f64.const', 'nan'], 'f64') // `nan` token, not raw NaN (survives self-host IR marshalling — see emitNum)
445
636
 
446
637
  // === Number static methods ===
447
638
 
@@ -501,7 +692,8 @@ export default (ctx) => {
501
692
 
502
693
  ctx.core.stdlib['__parseInt'] = `(func $__parseInt (param $str i64) (param $radix i32) (result f64)
503
694
  (local $off i32) (local $len i32) (local $i i32) (local $c i32) (local $neg i32)
504
- (local $result f64) (local $digit i32) (local $seen i32) (local $f f64)
695
+ (local $digit i32) (local $seen i32) (local $f f64)
696
+ (local $acc i64) (local $rad i64) (local $ovf i32) (local $exp i32) (local $sticky i32) (local $k i32) (local $e i32)
505
697
  (local.set $f (f64.reinterpret_i64 (local.get $str)))
506
698
  ;; If input is a number, just truncate
507
699
  (if (f64.eq (local.get $f) (local.get $f)) (then (return (f64.trunc (local.get $f)))))
@@ -524,16 +716,21 @@ export default (ctx) => {
524
716
  (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
525
717
  (i32.eq (call $__char_at (local.get $str) (local.get $i)) (i32.const 43)))
526
718
  (then (local.set $i (i32.add (local.get $i) (i32.const 1)))))
527
- ;; 0x prefix → radix 16
528
- (if (i32.and (i32.eqz (local.get $radix))
719
+ ;; 0x prefix → radix 16 (stripped when radix is unspecified OR explicitly 16, per JS)
720
+ (if (i32.and (i32.or (i32.eqz (local.get $radix)) (i32.eq (local.get $radix) (i32.const 16)))
529
721
  (i32.and (i32.le_s (i32.add (local.get $i) (i32.const 1)) (local.get $len))
530
722
  (i32.and (i32.eq (call $__char_at (local.get $str) (local.get $i)) (i32.const 48))
531
723
  (i32.or (i32.eq (call $__char_at (local.get $str) (i32.add (local.get $i) (i32.const 1))) (i32.const 120))
532
724
  (i32.eq (call $__char_at (local.get $str) (i32.add (local.get $i) (i32.const 1))) (i32.const 88))))))
533
725
  (then (local.set $radix (i32.const 16)) (local.set $i (i32.add (local.get $i) (i32.const 2)))))
534
726
  (if (i32.eqz (local.get $radix)) (then (local.set $radix (i32.const 10))))
535
- ;; Parse digits
536
- (local.set $result (f64.const 0))
727
+ ;; Power-of-two radix → exact bit width per digit (lets the >2^64 path round once).
728
+ (if (i32.eqz (i32.and (local.get $radix) (i32.sub (local.get $radix) (i32.const 1))))
729
+ (then (local.set $k (i32.ctz (local.get $radix)))))
730
+ (local.set $rad (i64.extend_i32_u (local.get $radix)))
731
+ ;; Parse digits — accumulate EXACTLY in u64 (round-once via convert at the end, matching JS for
732
+ ;; any value < 2^64). On u64 overflow, freeze the high bits and track the dropped magnitude
733
+ ;; (exp) + a sticky bit so the final round-to-f64 still matches round-once for power-of-two radix.
537
734
  (block $done (loop $lp
538
735
  (br_if $done (i32.ge_s (local.get $i) (local.get $len)))
539
736
  (local.set $c (call $__char_at (local.get $str) (local.get $i)))
@@ -547,12 +744,41 @@ export default (ctx) => {
547
744
  (then (local.set $digit (i32.sub (local.get $c) (i32.const 55)))))
548
745
  (br_if $done (i32.or (i32.lt_s (local.get $digit) (i32.const 0)) (i32.ge_s (local.get $digit) (local.get $radix))))
549
746
  (local.set $seen (i32.const 1))
550
- (local.set $result (f64.add (f64.mul (local.get $result) (f64.convert_i32_s (local.get $radix))) (f64.convert_i32_s (local.get $digit))))
747
+ (if (i32.eqz (local.get $ovf))
748
+ (then
749
+ ;; acc*radix + digit, exact while it stays within unsigned 64-bit
750
+ (if (i64.le_u (local.get $acc) (i64.div_u (i64.sub (i64.const -1) (i64.extend_i32_u (local.get $digit))) (local.get $rad)))
751
+ (then (local.set $acc (i64.add (i64.mul (local.get $acc) (local.get $rad)) (i64.extend_i32_u (local.get $digit)))))
752
+ (else
753
+ (local.set $ovf (i32.const 1))
754
+ ;; non-power-of-two radix: continue round-each in f64 from the exact seed (keeps magnitude)
755
+ (if (i32.eqz (local.get $k)) (then (local.set $f (f64.convert_i64_u (local.get $acc)))))))))
756
+ (if (local.get $ovf)
757
+ (then
758
+ (if (local.get $k)
759
+ (then ;; power-of-two: this digit and all later ones sit below the frozen high bits
760
+ (local.set $exp (i32.add (local.get $exp) (local.get $k)))
761
+ (if (local.get $digit) (then (local.set $sticky (i32.const 1)))))
762
+ (else ;; other radix: round-each f64 — ≤1 ULP past 2^53, but only for >2^64 values
763
+ (local.set $f (f64.add (f64.mul (local.get $f) (f64.convert_i32_u (local.get $radix))) (f64.convert_i32_u (local.get $digit))))))))
551
764
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
552
765
  (br $lp)))
553
766
  ;; No digits consumed → NaN
554
767
  (if (i32.eqz (local.get $seen)) (then (return (f64.const nan))))
555
- (if (result f64) (local.get $neg) (then (f64.neg (local.get $result))) (else (local.get $result))))`
768
+ ;; Produce the f64. No overflow exact round-once convert. Overflow+power-of-two round the
769
+ ;; frozen high bits (sticky-injected to break ties correctly) and scale by 2^exp. Overflow on
770
+ ;; another radix → $f already holds the round-each result.
771
+ (if (i32.eqz (local.get $ovf))
772
+ (then (local.set $f (f64.convert_i64_u (local.get $acc))))
773
+ (else (if (local.get $k)
774
+ (then
775
+ (if (local.get $sticky) (then (local.set $acc (i64.or (local.get $acc) (i64.const 1)))))
776
+ (local.set $f (f64.convert_i64_u (local.get $acc)))
777
+ (local.set $e (i32.add (local.get $exp) (i32.const 1023)))
778
+ (if (i32.ge_s (local.get $e) (i32.const 2047))
779
+ (then (local.set $f (f64.const inf)))
780
+ (else (local.set $f (f64.mul (local.get $f) (f64.reinterpret_i64 (i64.shl (i64.extend_i32_u (local.get $e)) (i64.const 52)))))))))))
781
+ (if (result f64) (local.get $neg) (then (f64.neg (local.get $f))) (else (local.get $f))))`
556
782
 
557
783
  // __strws(c: i32) → i32 — ECMA StrWhiteSpace predicate. Covers TAB..CR, SP,
558
784
  // NBSP, BOM, LS/PS, and every Unicode Space_Separator. U+180E is *not*
@@ -613,6 +839,8 @@ export default (ctx) => {
613
839
  (if (f64.eq (local.get $f) (local.get $f)) (then (return (local.get $f))))
614
840
  (if (i64.eq (local.get $v) (i64.const ${NULL_NAN})) (then (return (f64.const 0))))
615
841
  (if (i64.eq (local.get $v) (i64.const ${UNDEF_NAN})) (then (return (f64.const nan))))
842
+ (if (i64.eq (local.get $v) (i64.const ${FALSE_NAN})) (then (return (f64.const 0))))
843
+ (if (i64.eq (local.get $v) (i64.const ${TRUE_NAN})) (then (return (f64.const 1))))
616
844
  (local.set $t (call $__ptr_type (local.get $v)))
617
845
  ;; ToNumber(Symbol) is a TypeError. A Symbol is an ATOM (type 0) with a user
618
846
  ;; atom-id (>= 16); null/undefined returned above, and a bare NaN carries
@@ -854,23 +1082,22 @@ export default (ctx) => {
854
1082
  // render as "true"/"false" (spec step 1: ToString) before parsing — otherwise
855
1083
  // its 0/1 carrier bits are fed to the parser as if a string pointer. Other types
856
1084
  // (string already, number/object ToPrimitive) stay out of scope per the runner.
857
- const strInputI64 = (x) => valTypeOf(x) === VAL.BOOL ? asI64(emitBoolStr(x)) : asI64(emit(x))
1085
+ const strInputI64 = (x) => valTypeOf(x) === VAL.BOOL ? asI64(bool(x)) : asI64(emit(x))
858
1086
 
859
1087
  ctx.core.emit['Number.parseInt'] = (x, radix) => {
1088
+ if (ctx.transform.host === 'wasi') {
1089
+ inc('__parseInt')
1090
+ const radixIR = radix == null ? ['i32.const', 0] : toI32(toNumF64(radix, emit(radix)))
1091
+ return typed(['call', '$__parseInt', strInputI64(x), radixIR], 'f64')
1092
+ }
860
1093
  needParseInt()
861
- // Radix is coerced ToNumber then ToInt32 (modular wrap; NaN/±∞→0) per spec —
862
- // a raw trunc would turn a string radix into garbage and Infinity into maxint.
863
1094
  const radixIR = radix == null ? ['i32.const', 0] : toI32(toNumF64(radix, emit(radix)))
864
1095
  return typed(['call', '$__parseInt', strInputI64(x), radixIR], 'f64')
865
1096
  }
866
1097
  ctx.core.emit['parseInt'] = ctx.core.emit['Number.parseInt']
867
- const addImportOnce = (ctx, mod, name, fn) => {
868
- if (ctx.module.imports.some(i => i[1] === `"${mod}"` && i[2] === `"${name}"`)) return
869
- ctx.module.imports.push(['import', `"${mod}"`, `"${name}"`, fn])
870
- }
871
- const needParseFloat = () => addImportOnce(ctx, 'env', 'parseFloat',
1098
+ const needParseFloat = () => hostImport('env', 'parseFloat',
872
1099
  ['func', '$__parseFloat', ['param', 'i64'], ['result', 'f64']])
873
- const needParseInt = () => addImportOnce(ctx, 'env', 'parseInt',
1100
+ const needParseInt = () => hostImport('env', 'parseInt',
874
1101
  ['func', '$__parseInt', ['param', 'i64'], ['param', 'i32'], ['result', 'f64']])
875
1102
 
876
1103
  ctx.core.emit['Number.parseFloat'] = (x) => {
@@ -884,32 +1111,44 @@ export default (ctx) => {
884
1111
  ctx.core.emit['parseFloat'] = ctx.core.emit['Number.parseFloat']
885
1112
 
886
1113
  // Boolean(x) → truthiness (non-zero → 1, zero → 0)
887
- ctx.core.emit['Boolean'] = (x) => {
1114
+ reg('Boolean', ['__is_truthy'], (x) => {
888
1115
  if (x === undefined) return typed(['f64.const', 0], 'f64')
889
- inc('__is_truthy')
890
- const v = asI64(emit(x))
891
- return typed(['if', ['result', 'f64'], ['call', '$__is_truthy', v], ['then', ['f64.const', 1]], ['else', ['f64.const', 0]]], 'f64')
892
- }
1116
+ // Via truthyIR so a NUMBER arg gets the NaN-safe f64 test (Boolean(0/0) === false
1117
+ // on every platform); other types fall to __is_truthy inside truthyIR.
1118
+ return typed(['f64.convert_i32_s', truthyIR(emit(x))], 'f64')
1119
+ })
893
1120
 
894
1121
  // === Instance method emitters ===
895
1122
 
896
- ctx.core.emit['.number:toString'] = (n) => {
897
- inc('__ftoa')
898
- return typed(['call', '$__ftoa', asF64(emit(n)), ['i32.const', 0], ['i32.const', 0]], 'f64')
899
- }
1123
+ const ftoaDefault = (v) => typed(['call', '$__ftoa', v, ['i32.const', 0], ['i32.const', 0]], 'f64')
1124
+ reg('.number:toString', ['__ftoa', '__num_radix'], (n, radix) => {
1125
+ const v = asF64(emit(n))
1126
+ if (radix == null) return ftoaDefault(v)
1127
+ const rv = emit(radix)
1128
+ // Constant radix folds the 10-vs-other choice at compile time; radix 10 keeps
1129
+ // __ftoa's shortest-repr (the radix loop would emit float-noise tail digits).
1130
+ if (Array.isArray(rv) && rv[0] === 'f64.const' && typeof rv[1] === 'number')
1131
+ return rv[1] === 10 ? ftoaDefault(v) : typed(['call', '$__num_radix', v, ['i32.const', rv[1] | 0]], 'f64')
1132
+ const vt = temp('rv'), rt = tempI32('rr')
1133
+ return typed(['block', ['result', 'f64'],
1134
+ ['local.set', `$${vt}`, v],
1135
+ ['local.set', `$${rt}`, asI32(rv)],
1136
+ ['if', ['result', 'f64'], ['i32.eq', ['local.get', `$${rt}`], ['i32.const', 10]],
1137
+ ['then', ftoaDefault(['local.get', `$${vt}`])],
1138
+ ['else', ['call', '$__num_radix', ['local.get', `$${vt}`], ['local.get', `$${rt}`]]]]], 'f64')
1139
+ })
900
1140
 
901
- ctx.core.emit['.number:toFixed'] = (n, d) => {
902
- inc('__ftoa')
903
- return typed(['call', '$__ftoa', asF64(emit(n)), asI32(emit(d || [, 0])), ['i32.const', 1]], 'f64')
904
- }
1141
+ // BigInt.prototype.toString(radix) i64-exact, default radix 10.
1142
+ reg('.bigint:toString', ['__radix_str'], (n, radix) =>
1143
+ typed(['call', '$__radix_str', asI64(emit(n)), radix == null ? ['i32.const', 10] : asI32(emit(radix))], 'f64'))
905
1144
 
906
- ctx.core.emit['.number:toExponential'] = (n, d) => {
907
- inc('__toExp')
908
- return typed(['call', '$__toExp', asF64(emit(n)), asI32(emit(d || [, 0])), ['i32.const', 0]], 'f64')
909
- }
1145
+ reg('.number:toFixed', ['__ftoa'], (n, d) =>
1146
+ typed(['call', '$__ftoa', asF64(emit(n)), asI32(emit(d || [, 0])), ['i32.const', 1]], 'f64'))
910
1147
 
911
- ctx.core.emit['.number:toPrecision'] = (n, p) => {
912
- inc('__ftoa', '__toExp')
1148
+ reg('.number:toExponential', ['__toExp'], (n, d) =>
1149
+ typed(['call', '$__toExp', asF64(emit(n)), asI32(emit(d || [, 0])), ['i32.const', 0]], 'f64'))
1150
+
1151
+ reg('.number:toPrecision', ['__ftoa', '__toExp'], (n, p) => {
913
1152
  const val = temp('pv'), t = temp('tp'), exp = tempI32('te'), pr = tempI32('pp')
914
1153
  return typed(['block', ['result', 'f64'],
915
1154
  ['local.set', `$${val}`, asF64(emit(n))],
@@ -936,7 +1175,7 @@ export default (ctx) => {
936
1175
  ['else', ['call', '$__ftoa', ['local.get', `$${val}`],
937
1176
  ['i32.sub', ['i32.sub', ['local.get', `$${pr}`], ['i32.const', 1]], ['local.get', `$${exp}`]],
938
1177
  ['i32.const', 1]]]]], 'f64')
939
- }
1178
+ })
940
1179
 
941
1180
  // Number(x) — identity for numbers, i64→f64 conversion for BigInt
942
1181
  ctx.core.emit['Number'] = (x) => {
@@ -963,7 +1202,7 @@ export default (ctx) => {
963
1202
  ['if', ['result', 'f64'], ['f64.eq', ['local.get', `$${t}`], ['local.get', `$${t}`]],
964
1203
  ['then', ['f64.reinterpret_i64', ['i64.trunc_sat_f64_s', ['local.get', `$${t}`]]]],
965
1204
  ['else', ['if', ['result', 'f64'],
966
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', PTR.STRING]],
1205
+ ptrTypeEq(['local.get', `$${t}`], PTR.STRING),
967
1206
  ['then', ['call', '$__to_bigint', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
968
1207
  ['else', ['local.get', `$${t}`]]]]]], 'f64')
969
1208
  }
@@ -986,11 +1225,18 @@ export default (ctx) => {
986
1225
  ['i64.shr_s', ['i64.shl', vval, ['local.get', `$${t}`]], ['local.get', `$${t}`]]]], 'f64')
987
1226
  }
988
1227
 
989
- // BigInt.asUintN(bits, bigint) — truncate to unsigned N-bit
1228
+ // BigInt.asUintN(bits, bigint) — truncate to unsigned N-bit.
1229
+ // (val << (64 - bits)) >>> (64 - bits) — logical shift zero-extends the low `bits`.
1230
+ // The naive `val & ((1 << bits) - 1)` mask is wrong at bits=64: i64.shl shifts mod 64,
1231
+ // so `1 << 64` is `1 << 0 = 1`, making the mask `0` and asUintN(64,·) collapse to 0 —
1232
+ // which also zeroed every bigint *literal* (emit reinterprets `Nn` via
1233
+ // BigInt.asUintN(64,·).toString() through this very handler in the self-host).
990
1234
  ctx.core.emit['BigInt.asUintN'] = (bits, val) => {
991
1235
  const vbits = asI32(emit(bits)), vval = asI64(emit(val))
992
- // val & ((1 << bits) - 1)
993
- return typed(['f64.reinterpret_i64',
994
- ['i64.and', vval, ['i64.sub', ['i64.shl', ['i64.const', 1], ['i64.extend_i32_s', vbits]], ['i64.const', 1]]]], 'f64')
1236
+ const shift = typed(['i64.sub', ['i64.const', 64], ['i64.extend_i32_s', vbits]], 'i64')
1237
+ const t = tempI64('bu')
1238
+ return typed(['f64.reinterpret_i64', ['block', ['result', 'i64'],
1239
+ ['local.set', `$${t}`, shift],
1240
+ ['i64.shr_u', ['i64.shl', vval, ['local.get', `$${t}`]], ['local.get', `$${t}`]]]], 'f64')
995
1241
  }
996
1242
  }