jz 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/module/number.js CHANGED
@@ -9,19 +9,139 @@
9
9
  * @module number
10
10
  */
11
11
 
12
- import { typed, asF64, asI32, asI64, toNumF64, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64 } from '../src/ir.js'
13
- import { emit } from '../src/emit.js'
14
- import { isReassigned } from '../src/ast.js'
15
- import { valTypeOf, VAL } from '../src/analyze.js'
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'
16
15
  import { inc, PTR } from '../src/ctx.js'
17
16
 
17
+ // ─── Shared decimal-number parsing fragments ────────────────────────────────
18
+ // `__to_num` (Number coercion) and `__parseFloat` both scan a StrDecimalLiteral
19
+ // significand + ExponentPart, and that scan was verbatim-identical between them
20
+ // — a "fix the same bug twice" hazard (commit 652ba5f patched both copies).
21
+ // These named fragments are the common core, spliced into both bodies; the
22
+ // produced WASM is unchanged, the source now has one place to fix.
23
+ // Required locals (every consumer declares them):
24
+ // $v i64 · $i $len $c $dot $seen $sigDigits $decExp $dropped $round
25
+ // $exp $expNeg $expDigits $sbase i32 · $mant i64 · $result f64
26
+
27
+ // In-bounds byte read for a confirmed string `$v`. `__char_at` is ~95 WASM
28
+ // instructions — too large for V8 to inline — so a scan that calls it per
29
+ // char pays a real call plus a redundant SSO/view/bounds dispatch every step.
30
+ // A non-SSO string (the common case: source slices, heap strings) keeps its
31
+ // bytes contiguous at `$v & 0xFFFFFFFF` (`$sbase`); the read collapses to one
32
+ // `i32.load8_u`. The SSO test is loop-invariant — V8 hoists it — and the SSO
33
+ // arm still routes through `__char_at` (its bytes are packed in the pointer).
34
+ // Callers MUST declare `$sbase i32`, set it once after `$v` is final and a
35
+ // confirmed string, and only pass indices proven `< $len` (`__char_at` would
36
+ // otherwise return its OOB 0; the inline load has no such guard).
37
+ const SBASE_INIT = '(local.set $sbase (i32.wrap_i64 (i64.and (local.get $v) (i64.const 4294967295))))'
38
+ const chAt = idx => `(if (result i32)
39
+ (i64.eqz (i64.and (local.get $v) (i64.const 0x0000400000000000)))
40
+ (then (i32.load8_u (i32.add (local.get $sbase) ${idx})))
41
+ (else (call $__char_at (local.get $v) ${idx})))`
42
+
43
+ // chAt for reads NOT dominated by a `$i < $len` guard. `i32.and` does not
44
+ // short-circuit, so `(i32.and (lt_s $i $len) (… chAt …))` would still run the
45
+ // unguarded inline load when `$i == $len`. chAtSafe restores `__char_at`'s
46
+ // total contract (0 out of bounds), so such a site drops the now-redundant
47
+ // outer guard and compares chAtSafe directly against the wanted byte.
48
+ const chAtSafe = idx => `(if (result i32)
49
+ (i32.lt_s ${idx} (local.get $len))
50
+ (then ${chAt(idx)})
51
+ (else (i32.const 0)))`
52
+
53
+ // 18-significant-digit significand → $mant; $decExp tracks the base-10 exponent
54
+ // of dropped/fractional digits; $round defers a single round-up.
55
+ const DEC_SIGNIFICAND = `
56
+ (block $numDone (loop $numLoop
57
+ (br_if $numDone (i32.ge_s (local.get $i) (local.get $len)))
58
+ (local.set $c ${chAt('(local.get $i)')})
59
+ (if (i32.and (i32.eq (local.get $c) (i32.const 46)) (i32.eqz (local.get $dot)))
60
+ (then
61
+ (local.set $dot (i32.const 1))
62
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
63
+ (br $numLoop)))
64
+ (br_if $numDone
65
+ (i32.or
66
+ (i32.lt_s (local.get $c) (i32.const 48))
67
+ (i32.gt_s (local.get $c) (i32.const 57))))
68
+ (local.set $seen (i32.const 1))
69
+ (local.set $c (i32.sub (local.get $c) (i32.const 48)))
70
+ (if (i32.and (i32.eqz (local.get $sigDigits)) (i32.eqz (local.get $c)))
71
+ (then
72
+ (if (local.get $dot) (then (local.set $decExp (i32.sub (local.get $decExp) (i32.const 1)))))
73
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
74
+ (br $numLoop)))
75
+ ;; Accumulate the significand in an i64 (exact to 18 decimal digits,
76
+ ;; since 10^18 < 2^63) and convert to f64 once at the end — a single
77
+ ;; correctly-rounded i64->f64 step instead of lossy per-digit f64 math.
78
+ (if (i32.lt_s (local.get $sigDigits) (i32.const 18))
79
+ (then
80
+ (local.set $mant
81
+ (i64.add
82
+ (i64.mul (local.get $mant) (i64.const 10))
83
+ (i64.extend_i32_s (local.get $c))))
84
+ (local.set $sigDigits (i32.add (local.get $sigDigits) (i32.const 1)))
85
+ (if (local.get $dot) (then (local.set $decExp (i32.sub (local.get $decExp) (i32.const 1))))))
86
+ (else
87
+ (if (i32.eqz (local.get $dropped))
88
+ (then (if (i32.ge_s (local.get $c) (i32.const 5)) (then (local.set $round (i32.const 1))))))
89
+ (local.set $dropped (i32.const 1))
90
+ (if (i32.eqz (local.get $dot)) (then (local.set $decExp (i32.add (local.get $decExp) (i32.const 1)))))))
91
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
92
+ (br $numLoop)))`
93
+
94
+ // No significant digit seen → NaN; apply the deferred round; $mant → $result.
95
+ const FINISH_SIGNIFICAND = `
96
+ (if (i32.eqz (local.get $seen)) (then (return (f64.const nan))))
97
+ (if (local.get $round) (then (local.set $mant (i64.add (local.get $mant) (i64.const 1)))))
98
+ (local.set $result (f64.convert_i64_u (local.get $mant)))`
99
+
100
+ // ExponentPart scan: 'e'/'E' + optional sign + digits → $exp / $expDigits.
101
+ // `tail` runs inside the e/E branch — Number rejects an empty exponent ("1e")
102
+ // as NaN, parseFloat ignores it, so each caller passes its own resolution.
103
+ const sciExponent = (tail) => `
104
+ (local.set $c ${chAtSafe('(local.get $i)')})
105
+ (if (i32.or
106
+ (i32.eq (local.get $c) (i32.const 101))
107
+ (i32.eq (local.get $c) (i32.const 69)))
108
+ (then
109
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
110
+ (if (i32.eq ${chAtSafe('(local.get $i)')} (i32.const 45))
111
+ (then (local.set $expNeg (i32.const 1)) (local.set $i (i32.add (local.get $i) (i32.const 1)))))
112
+ (if (i32.eq ${chAtSafe('(local.get $i)')} (i32.const 43))
113
+ (then (local.set $i (i32.add (local.get $i) (i32.const 1)))))
114
+ (block $expDone (loop $expLoop
115
+ (br_if $expDone (i32.ge_s (local.get $i) (local.get $len)))
116
+ (local.set $c ${chAt('(local.get $i)')})
117
+ (br_if $expDone
118
+ (i32.or
119
+ (i32.lt_s (local.get $c) (i32.const 48))
120
+ (i32.gt_s (local.get $c) (i32.const 57))))
121
+ (local.set $exp
122
+ (i32.add
123
+ (i32.mul (local.get $exp) (i32.const 10))
124
+ (i32.sub (local.get $c) (i32.const 48))))
125
+ (local.set $expDigits (i32.add (local.get $expDigits) (i32.const 1)))
126
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
127
+ (br $expLoop)))
128
+ ${tail}))`
129
+
130
+ // Apply the accumulated base-10 exponent to $result via __pow10.
131
+ const POW10_SCALE = `
132
+ (if (i32.gt_s (local.get $decExp) (i32.const 0))
133
+ (then (local.set $result (f64.mul (local.get $result) (call $__pow10 (local.get $decExp))))))
134
+ (if (i32.lt_s (local.get $decExp) (i32.const 0))
135
+ (then (local.set $result (f64.div (local.get $result) (call $__pow10 (i32.sub (i32.const 0) (local.get $decExp)))))))`
136
+
18
137
  export default (ctx) => {
19
138
  Object.assign(ctx.core.stdlibDeps, {
20
139
  __mkstr: ['__alloc'],
21
140
  __ftoa: ['__itoa', '__pow10', '__mkstr', '__static_str', '__toExp'],
22
141
  __toExp: ['__itoa', '__pow10', '__mkstr', '__static_str'],
23
- __to_num: ['__char_at', '__str_byteLen', '__pow10', '__to_str'],
24
- __to_bigint: ['__char_at', '__str_byteLen'],
142
+ __to_num: ['__char_at', '__str_byteLen', '__pow10', '__to_str', '__skipws', '__ptr_aux'],
143
+ __skipws: ['__char_at', '__strws'],
144
+ __to_bigint: ['__char_at', '__str_byteLen', '__num_to_bigint'],
25
145
  __parseInt: ['__char_at', '__str_byteLen'],
26
146
  __parseFloat: ['__char_at', '__str_byteLen', '__pow10', '__to_str'],
27
147
  })
@@ -34,6 +154,9 @@ export default (ctx) => {
34
154
  // so accumulated error stays at O(log n) ULPs.
35
155
  ctx.core.stdlib['__pow10'] = `(func $__pow10 (param $n i32) (result f64)
36
156
  (local $r f64)
157
+ ;; 10^309 already overflows f64 (max ~1.8e308); short-circuit so callers
158
+ ;; get Infinity rather than the truncated product of a 9-bit decomposition.
159
+ (if (i32.ge_s (local.get $n) (i32.const 309)) (then (return (f64.const inf))))
37
160
  (local.set $r (f64.const 1))
38
161
  (if (i32.and (local.get $n) (i32.const 1))
39
162
  (then (local.set $r (f64.mul (local.get $r) (f64.const 10)))))
@@ -106,16 +229,16 @@ export default (ctx) => {
106
229
  (if (f64.eq (local.get $val) (f64.const inf)) (then (return (call $__static_str (i32.const 1)))))
107
230
  (if (f64.eq (local.get $val) (f64.const -inf)) (then (return (call $__static_str (i32.const 2)))))
108
231
  ;; ES spec: |x| >= 1e21 or 0 < |x| < 1e-6 → exponential notation (default mode only).
109
- ;; Cap prec at 9: mantissa fits i32, avoiding trunc_f64_u trap. Fewer digits than
110
- ;; ECMAScript shortest-repr ideal, but valid output for very large/small numbers.
232
+ ;; __toExp clamps the digit count so its scaled mantissa fits an unsigned i32.
233
+ ;; Fewer digits than ECMAScript shortest-repr ideal, but valid output.
111
234
  (if (i32.eqz (local.get $mode))
112
235
  (then
113
236
  (if (f64.ge (f64.abs (local.get $val)) (f64.const 1e21))
114
- (then (return (call $__toExp (local.get $val) (i32.const 9)))))
237
+ (then (return (call $__toExp (local.get $val) (i32.const 8) (i32.const 1)))))
115
238
  (if (i32.and
116
239
  (f64.gt (f64.abs (local.get $val)) (f64.const 0))
117
240
  (f64.lt (f64.abs (local.get $val)) (f64.const 1e-6)))
118
- (then (return (call $__toExp (local.get $val) (i32.const 9)))))))
241
+ (then (return (call $__toExp (local.get $val) (i32.const 8) (i32.const 1)))))))
119
242
  (local.set $buf (call $__alloc (i32.const 40)))
120
243
  ;; Sign
121
244
  (if (f64.lt (local.get $val) (f64.const 0))
@@ -204,15 +327,22 @@ export default (ctx) => {
204
327
  (then (local.set $pos (i32.sub (local.get $pos) (i32.const 1)))))))
205
328
  (call $__mkstr (local.get $buf) (local.get $pos)))`
206
329
 
207
- // __toExp(val: f64, prec: i32) → f64 (NaN-boxed string)
208
- // Format: [-]d.ddd...e[+/-]dd — integer-based digit extraction
209
- ctx.core.stdlib['__toExp'] = `(func $__toExp (param $val f64) (param $prec i32) (result f64)
330
+ // __toExp(val: f64, prec: i32, strip: i32) → f64 (NaN-boxed string)
331
+ // Format: [-]d.ddd...e[+/-]dd — integer-based digit extraction.
332
+ // strip=1 drops trailing fractional zeros (default ToString); strip=0 keeps
333
+ // the exact prec digits (toExponential/toPrecision need a fixed digit count).
334
+ ctx.core.stdlib['__toExp'] = `(func $__toExp (param $val f64) (param $prec i32) (param $strip i32) (result f64)
210
335
  (local $buf i32) (local $pos i32) (local $neg i32) (local $exp i32)
211
336
  (local $len i32) (local $i i32) (local $j i32)
212
337
  (local $mantissa f64) (local $scale f64)
213
338
  (if (f64.ne (local.get $val) (local.get $val)) (then (return (call $__static_str (i32.const 0)))))
214
339
  (if (f64.eq (local.get $val) (f64.const inf)) (then (return (call $__static_str (i32.const 1)))))
215
340
  (if (f64.eq (local.get $val) (f64.const -inf)) (then (return (call $__static_str (i32.const 2)))))
341
+ ;; The scaled mantissa is (prec+1) digits; cap prec at 8 so it stays below
342
+ ;; 2^32 (10^9 < 2^32 < 10^10), otherwise i32.trunc_f64_u below traps with
343
+ ;; "float unrepresentable in integer range" — e.g. 7.5e-151 normalizes to
344
+ ;; 7.5 and 7.5*10^9 already overflows an unsigned i32.
345
+ (if (i32.gt_s (local.get $prec) (i32.const 8)) (then (local.set $prec (i32.const 8))))
216
346
  (local.set $buf (call $__alloc (i32.const 32)))
217
347
  ;; Sign
218
348
  (if (f64.lt (local.get $val) (f64.const 0))
@@ -258,6 +388,16 @@ export default (ctx) => {
258
388
  (i32.store8 (i32.add (local.get $buf) (i32.add (local.get $pos) (i32.const 1))) (i32.const 46))
259
389
  (local.set $pos (i32.add (local.get $pos) (i32.add (local.get $len) (i32.const 1)))))
260
390
  (else (local.set $pos (i32.add (local.get $pos) (local.get $len)))))
391
+ ;; Shortest form: drop trailing zeros (and a bare '.') from the mantissa.
392
+ ;; The leading digit is always 1-9, so the walk-back stops at the '.' at worst.
393
+ (if (i32.and (local.get $strip) (i32.gt_s (local.get $prec) (i32.const 0)))
394
+ (then
395
+ (block $sz (loop $szl
396
+ (br_if $sz (i32.ne (i32.load8_u (i32.sub (i32.add (local.get $buf) (local.get $pos)) (i32.const 1))) (i32.const 48)))
397
+ (local.set $pos (i32.sub (local.get $pos) (i32.const 1)))
398
+ (br $szl)))
399
+ (if (i32.eq (i32.load8_u (i32.sub (i32.add (local.get $buf) (local.get $pos)) (i32.const 1))) (i32.const 46))
400
+ (then (local.set $pos (i32.sub (local.get $pos) (i32.const 1)))))))
261
401
  ;; Write 'e', sign, exponent
262
402
  (i32.store8 (i32.add (local.get $buf) (local.get $pos)) (i32.const 101))
263
403
  (local.set $pos (i32.add (local.get $pos) (i32.const 1)))
@@ -348,6 +488,17 @@ export default (ctx) => {
348
488
  ['f64.eq', ['local.get', `$${t}`], ['f64.trunc', ['local.get', `$${t}`]]]], 'i32')
349
489
  }
350
490
 
491
+ // Number.isSafeInteger(x): integer AND |x| ≤ 2^53 − 1.
492
+ ctx.core.emit['Number.isSafeInteger'] = (x) => {
493
+ const v = asF64(emit(x))
494
+ const t = temp('t')
495
+ return typed(['i32.and',
496
+ ['i32.and',
497
+ ['f64.eq', ['local.tee', `$${t}`, v], ['local.get', `$${t}`]],
498
+ ['f64.eq', ['local.get', `$${t}`], ['f64.trunc', ['local.get', `$${t}`]]]],
499
+ ['f64.le', ['f64.abs', ['local.get', `$${t}`]], ['f64.const', 9007199254740991]]], 'i32')
500
+ }
501
+
351
502
  ctx.core.stdlib['__parseInt'] = `(func $__parseInt (param $str i64) (param $radix i32) (result f64)
352
503
  (local $off i32) (local $len i32) (local $i i32) (local $c i32) (local $neg i32)
353
504
  (local $result f64) (local $digit i32) (local $seen i32) (local $f f64)
@@ -403,17 +554,72 @@ export default (ctx) => {
403
554
  (if (i32.eqz (local.get $seen)) (then (return (f64.const nan))))
404
555
  (if (result f64) (local.get $neg) (then (f64.neg (local.get $result))) (else (local.get $result))))`
405
556
 
557
+ // __strws(c: i32) → i32 — ECMA StrWhiteSpace predicate. Covers TAB..CR, SP,
558
+ // NBSP, BOM, LS/PS, and every Unicode Space_Separator. U+180E is *not*
559
+ // whitespace (declassified in Unicode 6.3).
560
+ ctx.core.stdlib['__strws'] = `(func $__strws (param $c i32) (result i32)
561
+ (i32.or
562
+ (i32.or
563
+ (i32.and (i32.ge_s (local.get $c) (i32.const 9)) (i32.le_s (local.get $c) (i32.const 13)))
564
+ (i32.or (i32.eq (local.get $c) (i32.const 32)) (i32.eq (local.get $c) (i32.const 160))))
565
+ (i32.or
566
+ (i32.or
567
+ (i32.eq (local.get $c) (i32.const 0x1680))
568
+ (i32.and (i32.ge_s (local.get $c) (i32.const 0x2000)) (i32.le_s (local.get $c) (i32.const 0x200a))))
569
+ (i32.or
570
+ (i32.or (i32.eq (local.get $c) (i32.const 0x2028)) (i32.eq (local.get $c) (i32.const 0x2029)))
571
+ (i32.or
572
+ (i32.or (i32.eq (local.get $c) (i32.const 0x202f)) (i32.eq (local.get $c) (i32.const 0x205f)))
573
+ (i32.or (i32.eq (local.get $c) (i32.const 0x3000)) (i32.eq (local.get $c) (i32.const 0xfeff))))))))`
574
+
575
+ // __skipws(v, i, len) → i32 — advance the byte index i past any run of
576
+ // StrWhiteSpace. Strings are UTF-8, so each step decodes one scalar: every
577
+ // whitespace code point is ≤ U+FEFF (≤ 3 bytes), and a 4-byte lead is never
578
+ // whitespace, so it (and any non-space scalar) ends the run.
579
+ ctx.core.stdlib['__skipws'] = `(func $__skipws (param $v i64) (param $i i32) (param $len i32) (result i32)
580
+ (local $b i32) (local $cp i32) (local $n i32) (local $sbase i32)
581
+ ${SBASE_INIT}
582
+ (block $done (loop $l
583
+ (br_if $done (i32.ge_s (local.get $i) (local.get $len)))
584
+ (local.set $b ${chAt('(local.get $i)')})
585
+ (if (i32.lt_u (local.get $b) (i32.const 0x80))
586
+ (then (local.set $cp (local.get $b)) (local.set $n (i32.const 1)))
587
+ (else (if (i32.lt_u (local.get $b) (i32.const 0xe0))
588
+ (then
589
+ (local.set $n (i32.const 2))
590
+ (local.set $cp (i32.or
591
+ (i32.shl (i32.and (local.get $b) (i32.const 0x1f)) (i32.const 6))
592
+ (i32.and (call $__char_at (local.get $v) (i32.add (local.get $i) (i32.const 1))) (i32.const 0x3f)))))
593
+ (else (if (i32.lt_u (local.get $b) (i32.const 0xf0))
594
+ (then
595
+ (local.set $n (i32.const 3))
596
+ (local.set $cp (i32.or (i32.or
597
+ (i32.shl (i32.and (local.get $b) (i32.const 0x0f)) (i32.const 12))
598
+ (i32.shl (i32.and (call $__char_at (local.get $v) (i32.add (local.get $i) (i32.const 1))) (i32.const 0x3f)) (i32.const 6)))
599
+ (i32.and (call $__char_at (local.get $v) (i32.add (local.get $i) (i32.const 2))) (i32.const 0x3f)))))
600
+ (else (return (local.get $i))))))))
601
+ (br_if $done (i32.eqz (call $__strws (local.get $cp))))
602
+ (local.set $i (i32.add (local.get $i) (local.get $n)))
603
+ (br $l)))
604
+ (local.get $i))`
605
+
406
606
  ctx.core.stdlib['__to_num'] = `(func $__to_num (param $v i64) (result f64)
407
607
  (local $t i32) (local $len i32) (local $i i32) (local $c i32) (local $neg i32)
408
608
  (local $seen i32) (local $exp i32) (local $expNeg i32) (local $expDigits i32)
409
609
  (local $dot i32) (local $sigDigits i32) (local $decExp i32) (local $dropped i32) (local $round i32)
410
- (local $wsEnd i32)
411
- (local $result f64) (local $f f64)
610
+ (local $radix i32) (local $digit i32) (local $sbase i32)
611
+ (local $result f64) (local $f f64) (local $mant i64)
412
612
  (local.set $f (f64.reinterpret_i64 (local.get $v)))
413
613
  (if (f64.eq (local.get $f) (local.get $f)) (then (return (local.get $f))))
414
614
  (if (i64.eq (local.get $v) (i64.const ${NULL_NAN})) (then (return (f64.const 0))))
415
615
  (if (i64.eq (local.get $v) (i64.const ${UNDEF_NAN})) (then (return (f64.const nan))))
416
616
  (local.set $t (call $__ptr_type (local.get $v)))
617
+ ;; ToNumber(Symbol) is a TypeError. A Symbol is an ATOM (type 0) with a user
618
+ ;; atom-id (>= 16); null/undefined returned above, and a bare NaN carries
619
+ ;; aux 0, so type==0 && aux>=16 uniquely identifies a Symbol.
620
+ (if (i32.and (i32.eqz (local.get $t))
621
+ (i32.ge_u (call $__ptr_aux (local.get $v)) (i32.const 16)))
622
+ (then (throw $__jz_err (f64.const 0))))
417
623
  ;; Non-string values go through ToString per JS spec, then re-check the
418
624
  ;; type in case ToString itself returned a non-string sentinel.
419
625
  (if (i32.ne (local.get $t) (i32.const ${PTR.STRING}))
@@ -423,154 +629,160 @@ export default (ctx) => {
423
629
  (if (i32.ne (local.get $t) (i32.const ${PTR.STRING}))
424
630
  (then (return (f64.const nan))))))
425
631
  (local.set $len (call $__str_byteLen (local.get $v)))
426
- ;; Skip leading whitespace.
427
- (block $ws (loop $wsl
428
- (br_if $ws (i32.ge_s (local.get $i) (local.get $len)))
429
- (br_if $ws (i32.gt_s (call $__char_at (local.get $v) (local.get $i)) (i32.const 32)))
430
- (local.set $i (i32.add (local.get $i) (i32.const 1)))
431
- (br $wsl)))
432
- (local.set $wsEnd (local.get $i))
433
- ;; Sign.
434
- (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
435
- (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 45)))
436
- (then (local.set $neg (i32.const 1)) (local.set $i (i32.add (local.get $i) (i32.const 1)))))
437
- (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
438
- (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 43)))
439
- (then (local.set $i (i32.add (local.get $i) (i32.const 1)))))
440
- ;; 0x prefix → hex parse and early return
632
+ ${SBASE_INIT}
633
+ ;; Trim leading whitespace. An empty / all-whitespace string is +0.
634
+ (local.set $i (call $__skipws (local.get $v) (i32.const 0) (local.get $len)))
635
+ (if (i32.ge_s (local.get $i) (local.get $len)) (then (return (f64.const 0))))
636
+ ;; NonDecimalIntegerLiteral (0x / 0o / 0b). Per the grammar no sign may
637
+ ;; precede the prefix, so it is matched before sign consumption.
441
638
  (if (i32.and
442
- (i32.le_s (i32.add (local.get $i) (i32.const 1)) (local.get $len))
443
- (i32.and (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 48))
444
- (i32.or (i32.eq (call $__char_at (local.get $v) (i32.add (local.get $i) (i32.const 1))) (i32.const 120))
445
- (i32.eq (call $__char_at (local.get $v) (i32.add (local.get $i) (i32.const 1))) (i32.const 88)))))
639
+ (i32.lt_s (i32.add (local.get $i) (i32.const 1)) (local.get $len))
640
+ (i32.eq ${chAt('(local.get $i)')} (i32.const 48)))
641
+ (then
642
+ (local.set $c ${chAt('(i32.add (local.get $i) (i32.const 1))')})
643
+ (if (i32.or (i32.eq (local.get $c) (i32.const 120)) (i32.eq (local.get $c) (i32.const 88)))
644
+ (then (local.set $radix (i32.const 16))))
645
+ (if (i32.or (i32.eq (local.get $c) (i32.const 111)) (i32.eq (local.get $c) (i32.const 79)))
646
+ (then (local.set $radix (i32.const 8))))
647
+ (if (i32.or (i32.eq (local.get $c) (i32.const 98)) (i32.eq (local.get $c) (i32.const 66)))
648
+ (then (local.set $radix (i32.const 2))))))
649
+ (if (local.get $radix)
446
650
  (then
447
651
  (local.set $i (i32.add (local.get $i) (i32.const 2)))
448
- (block $hexDone (loop $hexLoop
449
- (br_if $hexDone (i32.ge_s (local.get $i) (local.get $len)))
450
- (local.set $c (call $__char_at (local.get $v) (local.get $i)))
451
- (if (i32.and (i32.ge_s (local.get $c) (i32.const 48)) (i32.le_s (local.get $c) (i32.const 57)))
452
- (then (local.set $result (f64.add (f64.mul (local.get $result) (f64.const 16)) (f64.convert_i32_s (i32.sub (local.get $c) (i32.const 48)))))
453
- (local.set $seen (i32.const 1)) (local.set $i (i32.add (local.get $i) (i32.const 1))) (br $hexLoop)))
454
- (if (i32.and (i32.ge_s (local.get $c) (i32.const 97)) (i32.le_s (local.get $c) (i32.const 102)))
455
- (then (local.set $result (f64.add (f64.mul (local.get $result) (f64.const 16)) (f64.convert_i32_s (i32.sub (local.get $c) (i32.const 87)))))
456
- (local.set $seen (i32.const 1)) (local.set $i (i32.add (local.get $i) (i32.const 1))) (br $hexLoop)))
457
- (if (i32.and (i32.ge_s (local.get $c) (i32.const 65)) (i32.le_s (local.get $c) (i32.const 70)))
458
- (then (local.set $result (f64.add (f64.mul (local.get $result) (f64.const 16)) (f64.convert_i32_s (i32.sub (local.get $c) (i32.const 55)))))
459
- (local.set $seen (i32.const 1)) (local.set $i (i32.add (local.get $i) (i32.const 1))) (br $hexLoop)))))
460
- (return (if (result f64) (local.get $neg) (then (f64.neg (local.get $result))) (else (local.get $result))))))
461
- ;; Decimal significand. Keep 17 significant decimal digits, track the
462
- ;; base-10 exponent for skipped digits, and round once before pow10 scaling.
463
- (block $numDone (loop $numLoop
464
- (br_if $numDone (i32.ge_s (local.get $i) (local.get $len)))
465
- (local.set $c (call $__char_at (local.get $v) (local.get $i)))
466
- (if (i32.and (i32.eq (local.get $c) (i32.const 46)) (i32.eqz (local.get $dot)))
467
- (then
468
- (local.set $dot (i32.const 1))
469
- (local.set $i (i32.add (local.get $i) (i32.const 1)))
470
- (br $numLoop)))
471
- (br_if $numDone
472
- (i32.or
473
- (i32.lt_s (local.get $c) (i32.const 48))
474
- (i32.gt_s (local.get $c) (i32.const 57))))
475
- (local.set $seen (i32.const 1))
476
- (local.set $c (i32.sub (local.get $c) (i32.const 48)))
477
- (if (i32.and (i32.eqz (local.get $sigDigits)) (i32.eqz (local.get $c)))
478
- (then
479
- (if (local.get $dot) (then (local.set $decExp (i32.sub (local.get $decExp) (i32.const 1)))))
652
+ (block $ndDone (loop $ndLoop
653
+ (br_if $ndDone (i32.ge_s (local.get $i) (local.get $len)))
654
+ (local.set $c ${chAt('(local.get $i)')})
655
+ ;; Decode digit; 99 sentinel for any non-[0-9a-fA-F] char so the
656
+ ;; unsigned ">= radix" test rejects it and any out-of-base digit.
657
+ (local.set $digit
658
+ (if (result i32) (i32.and (i32.ge_s (local.get $c) (i32.const 48)) (i32.le_s (local.get $c) (i32.const 57)))
659
+ (then (i32.sub (local.get $c) (i32.const 48)))
660
+ (else (if (result i32) (i32.and (i32.ge_s (local.get $c) (i32.const 97)) (i32.le_s (local.get $c) (i32.const 102)))
661
+ (then (i32.sub (local.get $c) (i32.const 87)))
662
+ (else (if (result i32) (i32.and (i32.ge_s (local.get $c) (i32.const 65)) (i32.le_s (local.get $c) (i32.const 70)))
663
+ (then (i32.sub (local.get $c) (i32.const 55)))
664
+ (else (i32.const 99))))))))
665
+ (br_if $ndDone (i32.ge_u (local.get $digit) (local.get $radix)))
666
+ (local.set $result (f64.add (f64.mul (local.get $result) (f64.convert_i32_s (local.get $radix))) (f64.convert_i32_s (local.get $digit))))
667
+ (local.set $seen (i32.const 1))
480
668
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
481
- (br $numLoop)))
482
- (if (i32.lt_s (local.get $sigDigits)
483
- (if (result i32) (local.get $dot) (then (i32.const 16)) (else (i32.const 17))))
484
- (then
485
- (local.set $result
486
- (f64.add
487
- (f64.mul (local.get $result) (f64.const 10))
488
- (f64.convert_i32_s (local.get $c))))
489
- (local.set $sigDigits (i32.add (local.get $sigDigits) (i32.const 1)))
490
- (if (local.get $dot) (then (local.set $decExp (i32.sub (local.get $decExp) (i32.const 1))))))
491
- (else
492
- (if (i32.eqz (local.get $dropped))
493
- (then (if (i32.ge_s (local.get $c) (i32.const 5)) (then (local.set $round (i32.const 1))))))
494
- (local.set $dropped (i32.const 1))
495
- (if (i32.eqz (local.get $dot)) (then (local.set $decExp (i32.add (local.get $decExp) (i32.const 1)))))))
496
- (local.set $i (i32.add (local.get $i) (i32.const 1)))
497
- (br $numLoop)))
498
- ;; No digits seen: empty/whitespace-only string coerces to 0; non-numeric
499
- ;; content (sign-only, "abc", etc.) coerces to NaN.
500
- (if (i32.eqz (local.get $seen))
669
+ (br $ndLoop)))
670
+ ;; No digits, or trailing non-whitespace ("0b1.0", "0xg") → NaN.
671
+ (if (i32.eqz (local.get $seen)) (then (return (f64.const nan))))
672
+ (local.set $i (call $__skipws (local.get $v) (local.get $i) (local.get $len)))
673
+ (if (i32.lt_s (local.get $i) (local.get $len)) (then (return (f64.const nan))))
674
+ (return (local.get $result))))
675
+ ;; Sign (StrDecimalLiteral only).
676
+ (if (i32.eq ${chAt('(local.get $i)')} (i32.const 45))
677
+ (then (local.set $neg (i32.const 1)) (local.set $i (i32.add (local.get $i) (i32.const 1)))))
678
+ (if (i32.eq ${chAtSafe('(local.get $i)')} (i32.const 43))
679
+ (then (local.set $i (i32.add (local.get $i) (i32.const 1)))))
680
+ ;; "Infinity" — the only non-numeric token ToNumber accepts. The 8 letters
681
+ ;; are packed little-endian in one i64; any mismatch, short input, or
682
+ ;; trailing non-whitespace makes the whole string NaN.
683
+ (if (i32.eq ${chAtSafe('(local.get $i)')} (i32.const 73))
501
684
  (then
502
- (if (i32.ge_s (local.get $wsEnd) (local.get $len))
503
- (then (return (f64.const 0))))
685
+ (block $infBad
686
+ (local.set $digit (i32.const 0))
687
+ (loop $infl
688
+ (if (i32.lt_s (local.get $digit) (i32.const 8))
689
+ (then
690
+ (br_if $infBad (i32.ge_s (i32.add (local.get $i) (local.get $digit)) (local.get $len)))
691
+ (br_if $infBad (i32.ne
692
+ ${chAt('(i32.add (local.get $i) (local.get $digit))')}
693
+ (i32.and (i32.wrap_i64 (i64.shr_u (i64.const 0x7974696e69666e49)
694
+ (i64.extend_i32_u (i32.shl (local.get $digit) (i32.const 3))))) (i32.const 255))))
695
+ (local.set $digit (i32.add (local.get $digit) (i32.const 1)))
696
+ (br $infl))))
697
+ (local.set $i (call $__skipws (local.get $v) (i32.add (local.get $i) (i32.const 8)) (local.get $len)))
698
+ (br_if $infBad (i32.lt_s (local.get $i) (local.get $len)))
699
+ (return (if (result f64) (local.get $neg) (then (f64.const -inf)) (else (f64.const inf)))))
504
700
  (return (f64.const nan))))
505
- (if (local.get $round) (then (local.set $result (f64.add (local.get $result) (f64.const 1)))))
506
- ;; Scientific notation.
507
- (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
508
- (i32.or
509
- (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 101))
510
- (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 69))))
511
- (then
512
- (local.set $i (i32.add (local.get $i) (i32.const 1)))
513
- (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
514
- (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 45)))
515
- (then (local.set $expNeg (i32.const 1)) (local.set $i (i32.add (local.get $i) (i32.const 1)))))
516
- (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
517
- (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 43)))
518
- (then (local.set $i (i32.add (local.get $i) (i32.const 1)))))
519
- (block $expDone (loop $expLoop
520
- (br_if $expDone (i32.ge_s (local.get $i) (local.get $len)))
521
- (local.set $c (call $__char_at (local.get $v) (local.get $i)))
522
- (br_if $expDone
523
- (i32.or
524
- (i32.lt_s (local.get $c) (i32.const 48))
525
- (i32.gt_s (local.get $c) (i32.const 57))))
526
- (local.set $exp
527
- (i32.add
528
- (i32.mul (local.get $exp) (i32.const 10))
529
- (i32.sub (local.get $c) (i32.const 48))))
530
- (local.set $expDigits (i32.add (local.get $expDigits) (i32.const 1)))
531
- (local.set $i (i32.add (local.get $i) (i32.const 1)))
532
- (br $expLoop)))
533
- (if (local.get $expDigits)
534
- (then
535
- (if (local.get $expNeg)
536
- (then (local.set $decExp (i32.sub (local.get $decExp) (local.get $exp))))
537
- (else (local.set $decExp (i32.add (local.get $decExp) (local.get $exp)))))))))
538
- (if (i32.gt_s (local.get $decExp) (i32.const 0))
539
- (then (local.set $result (f64.mul (local.get $result) (call $__pow10 (local.get $decExp))))))
540
- (if (i32.lt_s (local.get $decExp) (i32.const 0))
541
- (then (local.set $result (f64.div (local.get $result) (call $__pow10 (i32.sub (i32.const 0) (local.get $decExp)))))))
701
+ ;; Decimal significand. Keep 18 significant decimal digits, track the
702
+ ;; base-10 exponent for skipped digits, and round once before pow10 scaling.
703
+ ${DEC_SIGNIFICAND}
704
+ ;; No digits — the literal was a bare sign or stray text ("abc", "+") → NaN.
705
+ ;; (Empty / all-whitespace strings already returned +0 above.)
706
+ ${FINISH_SIGNIFICAND}
707
+ ;; Scientific notation. 'e'/'E' commits to an ExponentPart — at least one
708
+ ;; digit must follow ("1e", "5e+" are NaN).
709
+ ${sciExponent(`(if (i32.eqz (local.get $expDigits)) (then (return (f64.const nan))))
710
+ (if (local.get $expNeg)
711
+ (then (local.set $decExp (i32.sub (local.get $decExp) (local.get $exp))))
712
+ (else (local.set $decExp (i32.add (local.get $decExp) (local.get $exp)))))`)}
713
+ ;; Reject trailing non-whitespace ("5px", numeric separators "1_0", …).
714
+ (local.set $i (call $__skipws (local.get $v) (local.get $i) (local.get $len)))
715
+ (if (i32.lt_s (local.get $i) (local.get $len)) (then (return (f64.const nan))))
716
+ ${POW10_SCALE}
542
717
  (if (result f64) (local.get $neg) (then (f64.neg (local.get $result))) (else (local.get $result))))`
543
718
 
719
+ // NumberToBigInt: a RangeError unless n is an integral Number — finite and
720
+ // equal to its own truncation. NaN fails the f64.eq integrality test;
721
+ // ±Infinity fails the finite test. Non-integers (1.1, .5, …) fail integrality.
722
+ // The throw rides $__jz_err so `assert.throws`/try-catch observe it.
723
+ ctx.core.stdlib['__num_to_bigint'] = `(func $__num_to_bigint (param $n f64) (result f64)
724
+ (if (i32.eqz (i32.and
725
+ (f64.eq (local.get $n) (f64.trunc (local.get $n)))
726
+ (f64.lt (f64.abs (local.get $n)) (f64.const inf))))
727
+ (then (throw $__jz_err (f64.const 0))))
728
+ (f64.reinterpret_i64 (i64.trunc_sat_f64_s (local.get $n))))`
729
+
730
+ // StringToBigInt: strict — the whole trimmed string must be a single integer
731
+ // literal, else a SyntaxError ($__jz_err). Unlike parseInt this does NOT stop
732
+ // at the first bad char: `BigInt("10n")` and `BigInt("000 12")` throw. Empty
733
+ // or all-whitespace strings parse to 0n. Radix prefixes 0b/0o/0x (case-
734
+ // insensitive) are recognised only when no sign precedes them, so `-0x1`
735
+ // surfaces its `x` as an invalid decimal digit and throws, as the spec wants.
736
+ // (jz's BigInt is i64-backed, so values past 2^63 wrap — out of these tests'
737
+ // range.)
544
738
  ctx.core.stdlib['__to_bigint'] = `(func $__to_bigint (param $v i64) (result f64)
545
- (local $t i32) (local $len i32) (local $i i32) (local $c i32) (local $neg i32)
546
- (local $radix i32) (local $digit i32) (local $seen i32) (local $result i64) (local $f f64)
739
+ (local $t i32) (local $len i32) (local $i i32) (local $end i32) (local $c i32)
740
+ (local $neg i32) (local $sign i32) (local $radix i32) (local $digit i32)
741
+ (local $seen i32) (local $result i64) (local $f f64)
547
742
  (local.set $f (f64.reinterpret_i64 (local.get $v)))
548
743
  (if (f64.eq (local.get $f) (local.get $f))
549
- (then (return (f64.reinterpret_i64 (i64.trunc_sat_f64_s (local.get $f))))))
744
+ (then (return (call $__num_to_bigint (local.get $f)))))
550
745
  (local.set $t (call $__ptr_type (local.get $v)))
551
746
  (if (i32.ne (local.get $t) (i32.const ${PTR.STRING}))
552
747
  (then (return (f64.reinterpret_i64 (i64.const 0)))))
553
748
  (local.set $len (call $__str_byteLen (local.get $v)))
749
+ (local.set $end (local.get $len))
750
+ ;; Trim leading whitespace (any byte <= 32).
554
751
  (block $ws (loop $wsl
555
- (br_if $ws (i32.ge_s (local.get $i) (local.get $len)))
752
+ (br_if $ws (i32.ge_s (local.get $i) (local.get $end)))
556
753
  (br_if $ws (i32.gt_s (call $__char_at (local.get $v) (local.get $i)) (i32.const 32)))
557
754
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
558
755
  (br $wsl)))
559
- (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
560
- (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 45)))
561
- (then (local.set $neg (i32.const 1)) (local.set $i (i32.add (local.get $i) (i32.const 1)))))
562
- (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
563
- (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 43)))
564
- (then (local.set $i (i32.add (local.get $i) (i32.const 1)))))
756
+ ;; Trim trailing whitespace.
757
+ (block $te (loop $tel
758
+ (br_if $te (i32.le_s (local.get $end) (local.get $i)))
759
+ (br_if $te (i32.gt_s (call $__char_at (local.get $v) (i32.sub (local.get $end) (i32.const 1))) (i32.const 32)))
760
+ (local.set $end (i32.sub (local.get $end) (i32.const 1)))
761
+ (br $tel)))
762
+ ;; Empty / all-whitespace string → 0n.
763
+ (if (i32.ge_s (local.get $i) (local.get $end))
764
+ (then (return (f64.reinterpret_i64 (i64.const 0)))))
765
+ ;; Optional single leading sign — decimal literals only.
766
+ (local.set $c (call $__char_at (local.get $v) (local.get $i)))
767
+ (if (i32.eq (local.get $c) (i32.const 45))
768
+ (then (local.set $neg (i32.const 1)) (local.set $sign (i32.const 1)) (local.set $i (i32.add (local.get $i) (i32.const 1))))
769
+ (else (if (i32.eq (local.get $c) (i32.const 43))
770
+ (then (local.set $sign (i32.const 1)) (local.set $i (i32.add (local.get $i) (i32.const 1)))))))
565
771
  (local.set $radix (i32.const 10))
566
- (if (i32.and
567
- (i32.lt_s (i32.add (local.get $i) (i32.const 1)) (local.get $len))
568
- (i32.and (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 48))
569
- (i32.or (i32.eq (call $__char_at (local.get $v) (i32.add (local.get $i) (i32.const 1))) (i32.const 120))
570
- (i32.eq (call $__char_at (local.get $v) (i32.add (local.get $i) (i32.const 1))) (i32.const 88)))))
571
- (then (local.set $radix (i32.const 16)) (local.set $i (i32.add (local.get $i) (i32.const 2)))))
772
+ ;; Radix prefix 0b/0o/0x (case-insensitive) — not allowed after a sign.
773
+ (if (i32.and (i32.eqz (local.get $sign))
774
+ (i32.and (i32.lt_s (i32.add (local.get $i) (i32.const 1)) (local.get $end))
775
+ (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 48))))
776
+ (then
777
+ (local.set $c (i32.or (call $__char_at (local.get $v) (i32.add (local.get $i) (i32.const 1))) (i32.const 0x20)))
778
+ (if (i32.eq (local.get $c) (i32.const 98)) (then (local.set $radix (i32.const 2))))
779
+ (if (i32.eq (local.get $c) (i32.const 111)) (then (local.set $radix (i32.const 8))))
780
+ (if (i32.eq (local.get $c) (i32.const 120)) (then (local.set $radix (i32.const 16))))
781
+ (if (i32.ne (local.get $radix) (i32.const 10))
782
+ (then (local.set $i (i32.add (local.get $i) (i32.const 2)))))))
783
+ ;; Strict scan — every remaining char must be a valid radix digit.
572
784
  (block $done (loop $lp
573
- (br_if $done (i32.ge_s (local.get $i) (local.get $len)))
785
+ (br_if $done (i32.ge_s (local.get $i) (local.get $end)))
574
786
  (local.set $c (call $__char_at (local.get $v) (local.get $i)))
575
787
  (local.set $digit (i32.const -1))
576
788
  (if (i32.and (i32.ge_s (local.get $c) (i32.const 48)) (i32.le_s (local.get $c) (i32.const 57)))
@@ -579,7 +791,8 @@ export default (ctx) => {
579
791
  (then (local.set $digit (i32.sub (local.get $c) (i32.const 87)))))
580
792
  (if (i32.and (i32.ge_s (local.get $c) (i32.const 65)) (i32.le_s (local.get $c) (i32.const 90)))
581
793
  (then (local.set $digit (i32.sub (local.get $c) (i32.const 55)))))
582
- (br_if $done (i32.or (i32.lt_s (local.get $digit) (i32.const 0)) (i32.ge_s (local.get $digit) (local.get $radix))))
794
+ (if (i32.or (i32.lt_s (local.get $digit) (i32.const 0)) (i32.ge_s (local.get $digit) (local.get $radix)))
795
+ (then (throw $__jz_err (f64.const 0))))
583
796
  (local.set $seen (i32.const 1))
584
797
  (local.set $result
585
798
  (i64.add
@@ -587,7 +800,8 @@ export default (ctx) => {
587
800
  (i64.extend_i32_s (local.get $digit))))
588
801
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
589
802
  (br $lp)))
590
- (if (i32.eqz (local.get $seen)) (then (return (f64.reinterpret_i64 (i64.const 0)))))
803
+ ;; A sign or radix prefix with no digits ("-", "0x", "0b") is a SyntaxError.
804
+ (if (i32.eqz (local.get $seen)) (then (throw $__jz_err (f64.const 0))))
591
805
  (f64.reinterpret_i64
592
806
  (if (result i64) (local.get $neg)
593
807
  (then (i64.sub (i64.const 0) (local.get $result)))
@@ -597,7 +811,7 @@ export default (ctx) => {
597
811
  (local $t i32) (local $len i32) (local $i i32) (local $c i32) (local $neg i32)
598
812
  (local $seen i32) (local $exp i32) (local $expNeg i32) (local $expDigits i32)
599
813
  (local $dot i32) (local $sigDigits i32) (local $decExp i32) (local $dropped i32) (local $round i32)
600
- (local $result f64) (local $f f64)
814
+ (local $result f64) (local $f f64) (local $mant i64) (local $sbase i32)
601
815
  (local.set $f (f64.reinterpret_i64 (local.get $v)))
602
816
  (if (f64.eq (local.get $f) (local.get $f)) (then (return (local.get $f))))
603
817
  (local.set $t (call $__ptr_type (local.get $v)))
@@ -611,99 +825,43 @@ export default (ctx) => {
611
825
  (if (i32.ne (local.get $t) (i32.const ${PTR.STRING}))
612
826
  (then (return (f64.const nan))))))
613
827
  (local.set $len (call $__str_byteLen (local.get $v)))
828
+ ${SBASE_INIT}
614
829
  ;; Skip leading whitespace.
615
830
  (block $ws (loop $wsl
616
831
  (br_if $ws (i32.ge_s (local.get $i) (local.get $len)))
617
- (br_if $ws (i32.gt_s (call $__char_at (local.get $v) (local.get $i)) (i32.const 32)))
832
+ (br_if $ws (i32.gt_s ${chAt('(local.get $i)')} (i32.const 32)))
618
833
  (local.set $i (i32.add (local.get $i) (i32.const 1)))
619
834
  (br $wsl)))
620
835
  ;; Sign.
621
- (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
622
- (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 45)))
836
+ (if (i32.eq ${chAtSafe('(local.get $i)')} (i32.const 45))
623
837
  (then (local.set $neg (i32.const 1)) (local.set $i (i32.add (local.get $i) (i32.const 1)))))
624
- (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
625
- (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 43)))
838
+ (if (i32.eq ${chAtSafe('(local.get $i)')} (i32.const 43))
626
839
  (then (local.set $i (i32.add (local.get $i) (i32.const 1)))))
627
- ;; Decimal significand. Keep 17 significant decimal digits, track the
840
+ ;; Decimal significand. Keep 18 significant decimal digits, track the
628
841
  ;; base-10 exponent for skipped digits, and round once before pow10 scaling.
629
- (block $numDone (loop $numLoop
630
- (br_if $numDone (i32.ge_s (local.get $i) (local.get $len)))
631
- (local.set $c (call $__char_at (local.get $v) (local.get $i)))
632
- (if (i32.and (i32.eq (local.get $c) (i32.const 46)) (i32.eqz (local.get $dot)))
633
- (then
634
- (local.set $dot (i32.const 1))
635
- (local.set $i (i32.add (local.get $i) (i32.const 1)))
636
- (br $numLoop)))
637
- (br_if $numDone
638
- (i32.or
639
- (i32.lt_s (local.get $c) (i32.const 48))
640
- (i32.gt_s (local.get $c) (i32.const 57))))
641
- (local.set $seen (i32.const 1))
642
- (local.set $c (i32.sub (local.get $c) (i32.const 48)))
643
- (if (i32.and (i32.eqz (local.get $sigDigits)) (i32.eqz (local.get $c)))
644
- (then
645
- (if (local.get $dot) (then (local.set $decExp (i32.sub (local.get $decExp) (i32.const 1)))))
646
- (local.set $i (i32.add (local.get $i) (i32.const 1)))
647
- (br $numLoop)))
648
- (if (i32.lt_s (local.get $sigDigits)
649
- (if (result i32) (local.get $dot) (then (i32.const 16)) (else (i32.const 17))))
650
- (then
651
- (local.set $result
652
- (f64.add
653
- (f64.mul (local.get $result) (f64.const 10))
654
- (f64.convert_i32_s (local.get $c))))
655
- (local.set $sigDigits (i32.add (local.get $sigDigits) (i32.const 1)))
656
- (if (local.get $dot) (then (local.set $decExp (i32.sub (local.get $decExp) (i32.const 1))))))
657
- (else
658
- (if (i32.eqz (local.get $dropped))
659
- (then (if (i32.ge_s (local.get $c) (i32.const 5)) (then (local.set $round (i32.const 1))))))
660
- (local.set $dropped (i32.const 1))
661
- (if (i32.eqz (local.get $dot)) (then (local.set $decExp (i32.add (local.get $decExp) (i32.const 1)))))))
662
- (local.set $i (i32.add (local.get $i) (i32.const 1)))
663
- (br $numLoop)))
664
- (if (i32.eqz (local.get $seen)) (then (return (f64.const nan))))
665
- (if (local.get $round) (then (local.set $result (f64.add (local.get $result) (f64.const 1)))))
842
+ ${DEC_SIGNIFICAND}
843
+ ${FINISH_SIGNIFICAND}
666
844
  ;; Scientific notation.
667
- (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
668
- (i32.or
669
- (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 101))
670
- (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 69))))
671
- (then
672
- (local.set $i (i32.add (local.get $i) (i32.const 1)))
673
- (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
674
- (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 45)))
675
- (then (local.set $expNeg (i32.const 1)) (local.set $i (i32.add (local.get $i) (i32.const 1)))))
676
- (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
677
- (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 43)))
678
- (then (local.set $i (i32.add (local.get $i) (i32.const 1)))))
679
- (block $expDone (loop $expLoop
680
- (br_if $expDone (i32.ge_s (local.get $i) (local.get $len)))
681
- (local.set $c (call $__char_at (local.get $v) (local.get $i)))
682
- (br_if $expDone
683
- (i32.or
684
- (i32.lt_s (local.get $c) (i32.const 48))
685
- (i32.gt_s (local.get $c) (i32.const 57))))
686
- (local.set $exp
687
- (i32.add
688
- (i32.mul (local.get $exp) (i32.const 10))
689
- (i32.sub (local.get $c) (i32.const 48))))
690
- (local.set $expDigits (i32.add (local.get $expDigits) (i32.const 1)))
691
- (local.set $i (i32.add (local.get $i) (i32.const 1)))
692
- (br $expLoop)))
693
- (if (local.get $expDigits)
845
+ ${sciExponent(`(if (local.get $expDigits)
694
846
  (then
695
847
  (if (local.get $expNeg)
696
848
  (then (local.set $decExp (i32.sub (local.get $decExp) (local.get $exp))))
697
- (else (local.set $decExp (i32.add (local.get $decExp) (local.get $exp)))))))))
698
- (if (i32.gt_s (local.get $decExp) (i32.const 0))
699
- (then (local.set $result (f64.mul (local.get $result) (call $__pow10 (local.get $decExp))))))
700
- (if (i32.lt_s (local.get $decExp) (i32.const 0))
701
- (then (local.set $result (f64.div (local.get $result) (call $__pow10 (i32.sub (i32.const 0) (local.get $decExp)))))))
849
+ (else (local.set $decExp (i32.add (local.get $decExp) (local.get $exp)))))))`)}
850
+ ${POW10_SCALE}
702
851
  (if (result f64) (local.get $neg) (then (f64.neg (local.get $result))) (else (local.get $result))))`
