jz 0.3.1 → 0.5.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.
package/module/math.js CHANGED
@@ -2,10 +2,9 @@
2
2
  * Math module - Math.sin, Math.cos, Math.sqrt, Math.PI, etc.
3
3
  *
4
4
  * Module API:
5
- * - ctx.core.emit['math.X'] = (args) => WasmNode - custom emitters
5
+ * - ctx.core.emit['math.X'] = emitter(deps, args => WasmNode) - emitters with declarative stdlib deps
6
6
  * - ctx.core.stdlib['math.X'] = '(func ...)' - WAT function definitions
7
- * - ctx.deps['math.X'] = ['dep1', 'dep2'] - stdlib dependencies
8
- * - include('math.X') - marks stdlib for inclusion (called by emitters)
7
+ * - ctx.core.stdlibDeps['math.X'] = ['dep'] - direct stdlib→stdlib edges (expanded transitively)
9
8
  *
10
9
  * Prepare resolves Math.sin(x) → ['()', 'math.sin', x]
11
10
  * Compile looks up ctx.core.emit['math.sin'] and calls it.
@@ -13,23 +12,73 @@
13
12
  * @module math
14
13
  */
15
14
 
16
- import { typed, asF64, asI32, temp, arrayLoop } from '../src/ir.js'
15
+ import { typed, asF64, asI32, toI32, toNumF64, temp, arrayLoop, isLit, litVal } from '../src/ir.js'
17
16
  import { emit } from '../src/emit.js'
18
- import { inc } from '../src/ctx.js'
17
+ import { emitter } from '../src/ctx.js'
19
18
  import { repOf } from '../src/analyze.js'
20
19
 
21
20
  export default (ctx) => {
22
- // Helpers: all math ops take f64 and return f64
23
- const f = (op, a) => typed([op, asF64(emit(a))], 'f64')
24
- const f2 = (op, a, b) => typed([op, asF64(emit(a)), asF64(emit(b))], 'f64')
21
+ // Direct stdlib→stdlib call edges. resolveIncludes() expands these transitively,
22
+ // so each emitter declares only the single stdlib it directly calls.
23
+ Object.assign(ctx.core.stdlibDeps, {
24
+ 'math.sin': ['math.isFinite'],
25
+ 'math.cos': ['math.isFinite'],
26
+ 'math.tan': ['math.sin', 'math.cos'],
27
+ 'math.expm1': ['math.exp'],
28
+ 'math.log2': ['math.log'],
29
+ 'math.log1p': ['math.log'],
30
+ 'math.pow': ['math.exp', 'math.log'],
31
+ 'math.asin': ['math.atan'],
32
+ 'math.acos': ['math.asin'],
33
+ 'math.atan2': ['math.atan'],
34
+ 'math.sinh': ['math.exp'],
35
+ 'math.cosh': ['math.exp'],
36
+ 'math.tanh': ['math.exp'],
37
+ 'math.asinh': ['math.isFinite', 'math.log'],
38
+ 'math.acosh': ['math.log'],
39
+ 'math.atanh': ['math.log'],
40
+ 'math.cbrt': ['math.isFinite', 'math.pow'],
41
+ 'math.sumPrecise': ['__ptr_offset', '__len', '__alloc'],
42
+ })
43
+
44
+ // Helpers: all math ops take f64 and return f64. Args go through ToNumber
45
+ // (toNumF64) — ECMA Math methods coerce each argument, so null→0, undefined→NaN.
46
+ const f = (op, a) => typed([op, toNumF64(a, emit(a))], 'f64')
25
47
  // floor/ceil/trunc/round are no-ops on integer-valued operands. When the
26
48
  // arg is a local whose every def is integer-valued (intCertain lattice),
27
- // skip the wasm op and just hand back the operand cast to f64.
28
- const fInt = (op, a) => typeof a === 'string' && repOf(a)?.intCertain === true
29
- ? asF64(emit(a))
30
- : f(op, a)
31
- const call = (name, ...args) => (inc(name), typed(['call', `$${name}`, ...args.map(a => asF64(emit(a)))], 'f64'))
32
- const callDeps = (deps, name, ...args) => (inc(...deps), call(name, ...args))
49
+ // skip the wasm op and just hand back the operand cast to f64. Same elision
50
+ // fires for schema-field reads `o.x` when every observed write to that slot
51
+ // is integer-shaped (ctx.schema.slotIntCertainAt).
52
+ const isIntCertain = a => {
53
+ if (typeof a === 'string') return repOf(a)?.intCertain === true
54
+ if (Array.isArray(a) && a[0] === '.' && typeof a[1] === 'string' && typeof a[2] === 'string') {
55
+ return ctx.schema.slotIntCertainAt?.(a[1], a[2]) === true
56
+ }
57
+ return false
58
+ }
59
+ const fInt = (op, a) => isIntCertain(a) ? asF64(emit(a)) : f(op, a)
60
+ // ECMA Math methods perform ToNumber on each argument. toNumF64 short-circuits
61
+ // for known-number nodes, and routes everything else through __to_num so null→0,
62
+ // undefined→NaN, and strings get parsed. Without this, raw NaN-boxed pointers
63
+ // (null/undefined/strings) would propagate through math.log etc. and surface
64
+ // as the original null/undefined sentinel after decode.
65
+ const call = (name, ...args) => typed(['call', `$${name}`, ...args.map(a => toNumF64(a, emit(a)))], 'f64')
66
+
67
+ // Canonicalize a possibly-NaN f64 result. A wasm arithmetic op that mints a
68
+ // fresh NaN (f64.sqrt of a negative, f64.min/max with a NaN operand) leaves
69
+ // the sign bit nondeterministic — x86 yields the negative NaN 0xFFF8.., ARM
70
+ // the positive 0x7FF8... jz's carrier reserves 0x7FF8.. as THE number-NaN;
71
+ // a negative-NaN number is bit-identical to a negative BigInt and corrupts
72
+ // untyped === / typeof. So fold any NaN back to canonical where one is born.
73
+ const canon = (node) => {
74
+ const t = temp('cn')
75
+ return typed(['block', ['result', 'f64'],
76
+ ['local.set', `$${t}`, node],
77
+ ['select',
78
+ ['f64.const', 'nan'],
79
+ ['local.get', `$${t}`],
80
+ ['f64.ne', ['local.get', `$${t}`], ['local.get', `$${t}`]]]], 'f64')
81
+ }
33
82
 
34
83
  // Constants
35
84
  ctx.core.emit['math.PI'] = () => typed(['f64.const', Math.PI], 'f64')
@@ -53,101 +102,132 @@ export default (ctx) => {
53
102
  ['local.get', `$${acc}`]], 'f64')
54
103
  }
55
104
 
56
- // Built-in WASM ops
57
- ctx.core.emit['math.sqrt'] = a => f('f64.sqrt', a)
105
+ // Built-in WASM ops. sqrt/min/max mint a fresh NaN (sqrt of a negative, min/max
106
+ // with a NaN operand) whose sign is platform-nondeterministic — `canon` folds it
107
+ // back to the canonical pattern. abs/floor/ceil/trunc never produce a new NaN.
108
+ ctx.core.emit['math.sqrt'] = a => canon(f('f64.sqrt', a))
58
109
  ctx.core.emit['math.abs'] = a => f('f64.abs', a)
59
110
  ctx.core.emit['math.floor'] = a => fInt('f64.floor', a)
60
111
  ctx.core.emit['math.ceil'] = a => fInt('f64.ceil', a)
61
112
  ctx.core.emit['math.trunc'] = a => fInt('f64.trunc', a)
