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/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, toI32, temp, arrayLoop } from '../src/ir.js'
15
+ import { typed, asF64, asI32, toI32, toNumF64, temp, arrayLoop, isLit, litVal, isPureIR } 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,38 +102,45 @@ 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
70
- }
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
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)
78
132
  }
133
+ ctx.core.emit['math.min'] = minmax('f64.min', Infinity)
134
+ ctx.core.emit['math.max'] = minmax('f64.max', -Infinity)
79
135
  // f64.nearest is roundTiesToEven; JS Math.round is roundTiesToward+∞. They agree
80
136
  // everywhere except exact half-integers n+0.5 with n even (nearest→n, JS→n+1).
81
137
  // Detect that one case — `nearest(x) === x - 0.5` — and bump by one. (The −0.5→−0
82
138
  // and 0.49999…94→0 edges already match `f64.nearest`.)
83
139
  ctx.core.emit['math.round'] = a => {
84
- if (typeof a === 'string' && repOf(a)?.intCertain === true) return asF64(emit(a))
140
+ if (isIntCertain(a)) return asF64(emit(a))
85
141
  const t = temp('rnd'), n = temp('rnd')
86
142
  return typed(['block', ['result', 'f64'],
87
- ['local.set', `$${t}`, asF64(emit(a))],
143
+ ['local.set', `$${t}`, toNumF64(a, emit(a))],
88
144
  ['local.set', `$${n}`, ['f64.nearest', ['local.get', `$${t}`]]],
89
145
  ['select',
90
146
  ['f64.add', ['local.get', `$${n}`], ['f64.const', 1]],
@@ -92,54 +148,106 @@ export default (ctx) => {
92
148
  ['f64.eq', ['local.get', `$${n}`], ['f64.sub', ['local.get', `$${t}`], ['f64.const', 0.5]]]],
93
149
  ], 'f64')
94
150
  }
95
- 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')
96
152
 
97
153
  // Sign
98
- ctx.core.emit['math.sign'] = a => call('math.sign', a)
154
+ ctx.core.emit['math.sign'] = emitter(['math.sign'], a => call('math.sign', a))
99
155
 
100
156
  // Trig
101
- ctx.core.emit['math.sin'] = a => call('math.sin', a)
102
- ctx.core.emit['math.cos'] = a => call('math.cos', a)
103
- 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))
104
160
 
105
161
  // Inverse trig
106
- ctx.core.emit['math.asin'] = a => callDeps(['math.atan', 'math.asin'], 'math.asin', a)
107
- ctx.core.emit['math.acos'] = a => callDeps(['math.atan', 'math.asin', 'math.acos'], 'math.acos', a)
108
- ctx.core.emit['math.atan'] = a => call('math.atan', a)
109
- 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))
110
166
 
111
167
  // Hyperbolic
112
- ctx.core.emit['math.sinh'] = a => callDeps(['math.exp', 'math.sinh'], 'math.sinh', a)
113
- ctx.core.emit['math.cosh'] = a => callDeps(['math.exp', 'math.cosh'], 'math.cosh', a)
114
- 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))
115
171
 
116
172
  // Inverse hyperbolic
117
- ctx.core.emit['math.asinh'] = a => callDeps(['math.log', 'math.asinh'], 'math.asinh', a)
118
- ctx.core.emit['math.acosh'] = a => callDeps(['math.log', 'math.acosh'], 'math.acosh', a)
119
- 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))
120
176
 
121
177
  // Exponential and logarithmic
122
- ctx.core.emit['math.exp'] = a => call('math.exp', a)
123
- ctx.core.emit['math.expm1'] = a => callDeps(['math.exp', 'math.expm1'], 'math.expm1', a)
124
- ctx.core.emit['math.log'] = a => call('math.log', a)
125
- ctx.core.emit['math.log2'] = a => callDeps(['math.log', 'math.log2'], 'math.log2', a)
126
- ctx.core.emit['math.log10'] = a => callDeps(['math.log', 'math.log10'], 'math.log10', a)
127
- ctx.core.emit['math.log1p'] = a => callDeps(['math.log', 'math.log1p'], 'math.log1p', a)
128
-
129
- // Power
130
- ctx.core.emit['math.pow'] = (a, b) => callDeps(['math.exp', 'math.log', 'math.pow'], 'math.pow', a, b)
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))
184
+
185
+ // Power. Constant-integer-exponent `Math.pow(x,n)` / `x ** n` (|n| ≤ POW_FOLD_MAX)
186
+ // lower to inline square-and-multiply instead of a $math.pow call. The fold is
187
+ // bit-identical to $math.pow's integer fast path: that path runs the same LSB-first
188
+ // square-and-multiply, and an f64 product's magnitude is the rounded product of the
189
+ // operand magnitudes regardless of sign — so multiplying the *signed* base reproduces
190
+ // both the exact bits and the result sign (negative iff x<0 ∧ n odd, which is exactly
191
+ // its `neg_base`). A program whose only pow use is folded then never pulls the
192
+ // math.pow/exp/log stdlib. `**`'s exponent is parsed as a bare number (incl. negatives).
193
+ const POW_FOLD_MAX = 8
194
+ const get = name => ['local.get', `$${name}`]
195
+ const constInt = b => {
196
+ const v = typeof b === 'number' ? b
197
+ : (Array.isArray(b) && b.length === 2 && b[0] == null && typeof b[1] === 'number') ? b[1]
198
+ : null
199
+ return v != null && Number.isInteger(v) ? v : null
200
+ }
201
+ const foldPow = (a, n) => {
202
+ const baseIR = toNumF64(a, emit(a))
203
+ // pow(x,0) === 1 for every x (NaN/±0/±Inf included). Keep the base's side
204
+ // effects (a call, a throwing valueOf), discard its value, yield 1.
205
+ if (n === 0) return isPureIR(baseIR)
206
+ ? typed(['f64.const', 1], 'f64')
207
+ : typed(['block', ['result', 'f64'], ['drop', baseIR], ['f64.const', 1]], 'f64')
208
+ const b = temp('pw')
209
+ const stmts = [['local.set', `$${b}`, baseIR]]
210
+ // square-and-multiply, LSB-first — mirrors $math.pow's loop association exactly,
211
+ // so the rounding tree (and thus the last bit) matches.
212
+ let sq = b, res = null, minted = false
213
+ for (let m = Math.abs(n); m > 0; m >>= 1) {
214
+ if (m & 1) {
215
+ if (res === null) res = sq // lowest set bit: result := this square (skip ×1)
216
+ else { const r = temp('pw'); stmts.push(['local.set', `$${r}`, ['f64.mul', get(res), get(sq)]]); res = r; minted = true }
217
+ }
218
+ if (m >> 1) { const s = temp('pw'); stmts.push(['local.set', `$${s}`, ['f64.mul', get(sq), get(sq)]]); sq = s; minted = true }
219
+ }
220
+ let result = get(res)
221
+ if (n < 0) { result = ['f64.div', ['f64.const', 1], result]; minted = true } // y<0 → reciprocal, as $math.pow does
222
+ // A NaN minted by f64.mul/div has a platform-nondeterministic sign; jz's value
223
+ // model requires the one canonical number-NaN, so `canon` folds it back. Skip when
224
+ // the base provably can't be NaN (same test min/max uses) or when no op was minted
225
+ // (|n|=1 hands the base straight through, already canonical).
226
+ const inner = typed(['block', ['result', 'f64'], ...stmts, result], 'f64')
227
+ return (minted && !neverNaN(a, baseIR)) ? canon(inner) : inner
228
+ }
229
+ const powCall = emitter(['math.pow'], (a, b) => call('math.pow', a, b))
230
+ ctx.core.emit['math.pow'] = (a, b) => {
231
+ const n = constInt(b)
232
+ return n !== null && Math.abs(n) <= POW_FOLD_MAX ? foldPow(a, n) : powCall(a, b)
233
+ }
234
+ ctx.core.emit['math.pow'].deps = powCall.deps // metadata parity (tabulation/analysis)
131
235
  ctx.core.emit['**'] = ctx.core.emit['math.pow']
