jz 0.5.1 → 0.7.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 (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /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,13 @@ 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'],
158
+ __i32_to_str: ['__itoa', '__mkstr'],
141
159
  __toExp: ['__itoa', '__pow10', '__mkstr', '__static_str'],
160
+ __radix_str: ['__mkstr'],
161
+ __num_radix: ['__ftoa', '__mkstr'],
142
162
  __to_num: ['__char_at', '__str_byteLen', '__pow10', '__to_str', '__skipws', '__ptr_aux'],
143
163
  __skipws: ['__char_at', '__strws'],
144
164
  __to_bigint: ['__char_at', '__str_byteLen', '__num_to_bigint'],
@@ -193,22 +213,151 @@ export default (ctx) => {
193
213
  (br $l)))
194
214
  ;; Reverse
195
215
  (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)))
216
+ ${reverseBytesWat()}
217
+ (local.get $len))`
218
+
219
+ // __i32_to_str(val: i32) → f64 (NaN-boxed string) — ToString for a value the
220
+ // compiler proved is a signed i32. The whole point is to bypass __ftoa's float
221
+ // machinery (shortest-repr search, __toExp, __pow10): a known integer renders with
222
+ // just __itoa over its magnitude plus a sign byte. Lets `"id " + (n|0)` and integer
223
+ // templates skip the ~2 KB float formatter the generic ToString hard-pulls.
224
+ ctx.core.stdlib['__i32_to_str'] = `(func $__i32_to_str (param $val i32) (result f64)
225
+ (local $buf i32) (local $len i32) (local $u i32)
226
+ (local.set $buf (call $__alloc (i32.const 12)))
227
+ (if (i32.lt_s (local.get $val) (i32.const 0))
228
+ (then
229
+ (i32.store8 (local.get $buf) (i32.const 45)) ;; '-'
230
+ ;; magnitude as unsigned: negate via 0 - val (INT_MIN maps to itself, read u below)
231
+ (local.set $u (i32.sub (i32.const 0) (local.get $val)))
232
+ (local.set $len (call $__itoa (local.get $u) (i32.add (local.get $buf) (i32.const 1))))
233
+ (return (call $__mkstr (local.get $buf) (i32.add (local.get $len) (i32.const 1)))))
234
+ (else
235
+ (local.set $len (call $__itoa (local.get $val) (local.get $buf)))
236
+ (return (call $__mkstr (local.get $buf) (local.get $len)))))
237
+ (f64.const 0))`
238
+
239
+ // __radix_str(val: i64, radix: i32) → f64 (NaN-boxed string)
240
+ // Signed integer → radix string for BigInt.prototype.toString(radix). Digits go
241
+ // 0-9 then a-z (lowercase, per spec); magnitude is taken unsigned so i64.MIN
242
+ // (whose two's-complement negation is itself) formats correctly via div_u/rem_u.
243
+ ctx.core.stdlib['__radix_str'] = `(func $__radix_str (param $val i64) (param $radix i32) (result f64)
244
+ (local $buf i32) (local $pos i32) (local $neg i32) (local $mag i64) (local $r i64)
245
+ (local $dg i32) (local $i i32) (local $j i32) (local $tmp i32)
246
+ (local.set $buf (call $__alloc (i32.const 72)))
247
+ (local.set $r (i64.extend_i32_s (local.get $radix)))
248
+ (if (i64.eqz (local.get $val))
249
+ (then (i32.store8 (local.get $buf) (i32.const 48)) (return (call $__mkstr (local.get $buf) (i32.const 1)))))
250
+ (local.set $mag (local.get $val))
251
+ (if (i64.lt_s (local.get $val) (i64.const 0))
252
+ (then (local.set $neg (i32.const 1)) (local.set $mag (i64.sub (i64.const 0) (local.get $val)))))
253
+ (block $mb (loop $ml
254
+ (br_if $mb (i64.eqz (local.get $mag)))
255
+ (local.set $dg (i32.wrap_i64 (i64.rem_u (local.get $mag) (local.get $r))))
256
+ (i32.store8 (i32.add (local.get $buf) (local.get $pos))
257
+ (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))))
258
+ (local.set $mag (i64.div_u (local.get $mag) (local.get $r)))
259
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
260
+ (br $ml)))
261
+ (if (local.get $neg)
262
+ (then (i32.store8 (i32.add (local.get $buf) (local.get $pos)) (i32.const 45))
263
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))))
264
+ (local.set $j (i32.sub (local.get $pos) (i32.const 1)))
265
+ ${reverseBytesWat()}
266
+ (call $__mkstr (local.get $buf) (local.get $pos)))`
267
+
268
+ // __num_radix(val: f64, radix: i32) → f64 (NaN-boxed string)
269
+ // Number.prototype.toString(radix) for radix != 10. Non-finite values defer to
270
+ // __ftoa ("NaN"/"Infinity"/"-Infinity"). Integer part uses exact i64 division
271
+ // (magnitude bounded by jz's i64 BigInt domain); the fraction multiplies out in
272
+ // f64, capped at 100 digits (radix fractions are implementation-defined precision).
273
+ //
274
+ // Per 21.1.3.6: radix must be an integer in [2, 36] — otherwise RangeError.
275
+ // The check sits at the canonical entry point so the const-radix fold path
276
+ // (which still calls here for any non-10 constant) and the dynamic-radix
277
+ // branch are validated uniformly. `(throw $__jz_err …)` is picked up by
278
+ // ensureThrowRuntime via stdlib scan, so callers do not need to flag throws.
279
+ ctx.core.stdlib['__num_radix'] = `(func $__num_radix (param $val f64) (param $radix i32) (result f64)
280
+ (local $buf i32) (local $pos i32) (local $neg i32) (local $iv i64) (local $r i64) (local $rf f64)
281
+ (local $int f64) (local $frac f64) (local $dg i32) (local $i i32) (local $j i32) (local $tmp i32) (local $fn i32) (local $rv f64)
282
+ (if (i32.or (i32.lt_s (local.get $radix) (i32.const 2)) (i32.gt_s (local.get $radix) (i32.const 36)))
283
+ (then (throw $__jz_err (f64.const 0))))
284
+ (if (i32.or (f64.ne (local.get $val) (local.get $val)) (f64.eq (f64.abs (local.get $val)) (f64.const inf)))
285
+ (then (return (call $__ftoa (local.get $val) (i32.const 0) (i32.const 0)))))
286
+ (local.set $buf (call $__alloc (i32.const 180)))
287
+ (local.set $r (i64.extend_i32_s (local.get $radix)))
288
+ (local.set $rf (f64.convert_i32_s (local.get $radix)))
289
+ (if (f64.lt (local.get $val) (f64.const 0))
290
+ (then (local.set $neg (i32.const 1)) (local.set $val (f64.neg (local.get $val)))))
291
+ (local.set $int (f64.floor (local.get $val)))
292
+ (local.set $frac (f64.sub (local.get $val) (local.get $int)))
293
+ (local.set $iv (i64.trunc_sat_f64_u (local.get $int)))
294
+ (if (i64.eqz (local.get $iv))
295
+ (then (i32.store8 (local.get $buf) (i32.const 48)) (local.set $pos (i32.const 1)))
296
+ (else
297
+ (block $ib (loop $il
298
+ (br_if $ib (i64.eqz (local.get $iv)))
299
+ (local.set $dg (i32.wrap_i64 (i64.rem_u (local.get $iv) (local.get $r))))
300
+ (i32.store8 (i32.add (local.get $buf) (local.get $pos))
301
+ (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))))
302
+ (local.set $iv (i64.div_u (local.get $iv) (local.get $r)))
303
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
304
+ (br $il)))))
305
+ (if (local.get $neg)
306
+ (then (i32.store8 (i32.add (local.get $buf) (local.get $pos)) (i32.const 45))
307
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))))
308
+ (local.set $j (i32.sub (local.get $pos) (i32.const 1)))
309
+ (block $rb (loop $rl
310
+ (br_if $rb (i32.ge_s (local.get $i) (local.get $j)))
198
311
  (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))))