62
- ctx.core.emit['math.min'] = (a, b, ...rest) => {
63
- if (a === undefined) return typed(['f64.const', Infinity], 'f64')
64
- // Spread: Math.min(...arr) iterate array to find min
65
- if (!b && Array.isArray(a) && a[0] === '...') return emitArrayReduce('f64.min', a[1], Infinity)
66
- if (b === undefined) return typed(['f64.min', asF64(emit(a)), ['f64.const', Infinity]], 'f64')
67
- let r = f2('f64.min', a, b)
68
- for (const x of rest) r = typed(['f64.min', r, asF64(emit(x))], 'f64')
69
- return r
113
+ // Math.min/max fold their operands with a wasm op. f64.min/max PROPAGATE a
114
+ // NaN but never MINT one, so `canon` is needed only when an operand could
115
+ // itself be NaN. An operand provably never is when it's an intCertain local/
116
+ // slot, a non-NaN numeric literal, or an i32-typed carrier (`x|0`, compares,
117
+ // lengths). When every operand qualifies, drop `canon` erasing its cost
118
+ // from the common integer-clamp idiom Math.min(idx, len) / Math.max(x|0, lo).
119
+ const neverNaN = (src, v) =>
120
+ isIntCertain(src) || (typeof src === 'number' && src === src) ||
121
+ (v.type === 'i32' && v.ptrKind == null) || (isLit(v) && litVal(v) === litVal(v))
122
+ const minmax = (op, ident) => (a, b, ...rest) => {
123
+ if (a === undefined) return typed(['f64.const', ident], 'f64')
124
+ // Spread: Math.min(...arr) — array contents unknown, keep canon
125
+ if (!b && Array.isArray(a) && a[0] === '...') return canon(emitArrayReduce(op, a[1], ident))
126
+ const src = b === undefined ? [a] : [a, b, ...rest]
127
+ const ev = src.map(x => emit(x))
128
+ let r = typed([op, toNumF64(src[0], ev[0]),
129
+ b === undefined ? ['f64.const', ident] : toNumF64(src[1], ev[1])], 'f64')
130
+ for (let i = 2; i < src.length; i++) r = typed([op, r, toNumF64(src[i], ev[i])], 'f64')
131
+ return src.every((s, i) => neverNaN(s, ev[i])) ? r : canon(r)
70
132
  }
71
- ctx.core.emit['math.max'] = (a, b, ...rest) => {
72
- if (a === undefined) return typed(['f64.const', -Infinity], 'f64')
73
- if (!b && Array.isArray(a) && a[0] === '...') return emitArrayReduce('f64.max', a[1], -Infinity)
74
- if (b === undefined) return typed(['f64.max', asF64(emit(a)), ['f64.const', -Infinity]], 'f64')
75
- let r = f2('f64.max', a, b)
76
- for (const x of rest) r = typed(['f64.max', r, asF64(emit(x))], 'f64')
77
- return r
133
+ ctx.core.emit['math.min'] = minmax('f64.min', Infinity)
134
+ ctx.core.emit['math.max'] = minmax('f64.max', -Infinity)
135
+ // f64.nearest is roundTiesToEven; JS Math.round is roundTiesToward+∞. They agree
136
+ // everywhere except exact half-integers n+0.5 with n even (nearest→n, JS→n+1).
137
+ // Detect that one case — `nearest(x) === x - 0.5` and bump by one. (The −0.5→−0
138
+ // and 0.49999…94→0 edges already match `f64.nearest`.)
139
+ ctx.core.emit['math.round'] = a => {
140
+ if (isIntCertain(a)) return asF64(emit(a))
141
+ const t = temp('rnd'), n = temp('rnd')
142
+ return typed(['block', ['result', 'f64'],
143
+ ['local.set', `$${t}`, toNumF64(a, emit(a))],
144
+ ['local.set', `$${n}`, ['f64.nearest', ['local.get', `$${t}`]]],
145
+ ['select',
146
+ ['f64.add', ['local.get', `$${n}`], ['f64.const', 1]],
147
+ ['local.get', `$${n}`],
148
+ ['f64.eq', ['local.get', `$${n}`], ['f64.sub', ['local.get', `$${t}`], ['f64.const', 0.5]]]],
149
+ ], 'f64')
78
150
  }
79
- ctx.core.emit['math.round'] = a => fInt('f64.nearest', a)
80
- ctx.core.emit['math.fround'] = a => typed(['f64.promote_f32', ['f32.demote_f64', asF64(emit(a))]], 'f64')
151
+ ctx.core.emit['math.fround'] = a => typed(['f64.promote_f32', ['f32.demote_f64', toNumF64(a, emit(a))]], 'f64')
81
152
 
82
153
  // Sign
83
- ctx.core.emit['math.sign'] = a => call('math.sign', a)
154
+ ctx.core.emit['math.sign'] = emitter(['math.sign'], a => call('math.sign', a))
84
155
 
85
156
  // Trig
86
- ctx.core.emit['math.sin'] = a => call('math.sin', a)
87
- ctx.core.emit['math.cos'] = a => call('math.cos', a)
88
- ctx.core.emit['math.tan'] = a => callDeps(['math.sin', 'math.cos', 'math.tan'], 'math.tan', a)
157
+ ctx.core.emit['math.sin'] = emitter(['math.sin'], a => call('math.sin', a))
158
+ ctx.core.emit['math.cos'] = emitter(['math.cos'], a => call('math.cos', a))
159
+ ctx.core.emit['math.tan'] = emitter(['math.tan'], a => call('math.tan', a))
89
160
 
90
161
  // Inverse trig
91
- ctx.core.emit['math.asin'] = a => callDeps(['math.atan', 'math.asin'], 'math.asin', a)
92
- ctx.core.emit['math.acos'] = a => callDeps(['math.atan', 'math.asin', 'math.acos'], 'math.acos', a)
93
- ctx.core.emit['math.atan'] = a => call('math.atan', a)
94
- ctx.core.emit['math.atan2'] = (a, b) => callDeps(['math.atan', 'math.atan2'], 'math.atan2', a, b)
162
+ ctx.core.emit['math.asin'] = emitter(['math.asin'], a => call('math.asin', a))
163
+ ctx.core.emit['math.acos'] = emitter(['math.acos'], a => call('math.acos', a))
164
+ ctx.core.emit['math.atan'] = emitter(['math.atan'], a => call('math.atan', a))
165
+ ctx.core.emit['math.atan2'] = emitter(['math.atan2'], (a, b) => call('math.atan2', a, b))
95
166
 
96
167
  // Hyperbolic
97
- ctx.core.emit['math.sinh'] = a => callDeps(['math.exp', 'math.sinh'], 'math.sinh', a)
98
- ctx.core.emit['math.cosh'] = a => callDeps(['math.exp', 'math.cosh'], 'math.cosh', a)
99
- ctx.core.emit['math.tanh'] = a => callDeps(['math.exp', 'math.tanh'], 'math.tanh', a)
168
+ ctx.core.emit['math.sinh'] = emitter(['math.sinh'], a => call('math.sinh', a))
169
+ ctx.core.emit['math.cosh'] = emitter(['math.cosh'], a => call('math.cosh', a))
170
+ ctx.core.emit['math.tanh'] = emitter(['math.tanh'], a => call('math.tanh', a))
100
171
 
101
172
  // Inverse hyperbolic
102
- ctx.core.emit['math.asinh'] = a => callDeps(['math.log', 'math.asinh'], 'math.asinh', a)
103
- ctx.core.emit['math.acosh'] = a => callDeps(['math.log', 'math.acosh'], 'math.acosh', a)
104
- ctx.core.emit['math.atanh'] = a => callDeps(['math.log', 'math.atanh'], 'math.atanh', a)
173
+ ctx.core.emit['math.asinh'] = emitter(['math.asinh'], a => call('math.asinh', a))
174
+ ctx.core.emit['math.acosh'] = emitter(['math.acosh'], a => call('math.acosh', a))
175
+ ctx.core.emit['math.atanh'] = emitter(['math.atanh'], a => call('math.atanh', a))
105
176
 
106
177
  // Exponential and logarithmic
107
- ctx.core.emit['math.exp'] = a => call('math.exp', a)
108
- ctx.core.emit['math.expm1'] = a => callDeps(['math.exp', 'math.expm1'], 'math.expm1', a)
109
- ctx.core.emit['math.log'] = a => call('math.log', a)
110
- ctx.core.emit['math.log2'] = a => callDeps(['math.log', 'math.log2'], 'math.log2', a)
111
- ctx.core.emit['math.log10'] = a => callDeps(['math.log', 'math.log10'], 'math.log10', a)
112
- ctx.core.emit['math.log1p'] = a => callDeps(['math.log', 'math.log1p'], 'math.log1p', a)
178
+ ctx.core.emit['math.exp'] = emitter(['math.exp'], a => call('math.exp', a))
179
+ ctx.core.emit['math.expm1'] = emitter(['math.expm1'], a => call('math.expm1', a))
180
+ ctx.core.emit['math.log'] = emitter(['math.log'], a => call('math.log', a))
181
+ ctx.core.emit['math.log2'] = emitter(['math.log2'], a => call('math.log2', a))
182
+ ctx.core.emit['math.log10'] = emitter(['math.log10'], a => call('math.log10', a))
183
+ ctx.core.emit['math.log1p'] = emitter(['math.log1p'], a => call('math.log1p', a))
113
184
 
114
185
  // Power
115
- ctx.core.emit['math.pow'] = (a, b) => callDeps(['math.exp', 'math.log', 'math.pow'], 'math.pow', a, b)
186
+ ctx.core.emit['math.pow'] = emitter(['math.pow'], (a, b) => call('math.pow', a, b))
116
187
  ctx.core.emit['**'] = ctx.core.emit['math.pow']
117
- ctx.core.emit['math.cbrt'] = a => callDeps(['math.exp', 'math.log', 'math.pow', 'math.cbrt'], 'math.cbrt', a)
118
- ctx.core.emit['math.hypot'] = (a, b, ...rest) => {
188
+ ctx.core.emit['math.cbrt'] = emitter(['math.cbrt'], a => call('math.cbrt', a))
189
+ ctx.core.emit['math.hypot'] = emitter(['math.hypot'], (a, b, ...rest) => {
119
190
  if (a === undefined) return typed(['f64.const', 0], 'f64')
120
191
  if (b === undefined) return f('f64.abs', a)
121
192
  let r = call('math.hypot', a, b)
122
- for (const x of rest) {
123
- inc('math.hypot')
124
- r = typed(['call', '$math.hypot', r, asF64(emit(x))], 'f64')
125
- }
193
+ // ToNumber every rest arg too (matches min/max) — an object arg's valueOf
194
+ // must run and may throw, which Math.hypot propagates.
195
+ for (const x of rest) r = typed(['call', '$math.hypot', r, toNumF64(x, emit(x))], 'f64')
126
196
  return r
127
- }
197
+ })
198
+
199
+ // Math.sumPrecise(iterable) — exact, correctly-rounded summation (ECMA-262).
200
+ // jz models the array case; the WAT routine sums via a fixed-point accumulator.
201
+ ctx.core.emit['math.sumPrecise'] = emitter(['math.sumPrecise'], arr =>
202
+ typed(['call', '$math.sumPrecise', ['i64.reinterpret_f64', asF64(emit(arr))]], 'f64'))
128
203
 
129
204
  // Integer/bit operations: return i32 directly. Consumers `asF64`-rebox at
130
205
  // store/return boundaries; consumers staying in i32 (bit chains, i32 locals)
131
206
  // skip the convert/trunc round-trip entirely.
132
- ctx.core.emit['math.clz32'] = a => typed(['i32.clz', asI32(emit(a))], 'i32')
133
- ctx.core.emit['math.imul'] = (a, b) => typed(['i32.mul', asI32(emit(a)), asI32(emit(b))], 'i32')
207
+ // Operands take ECMAScript ToInt32 (wrapping), not saturation — `Math.imul(x, k)`
208
+ // with a literal k 2³¹ must wrap to negative, matching JS, not clamp to INT_MAX.
209
+ ctx.core.emit['math.clz32'] = a => typed(['i32.clz', toI32(emit(a))], 'i32')
210
+ ctx.core.emit['math.imul'] = (a, b) => typed(['i32.mul', toI32(emit(a)), toI32(emit(b))], 'i32')
134
211
 
135
212
  // Random
136
- ctx.core.emit['math.random'] = () => (inc('math.random'), typed(['call', '$math.random'], 'f64'))
213
+ ctx.core.emit['math.random'] = emitter(['math.random'], () => typed(['call', '$math.random'], 'f64'))
137
214
 
138
215
  // ============================================
139
216
  // WAT stdlib implementations
140
217
  // ============================================
141
218
 
142
219
  ctx.core.stdlib['math.sign'] = `(func $math.sign (param $x f64) (result f64)
143
- (if (result f64) (f64.eq (local.get $x) (f64.const 0.0))
144
- (then (f64.const 0.0))
145
- (else (if (result f64) (f64.gt (local.get $x) (f64.const 0.0))
146
- (then (f64.const 1.0))
147
- (else (f64.const -1.0))))))`
220
+ ;; sign(NaN) = NaN, sign(±0) = ±0 — both pass x through unchanged.
221
+ (if (f64.ne (local.get $x) (local.get $x)) (then (return (local.get $x))))
222
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
223
+ (if (result f64) (f64.gt (local.get $x) (f64.const 0.0))
224
+ (then (f64.const 1.0))
225
+ (else (f64.const -1.0))))`
148
226
 
149
227
  ctx.core.stdlib['math.sin'] = `(func $math.sin (param $x f64) (result f64)
150
228
  (local $n i32) (local $r f64) (local $x2 f64) (local $sign f64)
229
+ ;; NaN/±Infinity → NaN (avoid trapping i32.trunc on infinities).
230
+ (if (i32.eqz (call $math.isFinite (local.get $x))) (then (return (f64.const nan))))
151
231
  (local.set $sign (f64.const 1.0))
152
232
  (local.set $n (i32.trunc_f64_s (f64.floor (f64.div (local.get $x) (f64.const ${Math.PI})))))
153
233
  (local.set $r (f64.sub (local.get $x) (f64.mul (f64.convert_i32_s (local.get $n)) (f64.const ${Math.PI}))))
@@ -166,6 +246,8 @@ export default (ctx) => {
166
246
 
167
247
  ctx.core.stdlib['math.cos'] = `(func $math.cos (param $x f64) (result f64)
168
248
  (local $n i32) (local $r f64) (local $x2 f64) (local $sign f64)
249
+ ;; NaN/±Infinity → NaN (avoid trapping i32.trunc on infinities).
250
+ (if (i32.eqz (call $math.isFinite (local.get $x))) (then (return (f64.const nan))))
169
251
  (local.set $sign (f64.const 1.0))
170
252
  (local.set $n (i32.trunc_f64_s (f64.floor (f64.div (local.get $x) (f64.const ${Math.PI})))))
171
253
  (local.set $r (f64.sub (local.get $x) (f64.mul (f64.convert_i32_s (local.get $n)) (f64.const ${Math.PI}))))
@@ -188,7 +270,8 @@ export default (ctx) => {
188
270
  ctx.core.stdlib['math.exp'] = `(func $math.exp (param $x f64) (result f64)
189
271
  (local $k i32) (local $t f64) (local $t2 f64) (local $result f64) (local $pow2 f64)
190
272
  (if (f64.ne (local.get $x) (local.get $x)) (then (return (local.get $x))))
191
- (if (result f64) (f64.gt (local.get $x) (f64.const 709.0)) (then (f64.const 1.7976931348623157e+308)) (else
273
+ ;; +Infinity +Infinity; finite overflow (x > 709) also rounds to +Infinity.
274
+ (if (result f64) (f64.gt (local.get $x) (f64.const 709.0)) (then (f64.const inf)) (else
192
275
  (if (result f64) (f64.lt (local.get $x) (f64.const -745.0)) (then (f64.const 0.0)) (else
193
276
  (local.set $k (i32.trunc_f64_s (f64.div (local.get $x) (f64.const ${Math.LN2}))))
194
277
  (local.set $t (f64.sub (local.get $x) (f64.mul (f64.convert_i32_s (local.get $k)) (f64.const ${Math.LN2}))))
@@ -217,34 +300,54 @@ export default (ctx) => {
217
300
  (local.get $result))))))`
218
301
 
219
302
  ctx.core.stdlib['math.expm1'] = `(func $math.expm1 (param $x f64) (result f64)
303
+ ;; Preserve sign of zero: expm1(±0) = ±0.
304
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
220
305
  (f64.sub (call $math.exp (local.get $x)) (f64.const 1.0)))`
221
306
 
307
+ // log(x) via bit-level frexp + sqrt(2)-centered split + atanh series.
308
+ // x = m * 2^k with bits-extracted k (no loop)
309
+ // if m >= sqrt(2): m /= 2, k += 1 so m ∈ [sqrt(2)/2, sqrt(2)) ≈ [0.707, 1.414)
310
+ // s = (m-1)/(m+1) |s| ≤ 0.172
311
+ // log(x) = k·ln(2) + 2s·(1 + s²/3 + s⁴/5 + ... + s¹⁶/17)
312
+ // With 9 polynomial terms and |s|≤0.172, truncation error ≈ 2|s|·z⁹/19 ≈ 4e-17,
313
+ // close to f64 ulp. The whole routine is branchless after edge cases.
314
+ // Edge cases: NaN→NaN, ≤0 distinguishes 0→-Inf, <0→NaN; +Inf passes through.
222
315
  ctx.core.stdlib['math.log'] = `(func $math.log (param $x f64) (result f64)
223
- (local $k i32) (local $y f64) (local $s f64) (local $z f64)
316
+ (local $bits i64) (local $k i32) (local $m f64) (local $s f64) (local $z f64)
224
317
  (if (f64.ne (local.get $x) (local.get $x))
225
318
  (then (return (local.get $x))))
226
319
  (if (f64.le (local.get $x) (f64.const 0.0))
227
- (then (return (f64.const 0.0))))
228
- (if (f64.eq (local.get $x) (f64.const 1.0))
229
- (then (return (f64.const 0.0))))
320
+ (then
321
+ (if (f64.eq (local.get $x) (f64.const 0.0))
322
+ (then (return (f64.const -inf))))
323
+ (return (f64.const nan))))
230
324
  (if (f64.eq (local.get $x) (f64.const inf))
231
325
  (then (return (local.get $x))))
232
326
  (local.set $k (i32.const 0))
233
- (local.set $y (local.get $x))
234
- (block $done_up
235
- (loop $scale_up
236
- (br_if $done_up (f64.lt (local.get $y) (f64.const 2.0)))
237
- (local.set $y (f64.mul (local.get $y) (f64.const 0.5)))
238
- (local.set $k (i32.add (local.get $k) (i32.const 1)))
239
- (br $scale_up)))
240
- (block $done_down
241
- (loop $scale_down
242
- (br_if $done_down (f64.ge (local.get $y) (f64.const 1.0)))
243
- (local.set $y (f64.mul (local.get $y) (f64.const 2.0)))
244
- (local.set $k (i32.sub (local.get $k) (i32.const 1)))
245
- (br $scale_down)))
246
- (local.set $s (f64.div (f64.sub (local.get $y) (f64.const 1.0)) (f64.add (local.get $y) (f64.const 1.0))))
327
+ ;; Normalize denormals (exponent=0): scale by 2^54 and remember the shift,
328
+ ;; so the bit-extracted exponent below is meaningful for every finite x > 0.
329
+ (if (f64.lt (local.get $x) (f64.const 0x1p-1022))
330
+ (then
331
+ (local.set $x (f64.mul (local.get $x) (f64.const 0x1p54)))
332
+ (local.set $k (i32.const -54))))
333
+ ;; frexp via bit twiddling: k = ((bits >> 52) & 0x7ff) - 1023, then force exp=1023 so m ∈ [1,2).
334
+ (local.set $bits (i64.reinterpret_f64 (local.get $x)))
335
+ (local.set $k (i32.add (local.get $k) (i32.sub
336
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 52)) (i64.const 0x7ff)))
337
+ (i32.const 1023))))
338
+ (local.set $m (f64.reinterpret_i64
339
+ (i64.or
340
+ (i64.and (local.get $bits) (i64.const 0x000fffffffffffff))
341
+ (i64.const 0x3ff0000000000000))))
342
+ ;; Center on sqrt(2) to shrink |s| from 1/3 down to ~0.172.
343
+ (if (f64.ge (local.get $m) (f64.const 1.4142135623730951))
344
+ (then
345
+ (local.set $m (f64.mul (local.get $m) (f64.const 0.5)))
346
+ (local.set $k (i32.add (local.get $k) (i32.const 1)))))
347
+ (local.set $s (f64.div (f64.sub (local.get $m) (f64.const 1.0))
348
+ (f64.add (local.get $m) (f64.const 1.0))))
247
349
  (local.set $z (f64.mul (local.get $s) (local.get $s)))
350
+ ;; Horner: 1 + z/3 + z²/5 + z³/7 + z⁴/9 + z⁵/11 + z⁶/13 + z⁷/15 + z⁸/17
248
351
  (f64.add
249
352
  (f64.mul (f64.convert_i32_s (local.get $k)) (f64.const ${Math.LN2}))
250
353
  (f64.mul (f64.const 2.0) (f64.mul (local.get $s) (f64.add (f64.const 1.0)
@@ -252,16 +355,99 @@ export default (ctx) => {
252
355
  (f64.mul (local.get $z) (f64.add (f64.const 0.2)
253
356
  (f64.mul (local.get $z) (f64.add (f64.const 0.14285714285714285)
254
357
  (f64.mul (local.get $z) (f64.add (f64.const 0.1111111111111111)
255
- (f64.mul (local.get $z) (f64.const 0.09090909090909091)))))))))))))))`
358
+ (f64.mul (local.get $z) (f64.add (f64.const 0.09090909090909091)
359
+ (f64.mul (local.get $z) (f64.add (f64.const 0.07692307692307693)
360
+ (f64.mul (local.get $z) (f64.add (f64.const 0.06666666666666667)
361
+ (f64.mul (local.get $z) (f64.const 0.058823529411764705)))))))))))))))))))))`
256
362
 