132
- ctx.core.emit['math.cbrt'] = a => callDeps(['math.exp', 'math.log', 'math.pow', 'math.cbrt'], 'math.cbrt', a)
133
- ctx.core.emit['math.hypot'] = (a, b, ...rest) => {
236
+ ctx.core.emit['math.cbrt'] = emitter(['math.cbrt'], a => call('math.cbrt', a))
237
+ ctx.core.emit['math.hypot'] = emitter(['math.hypot'], (a, b, ...rest) => {
134
238
  if (a === undefined) return typed(['f64.const', 0], 'f64')
135
239
  if (b === undefined) return f('f64.abs', a)
136
240
  let r = call('math.hypot', a, b)
137
- for (const x of rest) {
138
- inc('math.hypot')
139
- r = typed(['call', '$math.hypot', r, asF64(emit(x))], 'f64')
140
- }
241
+ // ToNumber every rest arg too (matches min/max) — an object arg's valueOf
242
+ // must run and may throw, which Math.hypot propagates.
243
+ for (const x of rest) r = typed(['call', '$math.hypot', r, toNumF64(x, emit(x))], 'f64')
141
244
  return r
142
- }
245
+ })
246
+
247
+ // Math.sumPrecise(iterable) — exact, correctly-rounded summation (ECMA-262).
248
+ // jz models the array case; the WAT routine sums via a fixed-point accumulator.
249
+ ctx.core.emit['math.sumPrecise'] = emitter(['math.sumPrecise'], arr =>
250
+ typed(['call', '$math.sumPrecise', ['i64.reinterpret_f64', asF64(emit(arr))]], 'f64'))
143
251
 
144
252
  // Integer/bit operations: return i32 directly. Consumers `asF64`-rebox at
145
253
  // store/return boundaries; consumers staying in i32 (bit chains, i32 locals)