312
+ (i32.store8 (i32.add (local.get $buf) (local.get $i)) (i32.load8_u (i32.add (local.get $buf) (local.get $j))))
201
313
  (i32.store8 (i32.add (local.get $buf) (local.get $j)) (local.get $tmp))
202
314
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
203
315
  (local.set $j (i32.sub (local.get $j) (i32.const 1)))
204
316
  (br $rl)))
205
- (local.get $len))`
317
+ (if (f64.gt (local.get $frac) (f64.const 0))
318
+ (then
319
+ (i32.store8 (i32.add (local.get $buf) (local.get $pos)) (i32.const 46))
320
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
321
+ (block $fb (loop $fl
322
+ (br_if $fb (f64.le (local.get $frac) (f64.const 0)))
323
+ (br_if $fb (i32.ge_s (local.get $fn) (i32.const 100)))
324
+ (local.set $frac (f64.mul (local.get $frac) (local.get $rf)))
325
+ (local.set $dg (i32.trunc_f64_s (f64.floor (local.get $frac))))
326
+ (i32.store8 (i32.add (local.get $buf) (local.get $pos))
327
+ (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))))
328
+ (local.set $frac (f64.sub (local.get $frac) (f64.floor (local.get $frac))))
329
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
330
+ (local.set $fn (i32.add (local.get $fn) (i32.const 1)))
331
+ (br $fl)))))
332
+ ${!ctx.memory.shared ? `
333
+ ;; When __mkstr packed an SSO result it allocated nothing, so the digit scratch
334
+ ;; ($buf, at heap top) is dead — reclaim it so a heap-top accumulator stays on
335
+ ;; top and \`s += n.toString(r)\` bump-extends instead of reallocating (O(n)).
336
+ (local.set $rv (call $__mkstr (local.get $buf) (local.get $pos)))
337
+ (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}))
338
+ (then (global.set $__heap (local.get $buf))))
339
+ (local.get $rv)` : `(call $__mkstr (local.get $buf) (local.get $pos))`})`
206
340
 
207
341
  // __mkstr(buf: i32, len: i32) → f64 — copy scratch buffer to heap string.
208
342
  // Hot (~60M calls in watr self-host via __ftoa). bulk memory.copy is ~10× faster than
209
343
  // a hand-rolled byte loop (wasm2c lowers it to memcpy under PGO+LTO).
210
344
  ctx.core.stdlib['__mkstr'] = `(func $__mkstr (param $buf i32) (param $len i32) (result f64)
211
- (local $off i32)
345
+ (local $off i32) (local $i i32) (local $packed i32) (local $b i32)
346
+ ;; SSO fast path: ≤4 ASCII bytes pack into the pointer with no allocation, so a
347
+ ;; number-format result doesn't displace a heap-top accumulator — keeping the
348
+ ;; canonical \`s += n.toString(r)\` builder O(n) via the bump-extend path.
349
+ (if (i32.le_u (local.get $len) (i32.const 4))
350
+ (then
351
+ (block $heap
352
+ (loop $pk
353
+ (if (i32.lt_u (local.get $i) (local.get $len))
354
+ (then
355
+ (local.set $b (i32.load8_u (i32.add (local.get $buf) (local.get $i))))
356
+ (br_if $heap (i32.ge_u (local.get $b) (i32.const 0x80)))
357
+ (local.set $packed (i32.or (local.get $packed) (i32.shl (local.get $b) (i32.shl (local.get $i) (i32.const 3)))))
358
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
359
+ (br $pk))))
360
+ (return (call $__mkptr (i32.const ${PTR.STRING}) (i32.or (i32.const ${LAYOUT.SSO_BIT}) (local.get $len)) (local.get $packed))))))
212
361
  (local.set $off (call $__alloc (i32.add (i32.const 4) (local.get $len))))
213
362
  (i32.store (local.get $off) (local.get $len))
214
363
  (local.set $off (i32.add (local.get $off) (i32.const 4)))
@@ -251,7 +400,12 @@ export default (ctx) => {
251
400
  ;; Default mode: auto-select precision (up to 9 digits, must fit i32 when scaled)
252
401
  (if (i32.eqz (local.get $mode))
253
402
  (then (local.set $prec (i32.const 9))))
254
- ;; Round and scale to integer: scaled = nearest(val * 10^prec)
403
+ ;; Round and scale to integer: scaled = nearest(val * 10^prec).
404
+ ;; NOTE: toFixed/toPrecision round ties-to-even here (f64.nearest), which differs from
405
+ ;; JS's round-half-away-from-zero on exact halves like (2.5).toFixed(0) → '2' vs '3'.
406
+ ;; A naive floor(x+0.5) "fixes" those but breaks values like 1.45 (whose ×10 rounds up
407
+ ;; to 14.5 in f64, giving '1.5' vs JS '1.4'); bit-exact toFixed needs the exact-decimal
408
+ ;; algorithm. Documented as a known difference rather than trading one error for another.
255
409
  (local.set $scale (call $__pow10 (local.get $prec)))
256
410
  (local.set $scaled (f64.nearest (f64.mul (local.get $val) (local.get $scale))))
257
411
  ;; If scaled doesn't fit i32, reduce precision until it does (min prec=0)
@@ -267,19 +421,40 @@ export default (ctx) => {
267
421
  (then
268
422
  (local.set $int (i32.trunc_f64_u (f64.div (local.get $scaled) (local.get $scale))))
269
423
  (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))))))
424
+ (f64.mul (f64.convert_i32_u (local.get $int)) (local.get $scale)))))
425
+ ;; Default mode, fit loop reduced prec to 0: the rounded integer is ready, but the
426
+ ;; original val may still have a fractional part that was discarded. Recover it:
427
+ ;; frac_f = val - trunc(val); since frac_f ∈ [0,1), frac_f*10^9 < 10^9 < 2^31 — safe.
428
+ (if (i32.and (i32.eqz (local.get $mode)) (i32.eqz (local.get $prec)))
429
+ (then
430
+ (local.set $abs (f64.sub (local.get $val) (f64.trunc (local.get $val))))
431
+ (if (f64.gt (local.get $abs) (f64.const 0))
432
+ (then
433
+ ;; $int was taken from f64.nearest(val), which rounds .5+ UP (999999999.9 → 1e9),
434
+ ;; but $abs/$frac below derive from f64.trunc(val). Re-derive $int from the same
435
+ ;; trunc so integer and fraction agree — else String(999999999.9) → "1000000000.9".
436
+ (local.set $int (i32.trunc_f64_u (f64.trunc (local.get $val))))
437
+ (local.set $prec (i32.const 9))
438
+ (local.set $scale (call $__pow10 (i32.const 9)))
439
+ ;; round: trunc_u(x+0.5) == floor(x+0.5) for the positive frac scale
440
+ (local.set $frac (i32.trunc_f64_u (f64.add
441
+ (f64.mul (local.get $abs) (f64.const 1000000000))
442
+ (f64.const 0.5)))))))))
271
443
  (else
272
444
  (local.set $int (i32.const 0))
273
445
  (local.set $frac (i32.const 0))
274
446
  (local.set $prec (i32.const 0))
275
447
  (local.set $abs (f64.trunc (local.get $val)))
276
- ;; Write large integer digits reversed
448
+ ;; Write large integer digits reversed.
449
+ ;; Clamp digit to [0,9]: f64 precision loss for large values can make the naive
450
+ ;; subtraction (abs - trunc(abs/10)*10) go slightly negative → i32.trunc_f64_u trap.
277
451
  (local.set $ilen (local.get $pos))
278
452
  (block $ld (loop $ll
279
453
  (br_if $ld (f64.lt (local.get $abs) (f64.const 1)))
280
454
  (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))))))
455
+ (i32.add (i32.const 48) (i32.trunc_f64_u (f64.max (f64.const 0) (f64.min (f64.const 9)
456
+ (f64.nearest (f64.sub (local.get $abs)
457
+ (f64.mul (f64.trunc (f64.div (local.get $abs) (f64.const 10))) (f64.const 10)))))))))
283
458
  (local.set $abs (f64.trunc (f64.div (local.get $abs) (f64.const 10))))
284
459
  (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
285
460
  (br $ll)))
@@ -294,6 +469,43 @@ export default (ctx) => {
294
469
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
295
470
  (local.set $j (i32.sub (local.get $j) (i32.const 1)))
296
471
  (br $rl)))
472
+ ;; Default mode: emit fractional part if val has one (large-int path skipped it before).
473
+ ;; frac_f = val - trunc(val); since frac_f ∈ [0,1), frac_f*10^9 < 10^9 < 2^31 — safe.
474
+ (if (i32.eqz (local.get $mode))
475
+ (then
476
+ (local.set $abs (f64.sub (local.get $val) (f64.trunc (local.get $val))))
477
+ (if (f64.gt (local.get $abs) (f64.const 0))
478
+ (then
479
+ ;; round: trunc_u(x+0.5) == floor(x+0.5) for the positive frac scale
480
+ (local.set $frac (i32.trunc_f64_u (f64.add
481
+ (f64.mul (local.get $abs) (f64.const 1000000000))
482
+ (f64.const 0.5))))
483
+ (i32.store8 (i32.add (local.get $buf) (local.get $pos)) (i32.const 46))
484
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
485
+ ;; 9 fractional digits from $frac, high-to-low
486
+ (local.set $i (i32.const 8))
487
+ (block $fd2 (loop $fl2
488
+ (br_if $fd2 (i32.lt_s (local.get $i) (i32.const 0)))
489
+ (local.set $j (i32.div_u (local.get $frac) (i32.trunc_f64_u (call $__pow10 (local.get $i)))))
490
+ (i32.store8 (i32.add (local.get $buf) (local.get $pos))
491
+ (i32.add (i32.const 48) (i32.rem_u (local.get $j) (i32.const 10))))
492
+ (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
493
+ (local.set $i (i32.sub (local.get $i) (i32.const 1)))
494
+ (br $fl2)))
495
+ ;; Strip trailing zeros
496
+ (block $sz2 (loop $sl2
497
+ (br_if $sz2 (i32.le_s (local.get $pos) (i32.const 0)))
498
+ (br_if $sz2 (i32.ne
499
+ (i32.load8_u (i32.add (local.get $buf) (i32.sub (local.get $pos) (i32.const 1))))
500
+ (i32.const 48)))
501
+ (local.set $pos (i32.sub (local.get $pos) (i32.const 1)))
502
+ (br $sl2)))
503
+ ;; Strip trailing dot
504
+ (if (i32.and (i32.gt_s (local.get $pos) (i32.const 0))
505
+ (i32.eq
506
+ (i32.load8_u (i32.add (local.get $buf) (i32.sub (local.get $pos) (i32.const 1))))
507
+ (i32.const 46)))
508
+ (then (local.set $pos (i32.sub (local.get $pos) (i32.const 1)))))))))
297
509
  (return (call $__mkstr (local.get $buf) (local.get $pos)))))
298
510
  ;; Write integer part
299
511
  (local.set $ilen (call $__itoa (local.get $int) (i32.add (local.get $buf) (local.get $pos))))
@@ -365,7 +577,7 @@ export default (ctx) => {
365
577
  (local.set $val (f64.mul (local.get $val) (f64.const 10)))
366
578
  (local.set $exp (i32.sub (local.get $exp) (i32.const 1)))
367
579
  (br $l2)))))
368
- ;; Scale to integer mantissa: nearest(val * 10^prec)
580
+ ;; Scale to integer mantissa: nearest(val * 10^prec). Ties-to-even (see __ftoa note).
369
581
  (local.set $scale (call $__pow10 (local.get $prec)))
370
582
  (local.set $mantissa (f64.nearest (f64.mul (local.get $val) (local.get $scale))))
371
583
  ;; Rounding overflow (e.g. 9.95 → 1000 when prec=1, scale=10)
@@ -441,7 +653,7 @@ export default (ctx) => {
441
653
  ctx.core.emit['Number.MIN_VALUE'] = () => typed(['f64.const', 5e-324], 'f64')
442
654
  ctx.core.emit['Number.POSITIVE_INFINITY'] = () => typed(['f64.const', Infinity], 'f64')
443
655
  ctx.core.emit['Number.NEGATIVE_INFINITY'] = () => typed(['f64.const', -Infinity], 'f64')
444
- ctx.core.emit['Number.NaN'] = () => typed(['f64.const', NaN], 'f64')
656
+ ctx.core.emit['Number.NaN'] = () => typed(['f64.const', 'nan'], 'f64') // `nan` token, not raw NaN (survives self-host IR marshalling — see emitNum)
445
657
 
446
658
  // === Number static methods ===
447
659
 
@@ -501,7 +713,8 @@ export default (ctx) => {
501
713
 
502
714
  ctx.core.stdlib['__parseInt'] = `(func $__parseInt (param $str i64) (param $radix i32) (result f64)
503
715
  (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)
716
+ (local $digit i32) (local $seen i32) (local $f f64)
717
+ (local $acc i64) (local $rad i64) (local $ovf i32) (local $exp i32) (local $sticky i32) (local $k i32) (local $e i32)
505
718
  (local.set $f (f64.reinterpret_i64 (local.get $str)))
506
719
  ;; If input is a number, just truncate
507
720
  (if (f64.eq (local.get $f) (local.get $f)) (then (return (f64.trunc (local.get $f)))))
@@ -524,16 +737,21 @@ export default (ctx) => {
524
737
  (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
525
738
  (i32.eq (call $__char_at (local.get $str) (local.get $i)) (i32.const 43)))
526
739
  (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))
740
+ ;; 0x prefix → radix 16 (stripped when radix is unspecified OR explicitly 16, per JS)
741
+ (if (i32.and (i32.or (i32.eqz (local.get $radix)) (i32.eq (local.get $radix) (i32.const 16)))
529
742
  (i32.and (i32.le_s (i32.add (local.get $i) (i32.const 1)) (local.get $len))
530
743
  (i32.and (i32.eq (call $__char_at (local.get $str) (local.get $i)) (i32.const 48))
531
744
  (i32.or (i32.eq (call $__char_at (local.get $str) (i32.add (local.get $i) (i32.const 1))) (i32.const 120))
532
745
  (i32.eq (call $__char_at (local.get $str) (i32.add (local.get $i) (i32.const 1))) (i32.const 88))))))
533
746
  (then (local.set $radix (i32.const 16)) (local.set $i (i32.add (local.get $i) (i32.const 2)))))
534
747
  (if (i32.eqz (local.get $radix)) (then (local.set $radix (i32.const 10))))
535
- ;; Parse digits
536
- (local.set $result (f64.const 0))
748
+ ;; Power-of-two radix → exact bit width per digit (lets the >2^64 path round once).
749
+ (if (i32.eqz (i32.and (local.get $radix) (i32.sub (local.get $radix) (i32.const 1))))
750
+ (then (local.set $k (i32.ctz (local.get $radix)))))
751
+ (local.set $rad (i64.extend_i32_u (local.get $radix)))
752
+ ;; Parse digits — accumulate EXACTLY in u64 (round-once via convert at the end, matching JS for
753
+ ;; any value < 2^64). On u64 overflow, freeze the high bits and track the dropped magnitude
754
+ ;; (exp) + a sticky bit so the final round-to-f64 still matches round-once for power-of-two radix.
537
755
  (block $done (loop $lp
538
756
  (br_if $done (i32.ge_s (local.get $i) (local.get $len)))
539
757
  (local.set $c (call $__char_at (local.get $str) (local.get $i)))
@@ -547,12 +765,41 @@ export default (ctx) => {
547
765
  (then (local.set $digit (i32.sub (local.get $c) (i32.const 55)))))
548
766
  (br_if $done (i32.or (i32.lt_s (local.get $digit) (i32.const 0)) (i32.ge_s (local.get $digit) (local.get $radix))))
549
767
  (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))))
768
+ (if (i32.eqz (local.get $ovf))
769
+ (then
770
+ ;; acc*radix + digit, exact while it stays within unsigned 64-bit
771
+ (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)))
772
+ (then (local.set $acc (i64.add (i64.mul (local.get $acc) (local.get $rad)) (i64.extend_i32_u (local.get $digit)))))
773
+ (else
774
+ (local.set $ovf (i32.const 1))
775
+ ;; non-power-of-two radix: continue round-each in f64 from the exact seed (keeps magnitude)
776
+ (if (i32.eqz (local.get $k)) (then (local.set $f (f64.convert_i64_u (local.get $acc)))))))))
777
+ (if (local.get $ovf)
778
+ (then
779
+ (if (local.get $k)
780
+ (then ;; power-of-two: this digit and all later ones sit below the frozen high bits
781
+ (local.set $exp (i32.add (local.get $exp) (local.get $k)))
782
+ (if (local.get $digit) (then (local.set $sticky (i32.const 1)))))
783
+ (else ;; other radix: round-each f64 — ≤1 ULP past 2^53, but only for >2^64 values
784
+ (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
785
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
552
786
  (br $lp)))
553
787
  ;; No digits consumed → NaN
554
788
  (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))))`
789
+ ;; Produce the f64. No overflow exact round-once convert. Overflow+power-of-two round the
790
+ ;; frozen high bits (sticky-injected to break ties correctly) and scale by 2^exp. Overflow on
791
+ ;; another radix → $f already holds the round-each result.
792
+ (if (i32.eqz (local.get $ovf))
793
+ (then (local.set $f (f64.convert_i64_u (local.get $acc))))
794
+ (else (if (local.get $k)
795
+ (then
796
+ (if (local.get $sticky) (then (local.set $acc (i64.or (local.get $acc) (i64.const 1)))))
797
+ (local.set $f (f64.convert_i64_u (local.get $acc)))
798
+ (local.set $e (i32.add (local.get $exp) (i32.const 1023)))
799
+ (if (i32.ge_s (local.get $e) (i32.const 2047))
800
+ (then (local.set $f (f64.const inf)))
801
+ (else (local.set $f (f64.mul (local.get $f) (f64.reinterpret_i64 (i64.shl (i64.extend_i32_u (local.get $e)) (i64.const 52)))))))))))
802
+ (if (result f64) (local.get $neg) (then (f64.neg (local.get $f))) (else (local.get $f))))`
556
803
 