257
363
  ctx.core.stdlib['math.log2'] = `(func $math.log2 (param $x f64) (result f64)
258
364
  (f64.div (call $math.log (local.get $x)) (f64.const ${Math.LN2})))`
259
365
 
366
+ // log10 via fdlibm's two-term decomposition: log10(x) = k*log10(2) + log10(m).
367
+ // A plain log(x)/ln(10) double-rounds (rounding of log itself, then of the
368
+ // divide), so exact powers of ten drift — log10(1000) lands on 2.9999…996.
369
+ // Reducing x = m·2^k, splitting log10(2) and 1/ln(10) into hi/lo halves, and
370
+ // keeping the bulk term (k·log10_2hi, hi·ivln10hi) carry-free recovers the
371
+ // last ulps, so log10(10/100/1000/…) round-trips to exact integers.
260
372
  ctx.core.stdlib['math.log10'] = `(func $math.log10 (param $x f64) (result f64)
261
- (f64.div (call $math.log (local.get $x)) (f64.const ${Math.LN10})))`
262
-
373
+ (local $bits i64) (local $k i32) (local $m f64) (local $f f64)
374
+ (local $hfsq f64) (local $s f64) (local $z f64) (local $w f64)
375
+ (local $t1 f64) (local $t2 f64) (local $R f64)
376
+ (local $hi f64) (local $lo f64) (local $dk f64)
377
+ (local $valhi f64) (local $vallo f64) (local $y f64)
378
+ ;; Special values: NaN→NaN, x≤0 → (-inf for 0, NaN for negative), +inf→+inf.
379
+ (if (f64.ne (local.get $x) (local.get $x)) (then (return (local.get $x))))
380
+ (if (f64.le (local.get $x) (f64.const 0.0))
381
+ (then
382
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (f64.const -inf))))
383
+ (return (f64.const nan))))
384
+ (if (f64.eq (local.get $x) (f64.const inf)) (then (return (local.get $x))))
385
+ ;; Normalize subnormals so the bit-extracted exponent is meaningful.
386
+ (local.set $k (i32.const 0))
387
+ (if (f64.lt (local.get $x) (f64.const 0x1p-1022))
388
+ (then
389
+ (local.set $x (f64.mul (local.get $x) (f64.const 0x1p54)))
390
+ (local.set $k (i32.const -54))))
391
+ ;; frexp: k += exponent, m = mantissa forced into [1,2).
392
+ (local.set $bits (i64.reinterpret_f64 (local.get $x)))
393
+ (local.set $k (i32.add (local.get $k) (i32.sub
394
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 52)) (i64.const 0x7ff)))
395
+ (i32.const 1023))))
396
+ (local.set $m (f64.reinterpret_i64
397
+ (i64.or (i64.and (local.get $bits) (i64.const 0x000fffffffffffff))
398
+ (i64.const 0x3ff0000000000000))))
399
+ ;; Center on sqrt(2): m ∈ [sqrt2/2, sqrt2) keeps the kernel argument small.
400
+ (if (f64.ge (local.get $m) (f64.const 1.4142135623730951))
401
+ (then
402
+ (local.set $m (f64.mul (local.get $m) (f64.const 0.5)))
403
+ (local.set $k (i32.add (local.get $k) (i32.const 1)))))
404
+ ;; log(m) kernel: f - hfsq + s*(hfsq+R), s = f/(2+f), polynomial in s².
405
+ (local.set $f (f64.sub (local.get $m) (f64.const 1.0)))
406
+ (local.set $hfsq (f64.mul (f64.const 0.5) (f64.mul (local.get $f) (local.get $f))))
407
+ (local.set $s (f64.div (local.get $f) (f64.add (f64.const 2.0) (local.get $f))))
408
+ (local.set $z (f64.mul (local.get $s) (local.get $s)))
409
+ (local.set $w (f64.mul (local.get $z) (local.get $z)))
410
+ (local.set $t1 (f64.mul (local.get $w) (f64.add (f64.const 0.3999999999940942)
411
+ (f64.mul (local.get $w) (f64.add (f64.const 0.22222198432149792)
412
+ (f64.mul (local.get $w) (f64.const 0.15313837699209373)))))))
413
+ (local.set $t2 (f64.mul (local.get $z) (f64.add (f64.const 0.6666666666666735)
414
+ (f64.mul (local.get $w) (f64.add (f64.const 0.2857142874366239)
415
+ (f64.mul (local.get $w) (f64.add (f64.const 0.1818357216161805)
416
+ (f64.mul (local.get $w) (f64.const 0.14798198605116586)))))))))
417
+ (local.set $R (f64.add (local.get $t2) (local.get $t1)))
418
+ ;; hi = high 32 bits of (f - hfsq); lo = the carry-free remainder.
419
+ (local.set $hi (f64.sub (local.get $f) (local.get $hfsq)))
420
+ (local.set $hi (f64.reinterpret_i64
421
+ (i64.and (i64.reinterpret_f64 (local.get $hi)) (i64.const 0xffffffff00000000))))
422
+ (local.set $lo (f64.add
423
+ (f64.sub (f64.sub (local.get $f) (local.get $hi)) (local.get $hfsq))
424
+ (f64.mul (local.get $s) (f64.add (local.get $hfsq) (local.get $R)))))
425
+ ;; Combine with k·log10(2): bulk in val_hi, corrections in val_lo.
426
+ (local.set $valhi (f64.mul (local.get $hi) (f64.const 0.4342944818781689)))
427
+ (local.set $dk (f64.convert_i32_s (local.get $k)))
428
+ (local.set $y (f64.mul (local.get $dk) (f64.const 0.30102999566361177)))
429
+ (local.set $vallo (f64.add (f64.add
430
+ (f64.mul (local.get $dk) (f64.const 3.694239077158931e-13))
431
+ (f64.mul (f64.add (local.get $lo) (local.get $hi)) (f64.const 2.5082946711645275e-11)))
432
+ (f64.mul (local.get $lo) (f64.const 0.4342944818781689))))
433
+ (local.set $w (f64.add (local.get $y) (local.get $valhi)))
434
+ (local.set $vallo (f64.add (local.get $vallo)
435
+ (f64.add (f64.sub (local.get $y) (local.get $w)) (local.get $valhi))))
436
+ (f64.add (local.get $vallo) (local.get $w)))`
437
+
438
+ // log1p(x) via Kahan's compensated trick: with u = 1+x, log(u) loses bits when x is
439
+ // small (because u rounds to ~1), but the ratio x/(u-1) is exactly the missing factor.
440
+ // For u==1 (x below ulp), result is just x; preserves -0 from x=-0 path.
263
441
  ctx.core.stdlib['math.log1p'] = `(func $math.log1p (param $x f64) (result f64)
264
- (call $math.log (f64.add (f64.const 1.0) (local.get $x))))`
442
+ (local $u f64)
443
+ ;; log1p(+Inf) = +Inf — the ratio trick below would compute Inf/Inf = NaN.
444
+ (if (f64.eq (local.get $x) (f64.const inf)) (then (return (f64.const inf))))
445
+ (local.set $u (f64.add (f64.const 1.0) (local.get $x)))
446
+ (if (f64.eq (local.get $u) (f64.const 1.0))
447
+ (then (return (local.get $x))))
448
+ (f64.div
449
+ (f64.mul (call $math.log (local.get $u)) (local.get $x))
450
+ (f64.sub (local.get $u) (f64.const 1.0))))`
265
451
 