@@ -150,21 +258,24 @@ export default (ctx) => {
150
258
  ctx.core.emit['math.imul'] = (a, b) => typed(['i32.mul', toI32(emit(a)), toI32(emit(b))], 'i32')
151
259
 
152
260
  // Random
153
- ctx.core.emit['math.random'] = () => (inc('math.random'), typed(['call', '$math.random'], 'f64'))
261
+ ctx.core.emit['math.random'] = emitter(['math.random'], () => typed(['call', '$math.random'], 'f64'))
154
262
 
155
263
  // ============================================
156
264
  // WAT stdlib implementations
157
265
  // ============================================
158
266
 
159
267
  ctx.core.stdlib['math.sign'] = `(func $math.sign (param $x f64) (result f64)
160
- (if (result f64) (f64.eq (local.get $x) (f64.const 0.0))
161
- (then (f64.const 0.0))
162
- (else (if (result f64) (f64.gt (local.get $x) (f64.const 0.0))
163
- (then (f64.const 1.0))
164
- (else (f64.const -1.0))))))`
268
+ ;; sign(NaN) = NaN, sign(±0) = ±0 — both pass x through unchanged.
269
+ (if (f64.ne (local.get $x) (local.get $x)) (then (return (local.get $x))))
270
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
271
+ (if (result f64) (f64.gt (local.get $x) (f64.const 0.0))
272
+ (then (f64.const 1.0))
273
+ (else (f64.const -1.0))))`
165
274
 
166
275
  ctx.core.stdlib['math.sin'] = `(func $math.sin (param $x f64) (result f64)
167
276
  (local $n i32) (local $r f64) (local $x2 f64) (local $sign f64)
277
+ ;; NaN/±Infinity → NaN (avoid trapping i32.trunc on infinities).
278
+ (if (i32.eqz (call $math.isFinite (local.get $x))) (then (return (f64.const nan))))
168
279
  (local.set $sign (f64.const 1.0))
169
280
  (local.set $n (i32.trunc_f64_s (f64.floor (f64.div (local.get $x) (f64.const ${Math.PI})))))
170
281
  (local.set $r (f64.sub (local.get $x) (f64.mul (f64.convert_i32_s (local.get $n)) (f64.const ${Math.PI}))))
@@ -183,6 +294,8 @@ export default (ctx) => {
183
294
 
184
295
  ctx.core.stdlib['math.cos'] = `(func $math.cos (param $x f64) (result f64)
185
296
  (local $n i32) (local $r f64) (local $x2 f64) (local $sign f64)
297
+ ;; NaN/±Infinity → NaN (avoid trapping i32.trunc on infinities).
298
+ (if (i32.eqz (call $math.isFinite (local.get $x))) (then (return (f64.const nan))))
186
299
  (local.set $sign (f64.const 1.0))
187
300
  (local.set $n (i32.trunc_f64_s (f64.floor (f64.div (local.get $x) (f64.const ${Math.PI})))))
188
301
  (local.set $r (f64.sub (local.get $x) (f64.mul (f64.convert_i32_s (local.get $n)) (f64.const ${Math.PI}))))
@@ -205,7 +318,8 @@ export default (ctx) => {
205
318
  ctx.core.stdlib['math.exp'] = `(func $math.exp (param $x f64) (result f64)
206
319
  (local $k i32) (local $t f64) (local $t2 f64) (local $result f64) (local $pow2 f64)
207
320
  (if (f64.ne (local.get $x) (local.get $x)) (then (return (local.get $x))))
208
- (if (result f64) (f64.gt (local.get $x) (f64.const 709.0)) (then (f64.const 1.7976931348623157e+308)) (else
321
+ ;; +Infinity +Infinity; finite overflow (x > 709) also rounds to +Infinity.
322
+ (if (result f64) (f64.gt (local.get $x) (f64.const 709.0)) (then (f64.const inf)) (else
209
323
  (if (result f64) (f64.lt (local.get $x) (f64.const -745.0)) (then (f64.const 0.0)) (else
210
324
  (local.set $k (i32.trunc_f64_s (f64.div (local.get $x) (f64.const ${Math.LN2}))))
211
325
  (local.set $t (f64.sub (local.get $x) (f64.mul (f64.convert_i32_s (local.get $k)) (f64.const ${Math.LN2}))))
@@ -234,34 +348,54 @@ export default (ctx) => {
234
348
  (local.get $result))))))`
235
349
 
236
350
  ctx.core.stdlib['math.expm1'] = `(func $math.expm1 (param $x f64) (result f64)
351
+ ;; Preserve sign of zero: expm1(±0) = ±0.
352
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
237
353
  (f64.sub (call $math.exp (local.get $x)) (f64.const 1.0)))`
238
354
 
355
+ // log(x) via bit-level frexp + sqrt(2)-centered split + atanh series.
356
+ // x = m * 2^k with bits-extracted k (no loop)
357
+ // if m >= sqrt(2): m /= 2, k += 1 so m ∈ [sqrt(2)/2, sqrt(2)) ≈ [0.707, 1.414)
358
+ // s = (m-1)/(m+1) |s| ≤ 0.172
359
+ // log(x) = k·ln(2) + 2s·(1 + s²/3 + s⁴/5 + ... + s¹⁶/17)
360
+ // With 9 polynomial terms and |s|≤0.172, truncation error ≈ 2|s|·z⁹/19 ≈ 4e-17,
361
+ // close to f64 ulp. The whole routine is branchless after edge cases.
362
+ // Edge cases: NaN→NaN, ≤0 distinguishes 0→-Inf, <0→NaN; +Inf passes through.
239
363
  ctx.core.stdlib['math.log'] = `(func $math.log (param $x f64) (result f64)
240
- (local $k i32) (local $y f64) (local $s f64) (local $z f64)
364
+ (local $bits i64) (local $k i32) (local $m f64) (local $s f64) (local $z f64)
241
365
  (if (f64.ne (local.get $x) (local.get $x))
242
366
  (then (return (local.get $x))))
243
367
  (if (f64.le (local.get $x) (f64.const 0.0))
244
- (then (return (f64.const 0.0))))
245
- (if (f64.eq (local.get $x) (f64.const 1.0))
246
- (then (return (f64.const 0.0))))
368
+ (then
369
+ (if (f64.eq (local.get $x) (f64.const 0.0))
370
+ (then (return (f64.const -inf))))
371
+ (return (f64.const nan))))
247
372
  (if (f64.eq (local.get $x) (f64.const inf))
248
373
  (then (return (local.get $x))))
249
374
  (local.set $k (i32.const 0))
250
- (local.set $y (local.get $x))
251
- (block $done_up
252
- (loop $scale_up
253
- (br_if $done_up (f64.lt (local.get $y) (f64.const 2.0)))
254
- (local.set $y (f64.mul (local.get $y) (f64.const 0.5)))
255
- (local.set $k (i32.add (local.get $k) (i32.const 1)))
256
- (br $scale_up)))
257
- (block $done_down
258
- (loop $scale_down
259
- (br_if $done_down (f64.ge (local.get $y) (f64.const 1.0)))
260
- (local.set $y (f64.mul (local.get $y) (f64.const 2.0)))
261
- (local.set $k (i32.sub (local.get $k) (i32.const 1)))
262
- (br $scale_down)))
263
- (local.set $s (f64.div (f64.sub (local.get $y) (f64.const 1.0)) (f64.add (local.get $y) (f64.const 1.0))))
375
+ ;; Normalize denormals (exponent=0): scale by 2^54 and remember the shift,
376
+ ;; so the bit-extracted exponent below is meaningful for every finite x > 0.
377
+ (if (f64.lt (local.get $x) (f64.const 0x1p-1022))
378
+ (then
379
+ (local.set $x (f64.mul (local.get $x) (f64.const 0x1p54)))
380
+ (local.set $k (i32.const -54))))
381
+ ;; frexp via bit twiddling: k = ((bits >> 52) & 0x7ff) - 1023, then force exp=1023 so m ∈ [1,2).
382
+ (local.set $bits (i64.reinterpret_f64 (local.get $x)))
383
+ (local.set $k (i32.add (local.get $k) (i32.sub
384
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 52)) (i64.const 0x7ff)))
385
+ (i32.const 1023))))
386
+ (local.set $m (f64.reinterpret_i64
387
+ (i64.or
388
+ (i64.and (local.get $bits) (i64.const 0x000fffffffffffff))
389
+ (i64.const 0x3ff0000000000000))))
390
+ ;; Center on sqrt(2) to shrink |s| from 1/3 down to ~0.172.
391
+ (if (f64.ge (local.get $m) (f64.const 1.4142135623730951))
392
+ (then
393
+ (local.set $m (f64.mul (local.get $m) (f64.const 0.5)))
394
+ (local.set $k (i32.add (local.get $k) (i32.const 1)))))
395
+ (local.set $s (f64.div (f64.sub (local.get $m) (f64.const 1.0))
396
+ (f64.add (local.get $m) (f64.const 1.0))))
264
397
  (local.set $z (f64.mul (local.get $s) (local.get $s)))
398
+ ;; Horner: 1 + z/3 + z²/5 + z³/7 + z⁴/9 + z⁵/11 + z⁶/13 + z⁷/15 + z⁸/17
265
399
  (f64.add
266
400
  (f64.mul (f64.convert_i32_s (local.get $k)) (f64.const ${Math.LN2}))
267
401
  (f64.mul (f64.const 2.0) (f64.mul (local.get $s) (f64.add (f64.const 1.0)
@@ -269,16 +403,99 @@ export default (ctx) => {
269
403
  (f64.mul (local.get $z) (f64.add (f64.const 0.2)
270
404
  (f64.mul (local.get $z) (f64.add (f64.const 0.14285714285714285)
271
405
  (f64.mul (local.get $z) (f64.add (f64.const 0.1111111111111111)
272
- (f64.mul (local.get $z) (f64.const 0.09090909090909091)))))))))))))))`
406
+ (f64.mul (local.get $z) (f64.add (f64.const 0.09090909090909091)
407
+ (f64.mul (local.get $z) (f64.add (f64.const 0.07692307692307693)
408
+ (f64.mul (local.get $z) (f64.add (f64.const 0.06666666666666667)
409
+ (f64.mul (local.get $z) (f64.const 0.058823529411764705)))))))))))))))))))))`
273
410
 
274
411
  ctx.core.stdlib['math.log2'] = `(func $math.log2 (param $x f64) (result f64)
275
412
  (f64.div (call $math.log (local.get $x)) (f64.const ${Math.LN2})))`
276
413
 