557
804
  // __strws(c: i32) → i32 — ECMA StrWhiteSpace predicate. Covers TAB..CR, SP,
558
805
  // NBSP, BOM, LS/PS, and every Unicode Space_Separator. U+180E is *not*
@@ -613,6 +860,8 @@ export default (ctx) => {
613
860
  (if (f64.eq (local.get $f) (local.get $f)) (then (return (local.get $f))))
614
861
  (if (i64.eq (local.get $v) (i64.const ${NULL_NAN})) (then (return (f64.const 0))))
615
862
  (if (i64.eq (local.get $v) (i64.const ${UNDEF_NAN})) (then (return (f64.const nan))))
863
+ (if (i64.eq (local.get $v) (i64.const ${FALSE_NAN})) (then (return (f64.const 0))))
864
+ (if (i64.eq (local.get $v) (i64.const ${TRUE_NAN})) (then (return (f64.const 1))))
616
865
  (local.set $t (call $__ptr_type (local.get $v)))
617
866
  ;; ToNumber(Symbol) is a TypeError. A Symbol is an ATOM (type 0) with a user
618
867
  ;; atom-id (>= 16); null/undefined returned above, and a bare NaN carries
@@ -854,23 +1103,22 @@ export default (ctx) => {
854
1103
  // render as "true"/"false" (spec step 1: ToString) before parsing — otherwise
855
1104
  // its 0/1 carrier bits are fed to the parser as if a string pointer. Other types
856
1105
  // (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))
1106
+ const strInputI64 = (x) => valTypeOf(x) === VAL.BOOL ? asI64(bool(x)) : asI64(emit(x))
858
1107
 
859
1108
  ctx.core.emit['Number.parseInt'] = (x, radix) => {
1109
+ if (ctx.transform.host === 'wasi') {
1110
+ inc('__parseInt')
1111
+ const radixIR = radix == null ? ['i32.const', 0] : toI32(toNumF64(radix, emit(radix)))
1112
+ return typed(['call', '$__parseInt', strInputI64(x), radixIR], 'f64')
1113
+ }
860
1114
  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
1115
  const radixIR = radix == null ? ['i32.const', 0] : toI32(toNumF64(radix, emit(radix)))
864
1116
  return typed(['call', '$__parseInt', strInputI64(x), radixIR], 'f64')
865
1117
  }
866
1118
  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',
1119
+ const needParseFloat = () => hostImport('env', 'parseFloat',
872
1120
  ['func', '$__parseFloat', ['param', 'i64'], ['result', 'f64']])
873
- const needParseInt = () => addImportOnce(ctx, 'env', 'parseInt',
1121
+ const needParseInt = () => hostImport('env', 'parseInt',
874
1122
  ['func', '$__parseInt', ['param', 'i64'], ['param', 'i32'], ['result', 'f64']])
875
1123
 
876
1124
  ctx.core.emit['Number.parseFloat'] = (x) => {
@@ -884,32 +1132,44 @@ export default (ctx) => {
884
1132
  ctx.core.emit['parseFloat'] = ctx.core.emit['Number.parseFloat']
885
1133
 
886
1134
  // Boolean(x) → truthiness (non-zero → 1, zero → 0)
887
- ctx.core.emit['Boolean'] = (x) => {
1135
+ reg('Boolean', ['__is_truthy'], (x) => {
888
1136
  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
- }
1137
+ // Via truthyIR so a NUMBER arg gets the NaN-safe f64 test (Boolean(0/0) === false
1138
+ // on every platform); other types fall to __is_truthy inside truthyIR.
1139
+ return typed(['f64.convert_i32_s', truthyIR(emit(x))], 'f64')
1140
+ })
893
1141
 
894
1142
  // === Instance method emitters ===
895
1143
 
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
- }
1144
+ const ftoaDefault = (v) => typed(['call', '$__ftoa', v, ['i32.const', 0], ['i32.const', 0]], 'f64')
1145
+ reg('.number:toString', ['__ftoa', '__num_radix'], (n, radix) => {
1146
+ const v = asF64(emit(n))
1147
+ if (radix == null) return ftoaDefault(v)
1148
+ const rv = emit(radix)
1149
+ // Constant radix folds the 10-vs-other choice at compile time; radix 10 keeps
1150
+ // __ftoa's shortest-repr (the radix loop would emit float-noise tail digits).
1151
+ if (Array.isArray(rv) && rv[0] === 'f64.const' && typeof rv[1] === 'number')
1152
+ return rv[1] === 10 ? ftoaDefault(v) : typed(['call', '$__num_radix', v, ['i32.const', rv[1] | 0]], 'f64')
1153
+ const vt = temp('rv'), rt = tempI32('rr')
1154
+ return typed(['block', ['result', 'f64'],
1155
+ ['local.set', `$${vt}`, v],
1156
+ ['local.set', `$${rt}`, asI32(rv)],
1157
+ ['if', ['result', 'f64'], ['i32.eq', ['local.get', `$${rt}`], ['i32.const', 10]],
1158
+ ['then', ftoaDefault(['local.get', `$${vt}`])],
1159
+ ['else', ['call', '$__num_radix', ['local.get', `$${vt}`], ['local.get', `$${rt}`]]]]], 'f64')
1160
+ })
900
1161
 
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
- }
1162
+ // BigInt.prototype.toString(radix) i64-exact, default radix 10.
1163
+ reg('.bigint:toString', ['__radix_str'], (n, radix) =>
1164
+ typed(['call', '$__radix_str', asI64(emit(n)), radix == null ? ['i32.const', 10] : asI32(emit(radix))], 'f64'))
905
1165
 
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
- }
1166
+ reg('.number:toFixed', ['__ftoa'], (n, d) =>
1167
+ typed(['call', '$__ftoa', asF64(emit(n)), asI32(emit(d || [, 0])), ['i32.const', 1]], 'f64'))
910
1168
 
911
- ctx.core.emit['.number:toPrecision'] = (n, p) => {
912
- inc('__ftoa', '__toExp')
1169
+ reg('.number:toExponential', ['__toExp'], (n, d) =>
1170
+ typed(['call', '$__toExp', asF64(emit(n)), asI32(emit(d || [, 0])), ['i32.const', 0]], 'f64'))
1171
+
1172
+ reg('.number:toPrecision', ['__ftoa', '__toExp'], (n, p) => {
913
1173
  const val = temp('pv'), t = temp('tp'), exp = tempI32('te'), pr = tempI32('pp')
914
1174
  return typed(['block', ['result', 'f64'],
915
1175
  ['local.set', `$${val}`, asF64(emit(n))],
@@ -936,7 +1196,7 @@ export default (ctx) => {
936
1196
  ['else', ['call', '$__ftoa', ['local.get', `$${val}`],
937
1197
  ['i32.sub', ['i32.sub', ['local.get', `$${pr}`], ['i32.const', 1]], ['local.get', `$${exp}`]],
938
1198
  ['i32.const', 1]]]]], 'f64')
939
- }
1199
+ })
940
1200
 
941
1201
  // Number(x) — identity for numbers, i64→f64 conversion for BigInt
942
1202
  ctx.core.emit['Number'] = (x) => {
@@ -963,7 +1223,7 @@ export default (ctx) => {
963
1223
  ['if', ['result', 'f64'], ['f64.eq', ['local.get', `$${t}`], ['local.get', `$${t}`]],
964
1224
  ['then', ['f64.reinterpret_i64', ['i64.trunc_sat_f64_s', ['local.get', `$${t}`]]]],
965
1225
  ['else', ['if', ['result', 'f64'],
966
- ['i32.eq', ['call', '$__ptr_type', ['i64.reinterpret_f64', ['local.get', `$${t}`]]], ['i32.const', PTR.STRING]],
1226
+ ptrTypeEq(['local.get', `$${t}`], PTR.STRING),
967
1227
  ['then', ['call', '$__to_bigint', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
968
1228
  ['else', ['local.get', `$${t}`]]]]]], 'f64')
969
1229
  }
@@ -986,11 +1246,18 @@ export default (ctx) => {
986
1246
  ['i64.shr_s', ['i64.shl', vval, ['local.get', `$${t}`]], ['local.get', `$${t}`]]]], 'f64')
987
1247
  }