266
452
  ctx.core.stdlib['math.pow'] = `(func $math.pow (param $x f64) (param $y f64) (result f64)
267
453
  (local $result f64) (local $n i32) (local $neg_base i32) (local $abs_x f64)
@@ -276,7 +462,7 @@ export default (ctx) => {
276
462
  (then
277
463
  (local.set $abs_x (f64.abs (local.get $x)))
278
464
  (if (f64.eq (local.get $abs_x) (f64.const 1.0))
279
- (then (return (f64.div (f64.const 0.0) (f64.const 0.0)))))
465
+ (then (return (f64.const nan))))
280
466
  (if (i32.eq (f64.gt (local.get $abs_x) (f64.const 1.0))
281
467
  (f64.gt (local.get $y) (f64.const 0.0)))
282
468
  (then (return (f64.const inf)))
@@ -313,6 +499,20 @@ export default (ctx) => {
313
499
  (if (local.get $neg_base)
314
500
  (then (local.set $result (f64.neg (local.get $result)))))
315
501
  (return (local.get $result))))
502
+ ;; x is ±Infinity with |y| >= 2^31 (the i32 fast path above handles smaller y):
503
+ ;; magnitude is Inf for y>0, 0 for y<0; sign is negative only when x is -Inf
504
+ ;; and y is an odd integer. Odd-ness is tested in f64 (y, y/2 both integral)
505
+ ;; to avoid an i32.trunc trap on |y| beyond i32 range.
506
+ (if (f64.eq (f64.abs (local.get $x)) (f64.const inf))
507
+ (then
508
+ (local.set $result
509
+ (select (f64.const inf) (f64.const 0.0) (f64.gt (local.get $y) (f64.const 0.0))))
510
+ (if (i32.and (f64.lt (local.get $x) (f64.const 0.0))
511
+ (i32.and (f64.eq (f64.nearest (local.get $y)) (local.get $y))
512
+ (f64.ne (f64.nearest (f64.mul (local.get $y) (f64.const 0.5)))
513
+ (f64.mul (local.get $y) (f64.const 0.5)))))
514
+ (then (local.set $result (f64.neg (local.get $result)))))
515
+ (return (local.get $result))))
316
516
  ;; x == 0 with non-integer y -> y<0 ? Infinity : 0 (sign-of-zero only matters for integer y, handled above)
317
517
  (if (f64.eq (local.get $x) (f64.const 0.0))
318
518
  (then
@@ -321,41 +521,85 @@ export default (ctx) => {
321
521
  (else (return (f64.const 0.0))))))
322
522
  ;; x < 0, non-integer finite y -> NaN
323
523
  (if (f64.lt (local.get $x) (f64.const 0.0))
324
- (then (return (f64.div (f64.const 0.0) (f64.const 0.0)))))
524
+ (then (return (f64.const nan))))
325
525
  (call $math.exp (f64.mul (local.get $y) (call $math.log (local.get $x)))))`