414
+ // log10 via fdlibm's two-term decomposition: log10(x) = k*log10(2) + log10(m).
415
+ // A plain log(x)/ln(10) double-rounds (rounding of log itself, then of the
416
+ // divide), so exact powers of ten drift — log10(1000) lands on 2.9999…996.
417
+ // Reducing x = m·2^k, splitting log10(2) and 1/ln(10) into hi/lo halves, and
418
+ // keeping the bulk term (k·log10_2hi, hi·ivln10hi) carry-free recovers the
419
+ // last ulps, so log10(10/100/1000/…) round-trips to exact integers.
277
420
  ctx.core.stdlib['math.log10'] = `(func $math.log10 (param $x f64) (result f64)
278
- (f64.div (call $math.log (local.get $x)) (f64.const ${Math.LN10})))`
279
-
421
+ (local $bits i64) (local $k i32) (local $m f64) (local $f f64)
422
+ (local $hfsq f64) (local $s f64) (local $z f64) (local $w f64)
423
+ (local $t1 f64) (local $t2 f64) (local $R f64)
424
+ (local $hi f64) (local $lo f64) (local $dk f64)
425
+ (local $valhi f64) (local $vallo f64) (local $y f64)
426
+ ;; Special values: NaN→NaN, x≤0 → (-inf for 0, NaN for negative), +inf→+inf.
427
+ (if (f64.ne (local.get $x) (local.get $x)) (then (return (local.get $x))))
428
+ (if (f64.le (local.get $x) (f64.const 0.0))
429
+ (then
430
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (f64.const -inf))))
431
+ (return (f64.const nan))))
432
+ (if (f64.eq (local.get $x) (f64.const inf)) (then (return (local.get $x))))
433
+ ;; Normalize subnormals so the bit-extracted exponent is meaningful.
434
+ (local.set $k (i32.const 0))
435
+ (if (f64.lt (local.get $x) (f64.const 0x1p-1022))
436
+ (then
437
+ (local.set $x (f64.mul (local.get $x) (f64.const 0x1p54)))
438
+ (local.set $k (i32.const -54))))
439
+ ;; frexp: k += exponent, m = mantissa forced into [1,2).
440
+ (local.set $bits (i64.reinterpret_f64 (local.get $x)))
441
+ (local.set $k (i32.add (local.get $k) (i32.sub
442
+ (i32.wrap_i64 (i64.and (i64.shr_u (local.get $bits) (i64.const 52)) (i64.const 0x7ff)))
443
+ (i32.const 1023))))
444
+ (local.set $m (f64.reinterpret_i64
445
+ (i64.or (i64.and (local.get $bits) (i64.const 0x000fffffffffffff))
446
+ (i64.const 0x3ff0000000000000))))
447
+ ;; Center on sqrt(2): m ∈ [sqrt2/2, sqrt2) keeps the kernel argument small.
448
+ (if (f64.ge (local.get $m) (f64.const 1.4142135623730951))
449
+ (then
450
+ (local.set $m (f64.mul (local.get $m) (f64.const 0.5)))
451
+ (local.set $k (i32.add (local.get $k) (i32.const 1)))))
452
+ ;; log(m) kernel: f - hfsq + s*(hfsq+R), s = f/(2+f), polynomial in s².
453
+ (local.set $f (f64.sub (local.get $m) (f64.const 1.0)))
454
+ (local.set $hfsq (f64.mul (f64.const 0.5) (f64.mul (local.get $f) (local.get $f))))
455
+ (local.set $s (f64.div (local.get $f) (f64.add (f64.const 2.0) (local.get $f))))
456
+ (local.set $z (f64.mul (local.get $s) (local.get $s)))
457
+ (local.set $w (f64.mul (local.get $z) (local.get $z)))
458
+ (local.set $t1 (f64.mul (local.get $w) (f64.add (f64.const 0.3999999999940942)
459
+ (f64.mul (local.get $w) (f64.add (f64.const 0.22222198432149792)
460
+ (f64.mul (local.get $w) (f64.const 0.15313837699209373)))))))
461
+ (local.set $t2 (f64.mul (local.get $z) (f64.add (f64.const 0.6666666666666735)
462
+ (f64.mul (local.get $w) (f64.add (f64.const 0.2857142874366239)
463
+ (f64.mul (local.get $w) (f64.add (f64.const 0.1818357216161805)
464
+ (f64.mul (local.get $w) (f64.const 0.14798198605116586)))))))))
465
+ (local.set $R (f64.add (local.get $t2) (local.get $t1)))
466
+ ;; hi = high 32 bits of (f - hfsq); lo = the carry-free remainder.
467
+ (local.set $hi (f64.sub (local.get $f) (local.get $hfsq)))
468
+ (local.set $hi (f64.reinterpret_i64
469
+ (i64.and (i64.reinterpret_f64 (local.get $hi)) (i64.const 0xffffffff00000000))))
470
+ (local.set $lo (f64.add
471
+ (f64.sub (f64.sub (local.get $f) (local.get $hi)) (local.get $hfsq))
472
+ (f64.mul (local.get $s) (f64.add (local.get $hfsq) (local.get $R)))))
473
+ ;; Combine with k·log10(2): bulk in val_hi, corrections in val_lo.
474
+ (local.set $valhi (f64.mul (local.get $hi) (f64.const 0.4342944818781689)))
475
+ (local.set $dk (f64.convert_i32_s (local.get $k)))
476
+ (local.set $y (f64.mul (local.get $dk) (f64.const 0.30102999566361177)))
477
+ (local.set $vallo (f64.add (f64.add
478
+ (f64.mul (local.get $dk) (f64.const 3.694239077158931e-13))
479
+ (f64.mul (f64.add (local.get $lo) (local.get $hi)) (f64.const 2.5082946711645275e-11)))
480
+ (f64.mul (local.get $lo) (f64.const 0.4342944818781689))))
481
+ (local.set $w (f64.add (local.get $y) (local.get $valhi)))
482
+ (local.set $vallo (f64.add (local.get $vallo)
483
+ (f64.add (f64.sub (local.get $y) (local.get $w)) (local.get $valhi))))
484
+ (f64.add (local.get $vallo) (local.get $w)))`
485
+
486
+ // log1p(x) via Kahan's compensated trick: with u = 1+x, log(u) loses bits when x is
487
+ // small (because u rounds to ~1), but the ratio x/(u-1) is exactly the missing factor.
488
+ // For u==1 (x below ulp), result is just x; preserves -0 from x=-0 path.
280
489
  ctx.core.stdlib['math.log1p'] = `(func $math.log1p (param $x f64) (result f64)
281
- (call $math.log (f64.add (f64.const 1.0) (local.get $x))))`
490
+ (local $u f64)
491
+ ;; log1p(+Inf) = +Inf — the ratio trick below would compute Inf/Inf = NaN.
492
+ (if (f64.eq (local.get $x) (f64.const inf)) (then (return (f64.const inf))))
493
+ (local.set $u (f64.add (f64.const 1.0) (local.get $x)))
494
+ (if (f64.eq (local.get $u) (f64.const 1.0))
495
+ (then (return (local.get $x))))
496
+ (f64.div
497
+ (f64.mul (call $math.log (local.get $u)) (local.get $x))
498
+ (f64.sub (local.get $u) (f64.const 1.0))))`
282
499
 
283
500
  ctx.core.stdlib['math.pow'] = `(func $math.pow (param $x f64) (param $y f64) (result f64)
284
501
  (local $result f64) (local $n i32) (local $neg_base i32) (local $abs_x f64)
@@ -293,7 +510,7 @@ export default (ctx) => {
293
510
  (then
294
511
  (local.set $abs_x (f64.abs (local.get $x)))
295
512
  (if (f64.eq (local.get $abs_x) (f64.const 1.0))
296
- (then (return (f64.div (f64.const 0.0) (f64.const 0.0)))))
513
+ (then (return (f64.const nan))))
297
514
  (if (i32.eq (f64.gt (local.get $abs_x) (f64.const 1.0))
298
515
  (f64.gt (local.get $y) (f64.const 0.0)))
299
516
  (then (return (f64.const inf)))
@@ -330,6 +547,20 @@ export default (ctx) => {
330
547
  (if (local.get $neg_base)
331
548
  (then (local.set $result (f64.neg (local.get $result)))))
332
549
  (return (local.get $result))))
550
+ ;; x is ±Infinity with |y| >= 2^31 (the i32 fast path above handles smaller y):
551
+ ;; magnitude is Inf for y>0, 0 for y<0; sign is negative only when x is -Inf
552
+ ;; and y is an odd integer. Odd-ness is tested in f64 (y, y/2 both integral)
553
+ ;; to avoid an i32.trunc trap on |y| beyond i32 range.
554
+ (if (f64.eq (f64.abs (local.get $x)) (f64.const inf))
555
+ (then
556
+ (local.set $result
557
+ (select (f64.const inf) (f64.const 0.0) (f64.gt (local.get $y) (f64.const 0.0))))
558
+ (if (i32.and (f64.lt (local.get $x) (f64.const 0.0))
559
+ (i32.and (f64.eq (f64.nearest (local.get $y)) (local.get $y))
560
+ (f64.ne (f64.nearest (f64.mul (local.get $y) (f64.const 0.5)))
561
+ (f64.mul (local.get $y) (f64.const 0.5)))))
562
+ (then (local.set $result (f64.neg (local.get $result)))))
563
+ (return (local.get $result))))
333
564
  ;; x == 0 with non-integer y -> y<0 ? Infinity : 0 (sign-of-zero only matters for integer y, handled above)
334
565
  (if (f64.eq (local.get $x) (f64.const 0.0))
335
566
  (then
@@ -338,41 +569,85 @@ export default (ctx) => {
338
569
  (else (return (f64.const 0.0))))))
339
570
  ;; x < 0, non-integer finite y -> NaN
340
571
  (if (f64.lt (local.get $x) (f64.const 0.0))
341
- (then (return (f64.div (f64.const 0.0) (f64.const 0.0)))))
572
+ (then (return (f64.const nan))))
342
573
  (call $math.exp (f64.mul (local.get $y) (call $math.log (local.get $x)))))`