703
852
 
853
+ // ToString(arg) for the string-input builtins. A statically-known boolean must
854
+ // render as "true"/"false" (spec step 1: ToString) before parsing — otherwise
855
+ // its 0/1 carrier bits are fed to the parser as if a string pointer. Other types
856
+ // (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))
858
+
704
859
  ctx.core.emit['Number.parseInt'] = (x, radix) => {
705
860
  needParseInt()
706
- return typed(['call', '$__parseInt', asI64(emit(x)), radix ? asI32(emit(radix)) : ['i32.const', 0]], 'f64')
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
+ const radixIR = radix == null ? ['i32.const', 0] : toI32(toNumF64(radix, emit(radix)))
864
+ return typed(['call', '$__parseInt', strInputI64(x), radixIR], 'f64')
707
865
  }
708
866
  ctx.core.emit['parseInt'] = ctx.core.emit['Number.parseInt']
709
867
  const addImportOnce = (ctx, mod, name, fn) => {
@@ -718,10 +876,10 @@ export default (ctx) => {
718
876
  ctx.core.emit['Number.parseFloat'] = (x) => {
719
877
  if (ctx.transform.host === 'wasi') {
720
878
  inc('__parseFloat')
721
- return typed(['call', '$__parseFloat', asI64(emit(x))], 'f64')
879
+ return typed(['call', '$__parseFloat', strInputI64(x)], 'f64')
722
880
  }
723
881
  needParseFloat()
724
- return typed(['call', '$__parseFloat', asI64(emit(x))], 'f64')
882
+ return typed(['call', '$__parseFloat', strInputI64(x)], 'f64')
725
883
  }
726
884
  ctx.core.emit['parseFloat'] = ctx.core.emit['Number.parseFloat']
727
885
 
@@ -747,7 +905,7 @@ export default (ctx) => {
747
905
 
748
906
  ctx.core.emit['.number:toExponential'] = (n, d) => {
749
907
  inc('__toExp')
750
- return typed(['call', '$__toExp', asF64(emit(n)), asI32(emit(d || [, 0]))], 'f64')
908
+ return typed(['call', '$__toExp', asF64(emit(n)), asI32(emit(d || [, 0])), ['i32.const', 0]], 'f64')
751
909
  }
752
910
 
753
911
  ctx.core.emit['.number:toPrecision'] = (n, p) => {
@@ -774,7 +932,7 @@ export default (ctx) => {
774
932
  ['i32.or',
775
933
  ['i32.lt_s', ['local.get', `$${exp}`], ['i32.const', -6]],
776
934
  ['i32.ge_s', ['local.get', `$${exp}`], ['local.get', `$${pr}`]]],
777
- ['then', ['call', '$__toExp', ['local.get', `$${val}`], ['i32.sub', ['local.get', `$${pr}`], ['i32.const', 1]]]],
935
+ ['then', ['call', '$__toExp', ['local.get', `$${val}`], ['i32.sub', ['local.get', `$${pr}`], ['i32.const', 1]], ['i32.const', 0]]],
778
936
  ['else', ['call', '$__ftoa', ['local.get', `$${val}`],
779
937
  ['i32.sub', ['i32.sub', ['local.get', `$${pr}`], ['i32.const', 1]], ['local.get', `$${exp}`]],
780
938
  ['i32.const', 1]]]]], 'f64')
@@ -792,6 +950,9 @@ export default (ctx) => {
792
950
  // For number input: truncate directly. For string / unknown: first coerce via __to_num
793
951
  // (handles both decimal and hex string parse), then truncate.
794
952
  ctx.core.emit['BigInt'] = (x) => {
953
+ // Every BigInt() path can fault: a non-integral Number is a RangeError and
954
+ // a malformed String is a SyntaxError, both raised via $__jz_err.
955
+ ctx.runtime.throws = true
795
956
  const vt = valTypeOf(x)
796
957
  if (vt === VAL.BIGINT) {
797
958
  if (typeof x === 'bigint' || (typeof x === 'string' && !isReassigned(ctx.func.body, x))) return emit(x)
@@ -806,8 +967,10 @@ export default (ctx) => {
806
967
  ['then', ['call', '$__to_bigint', ['i64.reinterpret_f64', ['local.get', `$${t}`]]]],
807
968
  ['else', ['local.get', `$${t}`]]]]]], 'f64')
808
969
  }
809
- if (vt === VAL.NUMBER)
810
- return typed(['f64.reinterpret_i64', ['i64.trunc_sat_f64_s', asF64(emit(x))]], 'f64')
970
+ if (vt === VAL.NUMBER) {
971
+ inc('__num_to_bigint')
972
+ return typed(['call', '$__num_to_bigint', asF64(emit(x))], 'f64')
973
+ }
811
974
  inc('__to_bigint')
812
975
  return typed(['call', '$__to_bigint', asI64(emit(x))], 'f64')
813
976
  }