326
526
 
527
+ // fdlibm atan: 4-region argument reduction onto |r| ≤ tan(π/16), then an
528
+ // 11-term odd polynomial split into even/odd parts. Accurate to <1 ulp —
529
+ // the old Taylor series was ~2e-6 off near |x|=0.5. Drives asin/acos/atan2.
327
530
  ctx.core.stdlib['math.atan'] = `(func $math.atan (param $x f64) (result f64)
328
- (local $x2 f64) (local $abs_x f64) (local $reduced f64)
531
+ (local $abs_x f64) (local $id i32) (local $r f64) (local $z f64) (local $w f64)
532
+ (local $s1 f64) (local $s2 f64) (local $ahi f64) (local $alo f64) (local $res f64)
533
+ ;; NaN passes through unchanged.
534
+ (if (f64.ne (local.get $x) (local.get $x)) (then (return (local.get $x))))
329
535
  (local.set $abs_x (f64.abs (local.get $x)))
330
- (if (result f64) (f64.gt (local.get $abs_x) (f64.const 1.0))
536
+ ;; |x| >= 2^66: atan saturates to ±π/2.
537
+ (if (f64.ge (local.get $abs_x) (f64.const 7.378697629483821e19))
538
+ (then (return (f64.copysign (f64.const 1.5707963267948966) (local.get $x)))))
539
+ (if (f64.lt (local.get $abs_x) (f64.const 0.4375))
331
540
  (then
332
- (if (result f64) (f64.gt (local.get $x) (f64.const 0.0))
333
- (then (f64.sub (f64.const ${Math.PI / 2}) (call $math.atan (f64.div (f64.const 1.0) (local.get $x)))))
334
- (else (f64.add (f64.neg (f64.const ${Math.PI / 2})) (call $math.atan (f64.div (f64.const 1.0) (local.get $x)))))))
541
+ ;; |x| < 2^-27: atan(x) ≈ x (also preserves sign of zero).
542
+ (if (f64.lt (local.get $abs_x) (f64.const 7.450580596923828e-9))
543
+ (then (return (local.get $x))))
544
+ (local.set $id (i32.const -1))
545
+ (local.set $r (local.get $x)))
335
546
  (else
336
- (if (result f64) (f64.gt (local.get $abs_x) (f64.const 0.5))
547
+ (local.set $r (local.get $abs_x))
548
+ (if (f64.lt (local.get $abs_x) (f64.const 1.1875))
337
549
  (then
338
- (local.set $reduced (f64.div (local.get $x) (f64.add (f64.const 1.0) (f64.sqrt (f64.add (f64.const 1.0) (f64.mul (local.get $x) (local.get $x)))))))
339
- (f64.mul (f64.const 2.0) (call $math.atan (local.get $reduced))))
550
+ (if (f64.lt (local.get $abs_x) (f64.const 0.6875))
551
+ (then ;; id=0: r = (2x-1)/(2+x)
552
+ (local.set $id (i32.const 0))
553
+ (local.set $r (f64.div (f64.sub (f64.mul (f64.const 2.0) (local.get $r)) (f64.const 1.0))
554
+ (f64.add (f64.const 2.0) (local.get $r)))))
555
+ (else ;; id=1: r = (x-1)/(x+1)
556
+ (local.set $id (i32.const 1))
557
+ (local.set $r (f64.div (f64.sub (local.get $r) (f64.const 1.0))
558
+ (f64.add (local.get $r) (f64.const 1.0)))))))
340
559
  (else
341
- (local.set $x2 (f64.mul (local.get $x) (local.get $x)))
342
- (f64.mul (local.get $x)
343
- (f64.sub (f64.const 1.0)
344
- (f64.mul (local.get $x2)
345
- (f64.sub (f64.const 0.3333333333333333)
346
- (f64.mul (local.get $x2)
347
- (f64.sub (f64.const 0.2)
348
- (f64.mul (local.get $x2)
349
- (f64.sub (f64.const 0.14285714285714285)
350
- (f64.mul (local.get $x2)
351
- (f64.sub (f64.const 0.1111111111111111)
352
- (f64.mul (local.get $x2)
353
- (f64.sub (f64.const 0.09090909090909091)
354
- (f64.mul (local.get $x2) (f64.const 0.07692307692307693)))))))))))))))))))`
560
+ (if (f64.lt (local.get $abs_x) (f64.const 2.4375))
561
+ (then ;; id=2: r = (x-1.5)/(1+1.5x)
562
+ (local.set $id (i32.const 2))
563
+ (local.set $r (f64.div (f64.sub (local.get $r) (f64.const 1.5))
564
+ (f64.add (f64.const 1.0) (f64.mul (f64.const 1.5) (local.get $r))))))
565
+ (else ;; id=3: r = -1/x
566
+ (local.set $id (i32.const 3))
567
+ (local.set $r (f64.div (f64.const -1.0) (local.get $r)))))))))
568
+ (local.set $z (f64.mul (local.get $r) (local.get $r)))
569
+ (local.set $w (f64.mul (local.get $z) (local.get $z)))
570
+ (local.set $s1 (f64.mul (local.get $z)
571
+ (f64.add (f64.const 0.3333333333333293)
572
+ (f64.mul (local.get $w) (f64.add (f64.const 0.14285714272503466)
573
+ (f64.mul (local.get $w) (f64.add (f64.const 0.09090887133436507)
574
+ (f64.mul (local.get $w) (f64.add (f64.const 0.06661073137387531)
575
+ (f64.mul (local.get $w) (f64.add (f64.const 0.049768779946159324)
576
+ (f64.mul (local.get $w) (f64.const 0.016285820115365782)))))))))))))
577
+ (local.set $s2 (f64.mul (local.get $w)
578
+ (f64.add (f64.const -0.19999999999876483)
579
+ (f64.mul (local.get $w) (f64.add (f64.const -0.11111110405462356)
580
+ (f64.mul (local.get $w) (f64.add (f64.const -0.0769187620504483)
581
+ (f64.mul (local.get $w) (f64.add (f64.const -0.058335701337905735)
582
+ (f64.mul (local.get $w) (f64.const -0.036531572744216916)))))))))))
583
+ ;; |x| < 0.4375: result = r - r*(s1+s2), sign carried by r itself.
584
+ (if (i32.lt_s (local.get $id) (i32.const 0))
585
+ (then (return (f64.sub (local.get $r) (f64.mul (local.get $r) (f64.add (local.get $s1) (local.get $s2)))))))
586
+ ;; Reconstruct: z = atanhi[id] - ((r*(s1+s2) - atanlo[id]) - r), sign of x.
587
+ (if (i32.eq (local.get $id) (i32.const 0))
588
+ (then (local.set $ahi (f64.const 0.4636476090008061)) (local.set $alo (f64.const 2.2698777452961687e-17)))
589
+ (else (if (i32.eq (local.get $id) (i32.const 1))
590
+ (then (local.set $ahi (f64.const 0.7853981633974483)) (local.set $alo (f64.const 3.061616997868383e-17)))
591
+ (else (if (i32.eq (local.get $id) (i32.const 2))
592
+ (then (local.set $ahi (f64.const 0.982793723247329)) (local.set $alo (f64.const 1.3903311031230998e-17)))
593
+ (else (local.set $ahi (f64.const 1.5707963267948966)) (local.set $alo (f64.const 6.123233995736766e-17))))))))
594
+ (local.set $res (f64.sub (local.get $ahi)
595
+ (f64.sub (f64.sub (f64.mul (local.get $r) (f64.add (local.get $s1) (local.get $s2))) (local.get $alo))
596
+ (local.get $r))))
597
+ (f64.copysign (local.get $res) (local.get $x)))`
355
598
 
356
599
  ctx.core.stdlib['math.asin'] = `(func $math.asin (param $x f64) (result f64)
600
+ ;; Domain is [-1, 1]; outside it (including ±Infinity), Math.asin returns NaN.
357
601
  (if (result f64) (f64.gt (f64.abs (local.get $x)) (f64.const 1.0))
358
- (then (f64.const 0.0))
602
+ (then (f64.const nan))
359
603
  (else (call $math.atan (f64.div (local.get $x)
360
604
  (f64.sqrt (f64.sub (f64.const 1.0) (f64.mul (local.get $x) (local.get $x)))))))))`
361
605
 
@@ -363,9 +607,18 @@ export default (ctx) => {
363
607
  (f64.sub (f64.const ${Math.PI / 2}) (call $math.asin (local.get $x))))`