343
574
 
575
+ // fdlibm atan: 4-region argument reduction onto |r| ≤ tan(π/16), then an
576
+ // 11-term odd polynomial split into even/odd parts. Accurate to <1 ulp —
577
+ // the old Taylor series was ~2e-6 off near |x|=0.5. Drives asin/acos/atan2.
344
578
  ctx.core.stdlib['math.atan'] = `(func $math.atan (param $x f64) (result f64)
345
- (local $x2 f64) (local $abs_x f64) (local $reduced f64)
579
+ (local $abs_x f64) (local $id i32) (local $r f64) (local $z f64) (local $w f64)
580
+ (local $s1 f64) (local $s2 f64) (local $ahi f64) (local $alo f64) (local $res f64)
581
+ ;; NaN passes through unchanged.
582
+ (if (f64.ne (local.get $x) (local.get $x)) (then (return (local.get $x))))
346
583
  (local.set $abs_x (f64.abs (local.get $x)))
347
- (if (result f64) (f64.gt (local.get $abs_x) (f64.const 1.0))
584
+ ;; |x| >= 2^66: atan saturates to ±π/2.
585
+ (if (f64.ge (local.get $abs_x) (f64.const 7.378697629483821e19))
586
+ (then (return (f64.copysign (f64.const 1.5707963267948966) (local.get $x)))))
587
+ (if (f64.lt (local.get $abs_x) (f64.const 0.4375))
348
588
  (then
349
- (if (result f64) (f64.gt (local.get $x) (f64.const 0.0))
350
- (then (f64.sub (f64.const ${Math.PI / 2}) (call $math.atan (f64.div (f64.const 1.0) (local.get $x)))))
351
- (else (f64.add (f64.neg (f64.const ${Math.PI / 2})) (call $math.atan (f64.div (f64.const 1.0) (local.get $x)))))))
589
+ ;; |x| < 2^-27: atan(x) ≈ x (also preserves sign of zero).
590
+ (if (f64.lt (local.get $abs_x) (f64.const 7.450580596923828e-9))
591
+ (then (return (local.get $x))))
592
+ (local.set $id (i32.const -1))
593
+ (local.set $r (local.get $x)))
352
594
  (else
353
- (if (result f64) (f64.gt (local.get $abs_x) (f64.const 0.5))
595
+ (local.set $r (local.get $abs_x))
596
+ (if (f64.lt (local.get $abs_x) (f64.const 1.1875))
354
597
  (then
355
- (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)))))))
356
- (f64.mul (f64.const 2.0) (call $math.atan (local.get $reduced))))
598
+ (if (f64.lt (local.get $abs_x) (f64.const 0.6875))
599
+ (then ;; id=0: r = (2x-1)/(2+x)
600
+ (local.set $id (i32.const 0))
601
+ (local.set $r (f64.div (f64.sub (f64.mul (f64.const 2.0) (local.get $r)) (f64.const 1.0))
602
+ (f64.add (f64.const 2.0) (local.get $r)))))
603
+ (else ;; id=1: r = (x-1)/(x+1)
604
+ (local.set $id (i32.const 1))
605
+ (local.set $r (f64.div (f64.sub (local.get $r) (f64.const 1.0))
606
+ (f64.add (local.get $r) (f64.const 1.0)))))))
357
607
  (else
358
- (local.set $x2 (f64.mul (local.get $x) (local.get $x)))
359
- (f64.mul (local.get $x)
360
- (f64.sub (f64.const 1.0)
361
- (f64.mul (local.get $x2)
362
- (f64.sub (f64.const 0.3333333333333333)
363
- (f64.mul (local.get $x2)
364
- (f64.sub (f64.const 0.2)
365
- (f64.mul (local.get $x2)
366
- (f64.sub (f64.const 0.14285714285714285)
367
- (f64.mul (local.get $x2)
368
- (f64.sub (f64.const 0.1111111111111111)
369
- (f64.mul (local.get $x2)
370
- (f64.sub (f64.const 0.09090909090909091)
371
- (f64.mul (local.get $x2) (f64.const 0.07692307692307693)))))))))))))))))))`
608
+ (if (f64.lt (local.get $abs_x) (f64.const 2.4375))
609
+ (then ;; id=2: r = (x-1.5)/(1+1.5x)
610
+ (local.set $id (i32.const 2))
611
+ (local.set $r (f64.div (f64.sub (local.get $r) (f64.const 1.5))
612
+ (f64.add (f64.const 1.0) (f64.mul (f64.const 1.5) (local.get $r))))))
613
+ (else ;; id=3: r = -1/x
614
+ (local.set $id (i32.const 3))
615
+ (local.set $r (f64.div (f64.const -1.0) (local.get $r)))))))))
616
+ (local.set $z (f64.mul (local.get $r) (local.get $r)))
617
+ (local.set $w (f64.mul (local.get $z) (local.get $z)))
618
+ (local.set $s1 (f64.mul (local.get $z)
619
+ (f64.add (f64.const 0.3333333333333293)
620
+ (f64.mul (local.get $w) (f64.add (f64.const 0.14285714272503466)
621
+ (f64.mul (local.get $w) (f64.add (f64.const 0.09090887133436507)
622
+ (f64.mul (local.get $w) (f64.add (f64.const 0.06661073137387531)
623
+ (f64.mul (local.get $w) (f64.add (f64.const 0.049768779946159324)
624
+ (f64.mul (local.get $w) (f64.const 0.016285820115365782)))))))))))))
625
+ (local.set $s2 (f64.mul (local.get $w)
626
+ (f64.add (f64.const -0.19999999999876483)
627
+ (f64.mul (local.get $w) (f64.add (f64.const -0.11111110405462356)
628
+ (f64.mul (local.get $w) (f64.add (f64.const -0.0769187620504483)
629
+ (f64.mul (local.get $w) (f64.add (f64.const -0.058335701337905735)
630
+ (f64.mul (local.get $w) (f64.const -0.036531572744216916)))))))))))
631
+ ;; |x| < 0.4375: result = r - r*(s1+s2), sign carried by r itself.
632
+ (if (i32.lt_s (local.get $id) (i32.const 0))
633
+ (then (return (f64.sub (local.get $r) (f64.mul (local.get $r) (f64.add (local.get $s1) (local.get $s2)))))))
634
+ ;; Reconstruct: z = atanhi[id] - ((r*(s1+s2) - atanlo[id]) - r), sign of x.
635
+ (if (i32.eq (local.get $id) (i32.const 0))
636
+ (then (local.set $ahi (f64.const 0.4636476090008061)) (local.set $alo (f64.const 2.2698777452961687e-17)))
637
+ (else (if (i32.eq (local.get $id) (i32.const 1))
638
+ (then (local.set $ahi (f64.const 0.7853981633974483)) (local.set $alo (f64.const 3.061616997868383e-17)))
639
+ (else (if (i32.eq (local.get $id) (i32.const 2))
640
+ (then (local.set $ahi (f64.const 0.982793723247329)) (local.set $alo (f64.const 1.3903311031230998e-17)))
641
+ (else (local.set $ahi (f64.const 1.5707963267948966)) (local.set $alo (f64.const 6.123233995736766e-17))))))))
642
+ (local.set $res (f64.sub (local.get $ahi)
643
+ (f64.sub (f64.sub (f64.mul (local.get $r) (f64.add (local.get $s1) (local.get $s2))) (local.get $alo))
644
+ (local.get $r))))
645
+ (f64.copysign (local.get $res) (local.get $x)))`
372
646
 
373
647
  ctx.core.stdlib['math.asin'] = `(func $math.asin (param $x f64) (result f64)
648
+ ;; Domain is [-1, 1]; outside it (including ±Infinity), Math.asin returns NaN.
374
649
  (if (result f64) (f64.gt (f64.abs (local.get $x)) (f64.const 1.0))
375
- (then (f64.const 0.0))
650
+ (then (f64.const nan))
376
651
  (else (call $math.atan (f64.div (local.get $x)
377
652
  (f64.sqrt (f64.sub (f64.const 1.0) (f64.mul (local.get $x) (local.get $x)))))))))`
378
653
 
@@ -380,9 +655,18 @@ export default (ctx) => {
380
655
  (f64.sub (f64.const ${Math.PI / 2}) (call $math.asin (local.get $x))))`