988
1248
 
989
- // BigInt.asUintN(bits, bigint) — truncate to unsigned N-bit
1249
+ // BigInt.asUintN(bits, bigint) — truncate to unsigned N-bit.
1250
+ // (val << (64 - bits)) >>> (64 - bits) — logical shift zero-extends the low `bits`.
1251
+ // The naive `val & ((1 << bits) - 1)` mask is wrong at bits=64: i64.shl shifts mod 64,
1252
+ // so `1 << 64` is `1 << 0 = 1`, making the mask `0` and asUintN(64,·) collapse to 0 —
1253
+ // which also zeroed every bigint *literal* (emit reinterprets `Nn` via
1254
+ // BigInt.asUintN(64,·).toString() through this very handler in the self-host).
990
1255
  ctx.core.emit['BigInt.asUintN'] = (bits, val) => {
991
1256
  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')
1257
+ const shift = typed(['i64.sub', ['i64.const', 64], ['i64.extend_i32_s', vbits]], 'i64')
1258
+ const t = tempI64('bu')
1259
+ return typed(['f64.reinterpret_i64', ['block', ['result', 'i64'],
1260
+ ['local.set', `$${t}`, shift],
1261
+ ['i64.shr_u', ['i64.shl', vval, ['local.get', `$${t}`]], ['local.get', `$${t}`]]]], 'f64')
995
1262
  }
996
1263
  }