364
608
 
365
609
  ctx.core.stdlib['math.atan2'] = `(func $math.atan2 (param $y f64) (param $x f64) (result f64)
610
+ ;; If either argument is NaN, the result is NaN (ECMA-262 21.3.2.5).
611
+ (if (f64.ne (local.get $x) (local.get $x)) (then (return (local.get $x))))
612
+ (if (f64.ne (local.get $y) (local.get $y)) (then (return (local.get $y))))
366
613
  (if (result f64) (f64.eq (local.get $x) (f64.const 0.0)) (then
367
- (if (result f64) (f64.eq (local.get $y) (f64.const 0.0)) (then (f64.const 0.0)) (else
368
- (if (result f64) (f64.gt (local.get $y) (f64.const 0.0)) (then (f64.const ${Math.PI / 2})) (else (f64.neg (f64.const ${Math.PI / 2})))))))
614
+ ;; y is ±0 too: result is ±0 when x is +0, ±π when x is -0; sign taken from y.
615
+ (if (result f64) (f64.eq (local.get $y) (f64.const 0.0))
616
+ (then (f64.copysign
617
+ (select (f64.const ${Math.PI}) (f64.const 0.0)
618
+ (f64.lt (f64.copysign (f64.const 1.0) (local.get $x)) (f64.const 0.0)))
619
+ (local.get $y)))
620
+ (else
621
+ (if (result f64) (f64.gt (local.get $y) (f64.const 0.0)) (then (f64.const ${Math.PI / 2})) (else (f64.neg (f64.const ${Math.PI / 2})))))))
369
622
  (else (if (result f64) (f64.ge (local.get $x) (f64.const 0.0))
370
623
  (then (call $math.atan (f64.div (local.get $y) (local.get $x))))
371
624
  (else (if (result f64) (f64.ge (local.get $y) (f64.const 0.0))
@@ -374,6 +627,8 @@ export default (ctx) => {
374
627
 
375
628
  ctx.core.stdlib['math.sinh'] = `(func $math.sinh (param $x f64) (result f64)
376
629
  (local $ex f64)
630
+ ;; Preserve sign of zero: sinh(±0) = ±0 (the f64.lt sign test below is false for -0).
631
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
377
632
  (local.set $ex (call $math.exp (f64.abs (local.get $x))))
378
633
  (local.set $ex (f64.mul (f64.const 0.5) (f64.sub (local.get $ex) (f64.div (f64.const 1.0) (local.get $ex)))))
379
634
  (if (result f64) (f64.lt (local.get $x) (f64.const 0.0)) (then (f64.neg (local.get $ex))) (else (local.get $ex))))`
@@ -384,6 +639,8 @@ export default (ctx) => {
384
639
 
385
640
  ctx.core.stdlib['math.tanh'] = `(func $math.tanh (param $x f64) (result f64)
386
641
  (local $e2x f64)
642
+ ;; Preserve sign of zero: tanh(±0) = ±0 (the f64.lt sign test below is false for -0).
643
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
387
644
  (if (result f64) (f64.gt (f64.abs (local.get $x)) (f64.const 22.0))
388
645
  (then (if (result f64) (f64.lt (local.get $x) (f64.const 0.0)) (then (f64.const -1.0)) (else (f64.const 1.0))))
389
646
  (else (local.set $e2x (call $math.exp (f64.mul (f64.const 2.0) (f64.abs (local.get $x)))))
@@ -391,29 +648,51 @@ export default (ctx) => {
391
648
  (if (result f64) (f64.lt (local.get $x) (f64.const 0.0)) (then (f64.neg (local.get $e2x))) (else (local.get $e2x))))))`
392
649
 
393
650
  ctx.core.stdlib['math.asinh'] = `(func $math.asinh (param $x f64) (result f64)
651
+ ;; ±Infinity and NaN pass through unchanged. (log(±Inf + sqrt(Inf²+1)) → NaN otherwise.)
652
+ (if (i32.eqz (call $math.isFinite (local.get $x))) (then (return (local.get $x))))
653
+ ;; Preserve sign of zero: asinh(±0) = ±0.
654
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
394
655
  (call $math.log (f64.add (local.get $x) (f64.sqrt (f64.add (f64.mul (local.get $x) (local.get $x)) (f64.const 1.0))))))`
395
656
 
396
657
  ctx.core.stdlib['math.acosh'] = `(func $math.acosh (param $x f64) (result f64)
397
- (if (result f64) (f64.lt (local.get $x) (f64.const 1.0)) (then (f64.const 0.0)) (else
658
+ (if (f64.eq (local.get $x) (f64.const inf)) (then (return (f64.const inf))))
659
+ ;; acosh is defined only for x >= 1; everything below (incl. -Inf) is NaN.
660
+ (if (result f64) (f64.lt (local.get $x) (f64.const 1.0)) (then (f64.const nan)) (else
398
661
  (call $math.log (f64.add (local.get $x) (f64.sqrt (f64.sub (f64.mul (local.get $x) (local.get $x)) (f64.const 1.0))))))))`
399
662
 
400
663
  ctx.core.stdlib['math.atanh'] = `(func $math.atanh (param $x f64) (result f64)
664
+ ;; Preserve sign of zero: atanh(±0) = ±0.
665
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
666
+ ;; ±Infinity → NaN. Without this the (1+x)/(1-x) ratio is Inf/Inf, whose
667
+ ;; sign-nondeterministic arithmetic NaN would escape non-canonical on x86.
668
+ (if (f64.eq (f64.abs (local.get $x)) (f64.const inf)) (then (return (f64.const nan))))
401
669
  (f64.mul (f64.const 0.5) (call $math.log (f64.div (f64.add (f64.const 1.0) (local.get $x)) (f64.sub (f64.const 1.0) (local.get $x))))))`
402
670
 
403
671
  ctx.core.stdlib['math.cbrt'] = `(func $math.cbrt (param $x f64) (result f64)
404
672
  (local $y f64)
673
+ ;; ±Infinity and NaN pass through; preserve sign of zero.
674
+ (if (i32.eqz (call $math.isFinite (local.get $x))) (then (return (local.get $x))))
675
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
405
676
  (if (result f64) (f64.lt (local.get $x) (f64.const 0.0))
406
677
  (then (f64.neg (call $math.cbrt (f64.neg (local.get $x)))))
407
- (else (if (result f64) (f64.eq (local.get $x) (f64.const 0.0))
408
- (then (f64.const 0.0))
409
- (else
410
- ;; Initial guess via pow, then Newton-Raphson: y = (2y + x/y²)/3
411
- (local.set $y (call $math.pow (local.get $x) (f64.const 0.3333333333333333)))
412
- (local.set $y (f64.div (f64.add (f64.mul (f64.const 2.0) (local.get $y)) (f64.div (local.get $x) (f64.mul (local.get $y) (local.get $y)))) (f64.const 3.0)))
413
- (local.set $y (f64.div (f64.add (f64.mul (f64.const 2.0) (local.get $y)) (f64.div (local.get $x) (f64.mul (local.get $y) (local.get $y)))) (f64.const 3.0)))
414
- (local.get $y))))))`
678
+ (else
679
+ ;; Initial guess via pow, then Newton-Raphson: y = (2y + x/y²)/3
680
+ (local.set $y (call $math.pow (local.get $x) (f64.const 0.3333333333333333)))
681
+ (local.set $y (f64.div (f64.add (f64.mul (f64.const 2.0) (local.get $y)) (f64.div (local.get $x) (f64.mul (local.get $y) (local.get $y)))) (f64.const 3.0)))
682
+ (local.set $y (f64.div (f64.add (f64.mul (f64.const 2.0) (local.get $y)) (f64.div (local.get $x) (f64.mul (local.get $y) (local.get $y)))) (f64.const 3.0)))
683
+ (local.get $y))))`
684
+
685
+ // Small finite-test helper (NaN→0, ±Inf→0, finite→1). Used by transcendental
686
+ // functions that need to short-circuit on infinite inputs.
687
+ ctx.core.stdlib['math.isFinite'] = `(func $math.isFinite (param $x f64) (result i32)
688
+ (i32.and
689
+ (f64.eq (local.get $x) (local.get $x))
690
+ (f64.lt (f64.abs (local.get $x)) (f64.const inf))))`
415
691
 
416
692
  ctx.core.stdlib['math.hypot'] = `(func $math.hypot (param $x f64) (param $y f64) (result f64)
693
+ ;; Any ±Infinity argument ⇒ +Infinity, even when the other is NaN (ECMA-262 21.3.2.18).
694
+ (if (f64.eq (f64.abs (local.get $x)) (f64.const inf)) (then (return (f64.const inf))))
695
+ (if (f64.eq (f64.abs (local.get $y)) (f64.const inf)) (then (return (f64.const inf))))
417
696
  (f64.sqrt (f64.add (f64.mul (local.get $x) (local.get $x)) (f64.mul (local.get $y) (local.get $y)))))`
418
697
 
419
698
  ctx.core.stdlib['math.random'] = `(func $math.random (result f64)
@@ -425,6 +704,167 @@ export default (ctx) => {
425
704
  (global.set $math.rng_state (local.get $s))
426
705
  (f64.div (f64.convert_i32_u (i32.and (local.get $s) (i32.const 0x7FFFFFFF))) (f64.const 2147483647.0)))`
427
706
 
707
+ ctx.core.stdlib['math.sumPrecise'] = `(func $math.sumPrecise (param $arr i64) (result f64)
708
+ ;; Exact summation via a 2304-bit fixed-point accumulator (36 i64 words,
709
+ ;; little-endian two's complement) holding sum*2^1074. Every finite f64 is an
710
+ ;; integer multiple of 2^-1074, so the running sum carries zero rounding
711
+ ;; error; a single ties-to-even rounding at the end yields the result.
712
+ (local $base i32) (local $n i32) (local $i i32) (local $acc i32) (local $addr i32) (local $j i32)
713
+ (local $b i64) (local $exp i32) (local $sig i64) (local $shift i32) (local $wi i32) (local $bo i32)
714
+ (local $lo i64) (local $hi i64) (local $loW i64) (local $hiW i64) (local $ext i64) (local $neg i32)
715
+ (local $carry i32) (local $old i64) (local $s i64) (local $s2 i64) (local $addend i64)
716
+ (local $sawNaN i32) (local $posInf i32) (local $negInf i32) (local $allNegZero i32)
717
+ (local $L i32) (local $word i64) (local $resultNeg i32)
718
+ (local $rwi i32) (local $rbo i32) (local $top i64) (local $roundBit i64) (local $sticky i32) (local $k i32)
719
+ (local $pow f64) (local $res f64)
720
+ ;; allocate + zero 36 i64 words
721
+ (local.set $acc (call $__alloc (i32.const 288)))
722
+ (local.set $j (i32.const 0))
723
+ (block $zdone (loop $zero
724
+ (br_if $zdone (i32.ge_u (local.get $j) (i32.const 288)))
725
+ (i64.store (i32.add (local.get $acc) (local.get $j)) (i64.const 0))
726
+ (local.set $j (i32.add (local.get $j) (i32.const 8)))
727
+ (br $zero)))
728
+ (local.set $allNegZero (i32.const 1))
729
+ (local.set $base (call $__ptr_offset (local.get $arr)))
730
+ (local.set $n (call $__len (local.get $arr)))
731
+ ;; accumulate every element
732
+ (local.set $i (i32.const 0))
733
+ (block $idone (loop $iter
734
+ (br_if $idone (i32.ge_u (local.get $i) (local.get $n)))
735
+ (block $next
736
+ (local.set $b (i64.load (i32.add (local.get $base) (i32.shl (local.get $i) (i32.const 3)))))
737
+ (local.set $exp (i32.wrap_i64 (i64.and (i64.shr_u (local.get $b) (i64.const 52)) (i64.const 0x7ff))))
738
+ (local.set $sig (i64.and (local.get $b) (i64.const 0xfffffffffffff)))
739
+ (local.set $neg (i32.wrap_i64 (i64.shr_u (local.get $b) (i64.const 63))))
740
+ ;; NaN / +-Infinity
741
+ (if (i32.eq (local.get $exp) (i32.const 0x7ff))
742
+ (then
743
+ (if (i64.ne (local.get $sig) (i64.const 0))
744
+ (then (local.set $sawNaN (i32.const 1)))
745
+ (else (if (local.get $neg)
746
+ (then (local.set $negInf (i32.const 1)))
747
+ (else (local.set $posInf (i32.const 1))))))
748
+ (br $next)))
749
+ ;; -0 tracking: any element not bit-identical to -0 clears allNegZero
750
+ (if (i64.ne (local.get $b) (i64.const 0x8000000000000000))
751
+ (then (local.set $allNegZero (i32.const 0))))
752
+ ;; +-0 contributes nothing
753
+ (if (i32.and (i32.eqz (local.get $exp)) (i64.eqz (local.get $sig)))
754
+ (then (br $next)))
755
+ ;; significand + bit shift: normal adds the implicit bit, shift=exp-1; subnormal shift=0
756
+ (if (i32.eqz (local.get $exp))
757
+ (then (local.set $shift (i32.const 0)))
758
+ (else
759
+ (local.set $sig (i64.or (local.get $sig) (i64.const 0x10000000000000)))
760
+ (local.set $shift (i32.sub (local.get $exp) (i32.const 1)))))
761
+ (local.set $wi (i32.shr_u (local.get $shift) (i32.const 6)))
762
+ (local.set $bo (i32.and (local.get $shift) (i32.const 63)))
763
+ (local.set $lo (i64.shl (local.get $sig) (i64.extend_i32_u (local.get $bo))))
764
+ (local.set $hi (if (result i64) (i32.eqz (local.get $bo))
765
+ (then (i64.const 0))
766
+ (else (i64.shr_u (local.get $sig) (i64.extend_i32_u (i32.sub (i32.const 64) (local.get $bo)))))))
767
+ ;; subtraction of a negative element = adding (~M)+1 with sign-extension ext=-1
768
+ (local.set $ext (i64.extend_i32_s (i32.sub (i32.const 0) (local.get $neg))))
769
+ (local.set $loW (i64.xor (local.get $lo) (local.get $ext)))
770
+ (local.set $hiW (i64.xor (local.get $hi) (local.get $ext)))
771
+ (local.set $carry (local.get $neg))
772
+ (local.set $j (local.get $wi))
773
+ (block $adone (loop $add
774
+ (br_if $adone (i32.ge_u (local.get $j) (i32.const 36)))
775
+ (local.set $addend (select (local.get $loW)
776
+ (select (local.get $hiW) (local.get $ext)
777
+ (i32.eq (local.get $j) (i32.add (local.get $wi) (i32.const 1))))
778
+ (i32.eq (local.get $j) (local.get $wi))))
779
+ (local.set $addr (i32.add (local.get $acc) (i32.shl (local.get $j) (i32.const 3))))
780
+ (local.set $old (i64.load (local.get $addr)))
781
+ (local.set $s (i64.add (local.get $old) (local.get $addend)))
782
+ (local.set $s2 (i64.add (local.get $s) (i64.extend_i32_u (local.get $carry))))
783
+ (local.set $carry (i32.or (i64.lt_u (local.get $s) (local.get $old)) (i64.lt_u (local.get $s2) (local.get $s))))
784
+ (i64.store (local.get $addr) (local.get $s2))
785
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
786
+ (br $add))))
787
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
788
+ (br $iter)))
789
+ ;; special results
790
+ (if (local.get $sawNaN) (then (return (f64.const nan))))
791
+ (if (i32.and (local.get $posInf) (local.get $negInf)) (then (return (f64.const nan))))
792
+ (if (local.get $posInf) (then (return (f64.const inf))))
793
+ (if (local.get $negInf) (then (return (f64.neg (f64.const inf)))))
794
+ ;; sign of accumulator = top bit of word 35; negate the magnitude if set
795
+ (local.set $resultNeg (i32.wrap_i64 (i64.shr_u (i64.load (i32.add (local.get $acc) (i32.const 280))) (i64.const 63))))
796
+ (if (local.get $resultNeg) (then
797
+ (local.set $j (i32.const 0))
798
+ (local.set $carry (i32.const 1))
799
+ (block $ndone (loop $negl
800
+ (br_if $ndone (i32.ge_u (local.get $j) (i32.const 36)))
801
+ (local.set $addr (i32.add (local.get $acc) (i32.shl (local.get $j) (i32.const 3))))
802
+ (local.set $old (i64.xor (i64.load (local.get $addr)) (i64.const -1)))
803
+ (local.set $s (i64.add (local.get $old) (i64.extend_i32_u (local.get $carry))))
804
+ (local.set $carry (i64.lt_u (local.get $s) (local.get $old)))
805
+ (i64.store (local.get $addr) (local.get $s))
806
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
807
+ (br $negl)))))
808
+ ;; bit length L (scan words high -> low)
809
+ (local.set $L (i32.const 0))
810
+ (local.set $j (i32.const 35))
811
+ (block $ldone (loop $lscan
812
+ (local.set $word (i64.load (i32.add (local.get $acc) (i32.shl (local.get $j) (i32.const 3)))))
813
+ (if (i64.ne (local.get $word) (i64.const 0))
814
+ (then
815
+ (local.set $L (i32.sub (i32.add (i32.mul (local.get $j) (i32.const 64)) (i32.const 64))
816
+ (i32.wrap_i64 (i64.clz (local.get $word)))))
817
+ (br $ldone)))
818
+ (br_if $ldone (i32.eqz (local.get $j)))
819
+ (local.set $j (i32.sub (local.get $j) (i32.const 1)))
820
+ (br $lscan)))
821
+ ;; sum is exactly zero: -0 for empty input or an all-(-0) list, else +0
822
+ (if (i32.eqz (local.get $L)) (then
823
+ (return (if (result f64) (i32.or (i32.eqz (local.get $n)) (local.get $allNegZero))
824
+ (then (f64.reinterpret_i64 (i64.const 0x8000000000000000)))
825
+ (else (f64.const 0))))))
826
+ ;; magnitude fits in 53 bits: exact, scale by 2^-1074 (reinterpret of i64 1)
827
+ (if (i32.le_u (local.get $L) (i32.const 53)) (then
828
+ (local.set $res (f64.mul (f64.convert_i64_u (i64.load (local.get $acc))) (f64.reinterpret_i64 (i64.const 1))))
829
+ (return (select (f64.neg (local.get $res)) (local.get $res) (local.get $resultNeg)))))
830
+ ;; round to nearest f64 (ties-to-even). top 53 bits start at bit L-53.
831
+ (local.set $wi (i32.shr_u (i32.sub (local.get $L) (i32.const 53)) (i32.const 6)))
832
+ (local.set $bo (i32.and (i32.sub (local.get $L) (i32.const 53)) (i32.const 63)))
833
+ (local.set $top (i64.shr_u (i64.load (i32.add (local.get $acc) (i32.shl (local.get $wi) (i32.const 3)))) (i64.extend_i32_u (local.get $bo))))
834
+ (if (i32.ne (local.get $bo) (i32.const 0)) (then
835
+ (local.set $top (i64.or (local.get $top)
836
+ (i64.shl (i64.load (i32.add (local.get $acc) (i32.shl (i32.add (local.get $wi) (i32.const 1)) (i32.const 3))))
837
+ (i64.extend_i32_u (i32.sub (i32.const 64) (local.get $bo))))))))
838
+ (local.set $top (i64.and (local.get $top) (i64.const 0x1fffffffffffff)))
839
+ ;; round bit at L-54, sticky = OR of every lower bit
840
+ (local.set $rwi (i32.shr_u (i32.sub (local.get $L) (i32.const 54)) (i32.const 6)))
841
+ (local.set $rbo (i32.and (i32.sub (local.get $L) (i32.const 54)) (i32.const 63)))
842
+ (local.set $roundBit (i64.and (i64.shr_u (i64.load (i32.add (local.get $acc) (i32.shl (local.get $rwi) (i32.const 3)))) (i64.extend_i32_u (local.get $rbo))) (i64.const 1)))
843
+ (local.set $sticky (i32.const 0))
844
+ (local.set $j (i32.const 0))
845
+ (block $sdone (loop $sscan
846
+ (br_if $sdone (i32.ge_u (local.get $j) (local.get $rwi)))
847
+ (if (i64.ne (i64.load (i32.add (local.get $acc) (i32.shl (local.get $j) (i32.const 3)))) (i64.const 0))
848
+ (then (local.set $sticky (i32.const 1))))
849
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
850
+ (br $sscan)))
851
+ (if (i64.ne (i64.and (i64.load (i32.add (local.get $acc) (i32.shl (local.get $rwi) (i32.const 3))))
852
+ (i64.sub (i64.shl (i64.const 1) (i64.extend_i32_u (local.get $rbo))) (i64.const 1)))
853
+ (i64.const 0))
854
+ (then (local.set $sticky (i32.const 1))))
855
+ (if (i32.and (i64.eq (local.get $roundBit) (i64.const 1))
856
+ (i32.or (local.get $sticky) (i32.wrap_i64 (i64.and (local.get $top) (i64.const 1)))))
857
+ (then (local.set $top (i64.add (local.get $top) (i64.const 1)))))
858
+ ;; result = top * 2^k where k is the exponent of top's low bit
859
+ (local.set $k (i32.sub (local.get $L) (i32.const 1127)))
860
+ (if (i32.ge_s (local.get $k) (i32.const 1024)) (then
861
+ (return (select (f64.neg (f64.const inf)) (f64.const inf) (local.get $resultNeg)))))
862
+ (local.set $pow (if (result f64) (i32.ge_s (local.get $k) (i32.const -1022))
863
+ (then (f64.reinterpret_i64 (i64.shl (i64.extend_i32_u (i32.add (local.get $k) (i32.const 1023))) (i64.const 52))))
864
+ (else (f64.reinterpret_i64 (i64.shl (i64.const 1) (i64.extend_i32_u (i32.add (local.get $k) (i32.const 1074))))))))
865
+ (local.set $res (f64.mul (f64.convert_i64_u (local.get $top)) (local.get $pow)))
866
+ (select (f64.neg (local.get $res)) (local.get $res) (local.get $resultNeg)))`
867
+
428
868
  // Global for random state
429
869
  ctx.scope.globals.set('math.rng_state', '(global $math.rng_state (mut i32) (i32.const 12345))')
430
870
  }