381
656
 
382
657
  ctx.core.stdlib['math.atan2'] = `(func $math.atan2 (param $y f64) (param $x f64) (result f64)
658
+ ;; If either argument is NaN, the result is NaN (ECMA-262 21.3.2.5).
659
+ (if (f64.ne (local.get $x) (local.get $x)) (then (return (local.get $x))))
660
+ (if (f64.ne (local.get $y) (local.get $y)) (then (return (local.get $y))))
383
661
  (if (result f64) (f64.eq (local.get $x) (f64.const 0.0)) (then
384
- (if (result f64) (f64.eq (local.get $y) (f64.const 0.0)) (then (f64.const 0.0)) (else
385
- (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})))))))
662
+ ;; y is ±0 too: result is ±0 when x is +0, ±π when x is -0; sign taken from y.
663
+ (if (result f64) (f64.eq (local.get $y) (f64.const 0.0))
664
+ (then (f64.copysign
665
+ (select (f64.const ${Math.PI}) (f64.const 0.0)
666
+ (f64.lt (f64.copysign (f64.const 1.0) (local.get $x)) (f64.const 0.0)))
667
+ (local.get $y)))
668
+ (else
669
+ (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})))))))
386
670
  (else (if (result f64) (f64.ge (local.get $x) (f64.const 0.0))
387
671
  (then (call $math.atan (f64.div (local.get $y) (local.get $x))))
388
672
  (else (if (result f64) (f64.ge (local.get $y) (f64.const 0.0))
@@ -391,6 +675,8 @@ export default (ctx) => {
391
675
 
392
676
  ctx.core.stdlib['math.sinh'] = `(func $math.sinh (param $x f64) (result f64)
393
677
  (local $ex f64)
678
+ ;; Preserve sign of zero: sinh(±0) = ±0 (the f64.lt sign test below is false for -0).
679
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
394
680
  (local.set $ex (call $math.exp (f64.abs (local.get $x))))
395
681
  (local.set $ex (f64.mul (f64.const 0.5) (f64.sub (local.get $ex) (f64.div (f64.const 1.0) (local.get $ex)))))
396
682
  (if (result f64) (f64.lt (local.get $x) (f64.const 0.0)) (then (f64.neg (local.get $ex))) (else (local.get $ex))))`
@@ -401,6 +687,8 @@ export default (ctx) => {
401
687
 
402
688
  ctx.core.stdlib['math.tanh'] = `(func $math.tanh (param $x f64) (result f64)
403
689
  (local $e2x f64)
690
+ ;; Preserve sign of zero: tanh(±0) = ±0 (the f64.lt sign test below is false for -0).
691
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
404
692
  (if (result f64) (f64.gt (f64.abs (local.get $x)) (f64.const 22.0))
405
693
  (then (if (result f64) (f64.lt (local.get $x) (f64.const 0.0)) (then (f64.const -1.0)) (else (f64.const 1.0))))
406
694
  (else (local.set $e2x (call $math.exp (f64.mul (f64.const 2.0) (f64.abs (local.get $x)))))
@@ -408,29 +696,51 @@ export default (ctx) => {
408
696
  (if (result f64) (f64.lt (local.get $x) (f64.const 0.0)) (then (f64.neg (local.get $e2x))) (else (local.get $e2x))))))`
409
697
 
410
698
  ctx.core.stdlib['math.asinh'] = `(func $math.asinh (param $x f64) (result f64)
699
+ ;; ±Infinity and NaN pass through unchanged. (log(±Inf + sqrt(Inf²+1)) → NaN otherwise.)
700
+ (if (i32.eqz (call $math.isFinite (local.get $x))) (then (return (local.get $x))))
701
+ ;; Preserve sign of zero: asinh(±0) = ±0.
702
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
411
703
  (call $math.log (f64.add (local.get $x) (f64.sqrt (f64.add (f64.mul (local.get $x) (local.get $x)) (f64.const 1.0))))))`
412
704
 
413
705
  ctx.core.stdlib['math.acosh'] = `(func $math.acosh (param $x f64) (result f64)
414
- (if (result f64) (f64.lt (local.get $x) (f64.const 1.0)) (then (f64.const 0.0)) (else
706
+ (if (f64.eq (local.get $x) (f64.const inf)) (then (return (f64.const inf))))
707
+ ;; acosh is defined only for x >= 1; everything below (incl. -Inf) is NaN.
708
+ (if (result f64) (f64.lt (local.get $x) (f64.const 1.0)) (then (f64.const nan)) (else
415
709
  (call $math.log (f64.add (local.get $x) (f64.sqrt (f64.sub (f64.mul (local.get $x) (local.get $x)) (f64.const 1.0))))))))`
416
710
 
417
711
  ctx.core.stdlib['math.atanh'] = `(func $math.atanh (param $x f64) (result f64)
712
+ ;; Preserve sign of zero: atanh(±0) = ±0.
713
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
714
+ ;; ±Infinity → NaN. Without this the (1+x)/(1-x) ratio is Inf/Inf, whose
715
+ ;; sign-nondeterministic arithmetic NaN would escape non-canonical on x86.
716
+ (if (f64.eq (f64.abs (local.get $x)) (f64.const inf)) (then (return (f64.const nan))))
418
717
  (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))))))`
419
718
 
420
719
  ctx.core.stdlib['math.cbrt'] = `(func $math.cbrt (param $x f64) (result f64)
421
720
  (local $y f64)
721
+ ;; ±Infinity and NaN pass through; preserve sign of zero.
722
+ (if (i32.eqz (call $math.isFinite (local.get $x))) (then (return (local.get $x))))
723
+ (if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
422
724
  (if (result f64) (f64.lt (local.get $x) (f64.const 0.0))
423
725
  (then (f64.neg (call $math.cbrt (f64.neg (local.get $x)))))
424
- (else (if (result f64) (f64.eq (local.get $x) (f64.const 0.0))
425
- (then (f64.const 0.0))
426
- (else
427
- ;; Initial guess via pow, then Newton-Raphson: y = (2y + x/y²)/3
428
- (local.set $y (call $math.pow (local.get $x) (f64.const 0.3333333333333333)))
429
- (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)))
430
- (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)))
431
- (local.get $y))))))`
726
+ (else
727
+ ;; Initial guess via pow, then Newton-Raphson: y = (2y + x/y²)/3
728
+ (local.set $y (call $math.pow (local.get $x) (f64.const 0.3333333333333333)))
729
+ (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)))
730
+ (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)))
731
+ (local.get $y))))`
732
+
733
+ // Small finite-test helper (NaN→0, ±Inf→0, finite→1). Used by transcendental
734
+ // functions that need to short-circuit on infinite inputs.
735
+ ctx.core.stdlib['math.isFinite'] = `(func $math.isFinite (param $x f64) (result i32)
736
+ (i32.and
737
+ (f64.eq (local.get $x) (local.get $x))
738
+ (f64.lt (f64.abs (local.get $x)) (f64.const inf))))`
432
739
 
433
740
  ctx.core.stdlib['math.hypot'] = `(func $math.hypot (param $x f64) (param $y f64) (result f64)
741
+ ;; Any ±Infinity argument ⇒ +Infinity, even when the other is NaN (ECMA-262 21.3.2.18).
742
+ (if (f64.eq (f64.abs (local.get $x)) (f64.const inf)) (then (return (f64.const inf))))
743
+ (if (f64.eq (f64.abs (local.get $y)) (f64.const inf)) (then (return (f64.const inf))))
434
744
  (f64.sqrt (f64.add (f64.mul (local.get $x) (local.get $x)) (f64.mul (local.get $y) (local.get $y)))))`
435
745
 
436
746
  ctx.core.stdlib['math.random'] = `(func $math.random (result f64)
@@ -442,6 +752,167 @@ export default (ctx) => {
442
752
  (global.set $math.rng_state (local.get $s))
443
753
  (f64.div (f64.convert_i32_u (i32.and (local.get $s) (i32.const 0x7FFFFFFF))) (f64.const 2147483647.0)))`
444
754
 
755
+ ctx.core.stdlib['math.sumPrecise'] = `(func $math.sumPrecise (param $arr i64) (result f64)
756
+ ;; Exact summation via a 2304-bit fixed-point accumulator (36 i64 words,
757
+ ;; little-endian two's complement) holding sum*2^1074. Every finite f64 is an
758
+ ;; integer multiple of 2^-1074, so the running sum carries zero rounding
759
+ ;; error; a single ties-to-even rounding at the end yields the result.
760
+ (local $base i32) (local $n i32) (local $i i32) (local $acc i32) (local $addr i32) (local $j i32)
761
+ (local $b i64) (local $exp i32) (local $sig i64) (local $shift i32) (local $wi i32) (local $bo i32)
762
+ (local $lo i64) (local $hi i64) (local $loW i64) (local $hiW i64) (local $ext i64) (local $neg i32)
763
+ (local $carry i32) (local $old i64) (local $s i64) (local $s2 i64) (local $addend i64)
764
+ (local $sawNaN i32) (local $posInf i32) (local $negInf i32) (local $allNegZero i32)
765
+ (local $L i32) (local $word i64) (local $resultNeg i32)
766
+ (local $rwi i32) (local $rbo i32) (local $top i64) (local $roundBit i64) (local $sticky i32) (local $k i32)
767
+ (local $pow f64) (local $res f64)
768
+ ;; allocate + zero 36 i64 words
769
+ (local.set $acc (call $__alloc (i32.const 288)))
770
+ (local.set $j (i32.const 0))
771
+ (block $zdone (loop $zero
772
+ (br_if $zdone (i32.ge_u (local.get $j) (i32.const 288)))
773
+ (i64.store (i32.add (local.get $acc) (local.get $j)) (i64.const 0))
774
+ (local.set $j (i32.add (local.get $j) (i32.const 8)))
775
+ (br $zero)))
776
+ (local.set $allNegZero (i32.const 1))
777
+ (local.set $base (call $__ptr_offset (local.get $arr)))
778
+ (local.set $n (call $__len (local.get $arr)))
779
+ ;; accumulate every element
780
+ (local.set $i (i32.const 0))
781
+ (block $idone (loop $iter
782
+ (br_if $idone (i32.ge_u (local.get $i) (local.get $n)))
783
+ (block $next
784
+ (local.set $b (i64.load (i32.add (local.get $base) (i32.shl (local.get $i) (i32.const 3)))))
785
+ (local.set $exp (i32.wrap_i64 (i64.and (i64.shr_u (local.get $b) (i64.const 52)) (i64.const 0x7ff))))
786
+ (local.set $sig (i64.and (local.get $b) (i64.const 0xfffffffffffff)))
787
+ (local.set $neg (i32.wrap_i64 (i64.shr_u (local.get $b) (i64.const 63))))
788
+ ;; NaN / +-Infinity
789
+ (if (i32.eq (local.get $exp) (i32.const 0x7ff))
790
+ (then
791
+ (if (i64.ne (local.get $sig) (i64.const 0))
792
+ (then (local.set $sawNaN (i32.const 1)))
793
+ (else (if (local.get $neg)
794
+ (then (local.set $negInf (i32.const 1)))
795
+ (else (local.set $posInf (i32.const 1))))))
796
+ (br $next)))
797
+ ;; -0 tracking: any element not bit-identical to -0 clears allNegZero
798
+ (if (i64.ne (local.get $b) (i64.const 0x8000000000000000))
799
+ (then (local.set $allNegZero (i32.const 0))))
800
+ ;; +-0 contributes nothing
801
+ (if (i32.and (i32.eqz (local.get $exp)) (i64.eqz (local.get $sig)))
802
+ (then (br $next)))
803
+ ;; significand + bit shift: normal adds the implicit bit, shift=exp-1; subnormal shift=0
804
+ (if (i32.eqz (local.get $exp))
805
+ (then (local.set $shift (i32.const 0)))
806
+ (else
807
+ (local.set $sig (i64.or (local.get $sig) (i64.const 0x10000000000000)))
808
+ (local.set $shift (i32.sub (local.get $exp) (i32.const 1)))))
809
+ (local.set $wi (i32.shr_u (local.get $shift) (i32.const 6)))
810
+ (local.set $bo (i32.and (local.get $shift) (i32.const 63)))
811
+ (local.set $lo (i64.shl (local.get $sig) (i64.extend_i32_u (local.get $bo))))
812
+ (local.set $hi (if (result i64) (i32.eqz (local.get $bo))
813
+ (then (i64.const 0))
814
+ (else (i64.shr_u (local.get $sig) (i64.extend_i32_u (i32.sub (i32.const 64) (local.get $bo)))))))
815
+ ;; subtraction of a negative element = adding (~M)+1 with sign-extension ext=-1
816
+ (local.set $ext (i64.extend_i32_s (i32.sub (i32.const 0) (local.get $neg))))
817
+ (local.set $loW (i64.xor (local.get $lo) (local.get $ext)))
818
+ (local.set $hiW (i64.xor (local.get $hi) (local.get $ext)))
819
+ (local.set $carry (local.get $neg))
820
+ (local.set $j (local.get $wi))
821
+ (block $adone (loop $add
822
+ (br_if $adone (i32.ge_u (local.get $j) (i32.const 36)))
823
+ (local.set $addend (select (local.get $loW)
824
+ (select (local.get $hiW) (local.get $ext)
825
+ (i32.eq (local.get $j) (i32.add (local.get $wi) (i32.const 1))))
826
+ (i32.eq (local.get $j) (local.get $wi))))
827
+ (local.set $addr (i32.add (local.get $acc) (i32.shl (local.get $j) (i32.const 3))))
828
+ (local.set $old (i64.load (local.get $addr)))
829
+ (local.set $s (i64.add (local.get $old) (local.get $addend)))
830
+ (local.set $s2 (i64.add (local.get $s) (i64.extend_i32_u (local.get $carry))))
831
+ (local.set $carry (i32.or (i64.lt_u (local.get $s) (local.get $old)) (i64.lt_u (local.get $s2) (local.get $s))))
832
+ (i64.store (local.get $addr) (local.get $s2))
833
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
834
+ (br $add))))
835
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
836
+ (br $iter)))
837
+ ;; special results
838
+ (if (local.get $sawNaN) (then (return (f64.const nan))))
839
+ (if (i32.and (local.get $posInf) (local.get $negInf)) (then (return (f64.const nan))))
840
+ (if (local.get $posInf) (then (return (f64.const inf))))
841
+ (if (local.get $negInf) (then (return (f64.neg (f64.const inf)))))
842
+ ;; sign of accumulator = top bit of word 35; negate the magnitude if set
843
+ (local.set $resultNeg (i32.wrap_i64 (i64.shr_u (i64.load (i32.add (local.get $acc) (i32.const 280))) (i64.const 63))))
844
+ (if (local.get $resultNeg) (then
845
+ (local.set $j (i32.const 0))
846
+ (local.set $carry (i32.const 1))
847
+ (block $ndone (loop $negl
848
+ (br_if $ndone (i32.ge_u (local.get $j) (i32.const 36)))
849
+ (local.set $addr (i32.add (local.get $acc) (i32.shl (local.get $j) (i32.const 3))))
850
+ (local.set $old (i64.xor (i64.load (local.get $addr)) (i64.const -1)))
851
+ (local.set $s (i64.add (local.get $old) (i64.extend_i32_u (local.get $carry))))
852
+ (local.set $carry (i64.lt_u (local.get $s) (local.get $old)))
853
+ (i64.store (local.get $addr) (local.get $s))
854
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
855
+ (br $negl)))))
856
+ ;; bit length L (scan words high -> low)
857
+ (local.set $L (i32.const 0))
858
+ (local.set $j (i32.const 35))
859
+ (block $ldone (loop $lscan
860
+ (local.set $word (i64.load (i32.add (local.get $acc) (i32.shl (local.get $j) (i32.const 3)))))
861
+ (if (i64.ne (local.get $word) (i64.const 0))
862
+ (then
863
+ (local.set $L (i32.sub (i32.add (i32.mul (local.get $j) (i32.const 64)) (i32.const 64))
864
+ (i32.wrap_i64 (i64.clz (local.get $word)))))
865
+ (br $ldone)))
866
+ (br_if $ldone (i32.eqz (local.get $j)))
867
+ (local.set $j (i32.sub (local.get $j) (i32.const 1)))
868
+ (br $lscan)))
869
+ ;; sum is exactly zero: -0 for empty input or an all-(-0) list, else +0
870
+ (if (i32.eqz (local.get $L)) (then
871
+ (return (if (result f64) (i32.or (i32.eqz (local.get $n)) (local.get $allNegZero))
872
+ (then (f64.reinterpret_i64 (i64.const 0x8000000000000000)))
873
+ (else (f64.const 0))))))
874
+ ;; magnitude fits in 53 bits: exact, scale by 2^-1074 (reinterpret of i64 1)
875
+ (if (i32.le_u (local.get $L) (i32.const 53)) (then
876
+ (local.set $res (f64.mul (f64.convert_i64_u (i64.load (local.get $acc))) (f64.reinterpret_i64 (i64.const 1))))
877
+ (return (select (f64.neg (local.get $res)) (local.get $res) (local.get $resultNeg)))))
878
+ ;; round to nearest f64 (ties-to-even). top 53 bits start at bit L-53.
879
+ (local.set $wi (i32.shr_u (i32.sub (local.get $L) (i32.const 53)) (i32.const 6)))
880
+ (local.set $bo (i32.and (i32.sub (local.get $L) (i32.const 53)) (i32.const 63)))
881
+ (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))))
882
+ (if (i32.ne (local.get $bo) (i32.const 0)) (then
883
+ (local.set $top (i64.or (local.get $top)
884
+ (i64.shl (i64.load (i32.add (local.get $acc) (i32.shl (i32.add (local.get $wi) (i32.const 1)) (i32.const 3))))
885
+ (i64.extend_i32_u (i32.sub (i32.const 64) (local.get $bo))))))))
886
+ (local.set $top (i64.and (local.get $top) (i64.const 0x1fffffffffffff)))
887
+ ;; round bit at L-54, sticky = OR of every lower bit
888
+ (local.set $rwi (i32.shr_u (i32.sub (local.get $L) (i32.const 54)) (i32.const 6)))
889
+ (local.set $rbo (i32.and (i32.sub (local.get $L) (i32.const 54)) (i32.const 63)))
890
+ (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)))
891
+ (local.set $sticky (i32.const 0))
892
+ (local.set $j (i32.const 0))
893
+ (block $sdone (loop $sscan
894
+ (br_if $sdone (i32.ge_u (local.get $j) (local.get $rwi)))
895
+ (if (i64.ne (i64.load (i32.add (local.get $acc) (i32.shl (local.get $j) (i32.const 3)))) (i64.const 0))
896
+ (then (local.set $sticky (i32.const 1))))
897
+ (local.set $j (i32.add (local.get $j) (i32.const 1)))
898
+ (br $sscan)))
899
+ (if (i64.ne (i64.and (i64.load (i32.add (local.get $acc) (i32.shl (local.get $rwi) (i32.const 3))))
900
+ (i64.sub (i64.shl (i64.const 1) (i64.extend_i32_u (local.get $rbo))) (i64.const 1)))
901
+ (i64.const 0))
902
+ (then (local.set $sticky (i32.const 1))))
903
+ (if (i32.and (i64.eq (local.get $roundBit) (i64.const 1))
904
+ (i32.or (local.get $sticky) (i32.wrap_i64 (i64.and (local.get $top) (i64.const 1)))))
905
+ (then (local.set $top (i64.add (local.get $top) (i64.const 1)))))
906
+ ;; result = top * 2^k where k is the exponent of top's low bit
907
+ (local.set $k (i32.sub (local.get $L) (i32.const 1127)))
908
+ (if (i32.ge_s (local.get $k) (i32.const 1024)) (then
909
+ (return (select (f64.neg (f64.const inf)) (f64.const inf) (local.get $resultNeg)))))
910
+ (local.set $pow (if (result f64) (i32.ge_s (local.get $k) (i32.const -1022))
911
+ (then (f64.reinterpret_i64 (i64.shl (i64.extend_i32_u (i32.add (local.get $k) (i32.const 1023))) (i64.const 52))))
912
+ (else (f64.reinterpret_i64 (i64.shl (i64.const 1) (i64.extend_i32_u (i32.add (local.get $k) (i32.const 1074))))))))
913
+ (local.set $res (f64.mul (f64.convert_i64_u (local.get $top)) (local.get $pow)))
914
+ (select (f64.neg (local.get $res)) (local.get $res) (local.get $resultNeg)))`
915
+
445
916
  // Global for random state
446
917
  ctx.scope.globals.set('math.rng_state', '(global $math.rng_state (mut i32) (i32.const 12345))')
447
918
  }