jz 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +315 -318
- package/bench/README.md +369 -0
- package/bench/bench.svg +102 -0
- package/cli.js +104 -30
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +362 -84
- package/interop.js +159 -186
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +184 -0
- package/module/array.js +377 -176
- package/module/collection.js +639 -145
- package/module/console.js +55 -43
- package/module/core.js +264 -153
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +551 -187
- package/module/number.js +327 -60
- package/module/object.js +474 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +117 -0
- package/module/string.js +621 -226
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +459 -73
- package/package.json +58 -14
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +29 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +824 -0
- package/src/compile/analyze.js +1600 -0
- package/src/compile/emit-assign.js +411 -0
- package/src/compile/emit.js +3512 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +778 -128
- package/src/{infer.js → compile/infer.js} +62 -98
- package/src/compile/loop-divmod.js +155 -0
- package/src/{narrow.js → compile/narrow.js} +409 -106
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +370 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +122 -0
- package/src/compile/plan/inline.js +682 -0
- package/src/compile/plan/literals.js +1199 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +649 -0
- package/src/compile/program-facts.js +423 -0
- package/src/ctx.js +220 -64
- package/src/ir.js +589 -172
- package/src/kind-traits.js +132 -0
- package/src/kind.js +524 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1432 -473
- package/src/optimize/vectorize.js +4660 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +649 -205
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +116 -0
- package/src/resolve.js +12 -3
- package/src/static.js +208 -0
- package/src/type.js +651 -0
- package/src/{assemble.js → wat/assemble.js} +275 -55
- package/src/wat/optimize.js +3938 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -3040
- package/src/jzify.js +0 -1580
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
package/module/math.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Math module - Math.sin, Math.cos, Math.sqrt, Math.PI, etc.
|
|
3
3
|
*
|
|
4
4
|
* Module API:
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
5
|
+
* - reg('math.X', deps, args => WasmNode) — emit handler + declarative stdlib deps
|
|
6
|
+
* - wat('math.X', `(func …)`) — WAT stdlib via bridge
|
|
7
|
+
* - deps({ 'math.X': ['dep'] }) - WAT stdlib→stdlib deps (expanded transitively)
|
|
8
8
|
*
|
|
9
9
|
* Prepare resolves Math.sin(x) → ['()', 'math.sin', x]
|
|
10
10
|
* Compile looks up ctx.core.emit['math.sin'] and calls it.
|
|
@@ -13,17 +13,26 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { typed, asF64, asI32, toI32, toNumF64, temp, arrayLoop, isLit, litVal, isPureIR } from '../src/ir.js'
|
|
16
|
-
import { emit } from '../src/
|
|
17
|
-
import {
|
|
18
|
-
import { repOf } from '../src/
|
|
16
|
+
import { emit, emitter, reg, deps, dual, tag, wat, hostImport } from '../src/bridge.js'
|
|
17
|
+
import { inc, declGlobal } from '../src/ctx.js'
|
|
18
|
+
import { repOf } from '../src/reps.js'
|
|
19
19
|
|
|
20
20
|
export default (ctx) => {
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
// Math.random seeding. DEFAULT: entropy-seeded once from the host on first use (crypto under
|
|
22
|
+
// host:'js', `random_get` under WASI), so randomness "just works" and isn't silently reproducible.
|
|
23
|
+
// `randomSeed: <n>` picks a fixed seed for a reproducible sequence; `true` forces entropy explicitly.
|
|
24
|
+
// Either way jz emits the randomness syscall only when `Math.random` is actually used.
|
|
25
|
+
const rngEntropy = ctx.transform.randomSeed === undefined || ctx.transform.randomSeed === true
|
|
26
|
+
const rngSeedConst = typeof ctx.transform.randomSeed === 'number'
|
|
27
|
+
? ((ctx.transform.randomSeed >>> 0) || 1) // xorshift dies on 0 → floor at 1
|
|
28
|
+
: 12345
|
|
29
|
+
deps({
|
|
30
|
+
'math.sin': ['math.sin_core'],
|
|
31
|
+
'math.cos': ['math.cos_core'],
|
|
32
|
+
'math.sin_core': [],
|
|
33
|
+
'math.cos_core': [],
|
|
26
34
|
'math.tan': ['math.sin', 'math.cos'],
|
|
35
|
+
'math.exp': ['math.exp2'],
|
|
27
36
|
'math.expm1': ['math.exp'],
|
|
28
37
|
'math.log2': ['math.log'],
|
|
29
38
|
'math.log1p': ['math.log'],
|
|
@@ -40,7 +49,6 @@ export default (ctx) => {
|
|
|
40
49
|
'math.cbrt': ['math.isFinite', 'math.pow'],
|
|
41
50
|
'math.sumPrecise': ['__ptr_offset', '__len', '__alloc'],
|
|
42
51
|
})
|
|
43
|
-
|
|
44
52
|
// Helpers: all math ops take f64 and return f64. Args go through ToNumber
|
|
45
53
|
// (toNumF64) — ECMA Math methods coerce each argument, so null→0, undefined→NaN.
|
|
46
54
|
const f = (op, a) => typed([op, toNumF64(a, emit(a))], 'f64')
|
|
@@ -56,13 +64,76 @@ export default (ctx) => {
|
|
|
56
64
|
}
|
|
57
65
|
return false
|
|
58
66
|
}
|
|
67
|
+
// Emit-time fast path: call $math.sin_core/$math.cos_core directly instead of
|
|
68
|
+
// the $math.sin/$math.cos wrappers. sin_core still runs the isFinite guard
|
|
69
|
+
// (finite operands can overflow to ±Inf); this only saves a call indirection.
|
|
70
|
+
const isBoundName = (name) =>
|
|
71
|
+
typeof name === 'string' && (
|
|
72
|
+
repOf(name) != null ||
|
|
73
|
+
ctx.func.locals?.has(name) ||
|
|
74
|
+
ctx.func.current?.params?.some(p => p.name === name)
|
|
75
|
+
)
|
|
76
|
+
const isSinCoreFastPath = (src) => {
|
|
77
|
+
if (src == null) return false
|
|
78
|
+
if (typeof src === 'number') return Number.isFinite(src)
|
|
79
|
+
if (typeof src === 'string') return isIntCertain(src) || isBoundName(src)
|
|
80
|
+
if (!Array.isArray(src)) return false
|
|
81
|
+
const op = src[0]
|
|
82
|
+
// jz source literals: [null, n], [, bool], [null, null]
|
|
83
|
+
if (op == null && src.length === 2) {
|
|
84
|
+
const v = src[1]
|
|
85
|
+
if (typeof v === 'number') return Number.isFinite(v)
|
|
86
|
+
if (typeof v === 'boolean') return true
|
|
87
|
+
if (v == null) return true
|
|
88
|
+
}
|
|
89
|
+
if (op === 'literal') {
|
|
90
|
+
const v = src[1]
|
|
91
|
+
return typeof v === 'number' && Number.isFinite(v)
|
|
92
|
+
}
|
|
93
|
+
if (op === 'global' || op === 'param') return true
|
|
94
|
+
if (op === '()') {
|
|
95
|
+
const fn = src[1]
|
|
96
|
+
if (fn === 'math.PI' || fn === 'math.E' || fn === 'math.LN2' || fn === 'math.SQRT2') return true
|
|
97
|
+
if (typeof fn === 'string' && fn.startsWith('math.')) {
|
|
98
|
+
const finiteOps = new Set([
|
|
99
|
+
'math.abs', 'math.floor', 'math.ceil', 'math.trunc', 'math.round', 'math.sqrt',
|
|
100
|
+
'math.sin', 'math.cos', 'math.tan', 'math.imul', 'math.clz32',
|
|
101
|
+
])
|
|
102
|
+
if (finiteOps.has(fn)) return src.slice(2).every(isSinCoreFastPath)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (op === '|' && src.length === 3) return isSinCoreFastPath(src[1]) && isSinCoreFastPath(src[2])
|
|
106
|
+
if (op === '%' || op === '&' || op === '^' || op === '<<' || op === '>>' || op === '>>>') {
|
|
107
|
+
return src.slice(1).every(isSinCoreFastPath)
|
|
108
|
+
}
|
|
109
|
+
if (op === '+' || op === '-' || op === '*' || op === '**') {
|
|
110
|
+
return src.slice(1).every(isSinCoreFastPath)
|
|
111
|
+
}
|
|
112
|
+
if (op === '/') return isSinCoreFastPath(src[1]) && isSinCoreFastPath(src[2])
|
|
113
|
+
if (op === '?:' && src.length === 4) return isSinCoreFastPath(src[2]) && isSinCoreFastPath(src[3])
|
|
114
|
+
if ((op === '&&' || op === '||') && src.length === 3) return isSinCoreFastPath(src[1]) && isSinCoreFastPath(src[2])
|
|
115
|
+
if (op === '!' && src.length === 2) return isSinCoreFastPath(src[1])
|
|
116
|
+
if (op === '[]' && src.length === 3) {
|
|
117
|
+
// Chord tables / semitone indices — index is int-shaped, element is a small int.
|
|
118
|
+
return isSinCoreFastPath(src[2]) || isIntCertain(src[2])
|
|
119
|
+
}
|
|
120
|
+
if (op === '.' && src.length === 3 && src[2] === 'length') {
|
|
121
|
+
return typeof src[1] === 'string' || isSinCoreFastPath(src[1])
|
|
122
|
+
}
|
|
123
|
+
if (op === '.' && src.length === 3) return isIntCertain(src)
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
59
126
|
const fInt = (op, a) => isIntCertain(a) ? asF64(emit(a)) : f(op, a)
|
|
60
127
|
// ECMA Math methods perform ToNumber on each argument. toNumF64 short-circuits
|
|
61
128
|
// for known-number nodes, and routes everything else through __to_num so null→0,
|
|
62
129
|
// undefined→NaN, and strings get parsed. Without this, raw NaN-boxed pointers
|
|
63
130
|
// (null/undefined/strings) would propagate through math.log etc. and surface
|
|
64
131
|
// as the original null/undefined sentinel after decode.
|
|
65
|
-
|
|
132
|
+
// A canon'd operand feeding a math call is redundant: the callee (log/sin/exp/…)
|
|
133
|
+
// propagates a non-canonical NaN identically and re-canon-izes its own result.
|
|
134
|
+
// `Math.log(Math.log(x))` thus sheds the inner per-call select + f64.ne.
|
|
135
|
+
const stripCanon = (v) => (v && v.canonOf != null) ? typed(v.canonOf, 'f64') : v
|
|
136
|
+
const fn = (name, ...args) => typed(['call', `$${name}`, ...args.map(a => stripCanon(toNumF64(a, emit(a))))], 'f64')
|
|
66
137
|
|
|
67
138
|
// Canonicalize a possibly-NaN f64 result. A wasm arithmetic op that mints a
|
|
68
139
|
// fresh NaN (f64.sqrt of a negative, f64.min/max with a NaN operand) leaves
|
|
@@ -72,14 +143,39 @@ export default (ctx) => {
|
|
|
72
143
|
// untyped === / typeof. So fold any NaN back to canonical where one is born.
|
|
73
144
|
const canon = (node) => {
|
|
74
145
|
const t = temp('cn')
|
|
75
|
-
|
|
146
|
+
const ir = typed(['block', ['result', 'f64'],
|
|
76
147
|
['local.set', `$${t}`, node],
|
|
77
148
|
['select',
|
|
78
149
|
['f64.const', 'nan'],
|
|
79
150
|
['local.get', `$${t}`],
|
|
80
151
|
['f64.ne', ['local.get', `$${t}`], ['local.get', `$${t}`]]]], 'f64')
|
|
152
|
+
// Tag the wrapper so a NaN-propagating f64 consumer (`f64.add`/`mul`/… that
|
|
153
|
+
// itself canon-izes on escape) can strip the redundant inner canon: the raw
|
|
154
|
+
// op result `node` propagates a freshly-minted NaN identically through the
|
|
155
|
+
// consumer, and only the OUTERMOST escaping value needs the canonical form.
|
|
156
|
+
ir.canonOf = node
|
|
157
|
+
return ir
|
|
81
158
|
}
|
|
82
159
|
|
|
160
|
+
// sqrt(x) needs no NaN-canon when its argument is provably ≥ 0 with no spurious NaN:
|
|
161
|
+
// the result is then a normal non-negative f64 (or +0 / +inf, or a propagated input
|
|
162
|
+
// NaN that is already canonical), never a freshly-minted ±NaN. A sum of pure squares
|
|
163
|
+
// — the vector-length idiom sqrt(x*x + y*y + …) — is exactly that: every term is ≥ +0
|
|
164
|
+
// (even ±0·±0 = +0, ±inf² = +inf), the sum never cancels to inf−inf, and any NaN can
|
|
165
|
+
// only come from a NaN input (already the canonical number-NaN). So a distance /
|
|
166
|
+
// normalize loop sheds the per-sqrt select + f64.ne + local on its critical path.
|
|
167
|
+
// `pure` excludes call/load/tee, so two structurally-equal operands of a `*` really
|
|
168
|
+
// are the same value (a genuine square), not two side-effecting evaluations.
|
|
169
|
+
const pureF64 = (n) => !Array.isArray(n) ? true :
|
|
170
|
+
(n[0] === 'local.get' || n[0] === 'global.get' || n[0] === 'f64.const' || n[0] === 'i32.const' || n[0] === 'i64.const') ? true :
|
|
171
|
+
(n[0] === 'f64.add' || n[0] === 'f64.sub' || n[0] === 'f64.mul' || n[0] === 'f64.div' || n[0] === 'f64.neg' || n[0] === 'f64.abs' || n[0] === 'f64.convert_i32_s' || n[0] === 'f64.convert_i32_u') ? n.slice(1).every(pureF64) : false
|
|
172
|
+
const sameIR = (a, b) => Array.isArray(a) !== Array.isArray(b) ? false : !Array.isArray(a) ? a === b : (a.length === b.length && a.every((x, i) => sameIR(x, b[i])))
|
|
173
|
+
const nonNegF64 = (n) => !Array.isArray(n) ? false :
|
|
174
|
+
n[0] === 'f64.mul' ? (pureF64(n[1]) && sameIR(n[1], n[2])) : // x·x ≥ 0
|
|
175
|
+
n[0] === 'f64.add' ? (nonNegF64(n[1]) && nonNegF64(n[2])) : // (≥0) + (≥0) ≥ 0
|
|
176
|
+
n[0] === 'f64.const' ? (typeof n[1] === 'number' && n[1] >= 0) : false
|
|
177
|
+
const sqrtIR = (a) => { const ir = f('f64.sqrt', a); return nonNegF64(ir[1]) ? ir : canon(ir) }
|
|
178
|
+
|
|
83
179
|
// Constants
|
|
84
180
|
ctx.core.emit['math.PI'] = () => typed(['f64.const', Math.PI], 'f64')
|
|
85
181
|
ctx.core.emit['math.E'] = () => typed(['f64.const', Math.E], 'f64')
|
|
@@ -105,7 +201,7 @@ export default (ctx) => {
|
|
|
105
201
|
// Built-in WASM ops. sqrt/min/max mint a fresh NaN (sqrt of a negative, min/max
|
|
106
202
|
// with a NaN operand) whose sign is platform-nondeterministic — `canon` folds it
|
|
107
203
|
// back to the canonical pattern. abs/floor/ceil/trunc never produce a new NaN.
|
|
108
|
-
ctx.core.emit['math.sqrt'] = a =>
|
|
204
|
+
ctx.core.emit['math.sqrt'] = a => sqrtIR(a)
|
|
109
205
|
ctx.core.emit['math.abs'] = a => f('f64.abs', a)
|
|
110
206
|
ctx.core.emit['math.floor'] = a => fInt('f64.floor', a)
|
|
111
207
|
ctx.core.emit['math.ceil'] = a => fInt('f64.ceil', a)
|
|
@@ -151,36 +247,42 @@ export default (ctx) => {
|
|
|
151
247
|
ctx.core.emit['math.fround'] = a => typed(['f64.promote_f32', ['f32.demote_f64', toNumF64(a, emit(a))]], 'f64')
|
|
152
248
|
|
|
153
249
|
// Sign
|
|
154
|
-
|
|
250
|
+
reg('math.sign', ['math.sign'], a => fn('math.sign', a))
|
|
155
251
|
|
|
156
|
-
// Trig
|
|
157
|
-
ctx.core.emit['math.sin'] =
|
|
158
|
-
|
|
159
|
-
|
|
252
|
+
// Trig — isSinCoreFastPath skips the $math.sin/$math.cos wrapper call.
|
|
253
|
+
ctx.core.emit['math.sin'] = dual(
|
|
254
|
+
emitter(['math.sin'], a => fn('math.sin', a)),
|
|
255
|
+
emitter(['math.sin_core'], a => fn('math.sin_core', a)),
|
|
256
|
+
isSinCoreFastPath)
|
|
257
|
+
ctx.core.emit['math.cos'] = dual(
|
|
258
|
+
emitter(['math.cos'], a => fn('math.cos', a)),
|
|
259
|
+
emitter(['math.cos_core'], a => fn('math.cos_core', a)),
|
|
260
|
+
isSinCoreFastPath)
|
|
261
|
+
reg('math.tan', ['math.tan'], a => fn('math.tan', a))
|
|
160
262
|
|
|
161
263
|
// Inverse trig
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
264
|
+
reg('math.asin', ['math.asin'], a => fn('math.asin', a))
|
|
265
|
+
reg('math.acos', ['math.acos'], a => fn('math.acos', a))
|
|
266
|
+
reg('math.atan', ['math.atan'], a => fn('math.atan', a))
|
|
267
|
+
reg('math.atan2', ['math.atan2'], (a, b) => fn('math.atan2', a, b))
|
|
166
268
|
|
|
167
269
|
// Hyperbolic
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
270
|
+
reg('math.sinh', ['math.sinh'], a => fn('math.sinh', a))
|
|
271
|
+
reg('math.cosh', ['math.cosh'], a => fn('math.cosh', a))
|
|
272
|
+
reg('math.tanh', ['math.tanh'], a => fn('math.tanh', a))
|
|
171
273
|
|
|
172
274
|
// Inverse hyperbolic
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
275
|
+
reg('math.asinh', ['math.asinh'], a => fn('math.asinh', a))
|
|
276
|
+
reg('math.acosh', ['math.acosh'], a => fn('math.acosh', a))
|
|
277
|
+
reg('math.atanh', ['math.atanh'], a => fn('math.atanh', a))
|
|
176
278
|
|
|
177
279
|
// Exponential and logarithmic
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
280
|
+
reg('math.exp', ['math.exp'], a => fn('math.exp', a))
|
|
281
|
+
reg('math.expm1', ['math.expm1'], a => fn('math.expm1', a))
|
|
282
|
+
reg('math.log', ['math.log'], a => fn('math.log', a))
|
|
283
|
+
reg('math.log2', ['math.log2'], a => fn('math.log2', a))
|
|
284
|
+
reg('math.log10', ['math.log10'], a => fn('math.log10', a))
|
|
285
|
+
reg('math.log1p', ['math.log1p'], a => fn('math.log1p', a))
|
|
184
286
|
|
|
185
287
|
// Power. Constant-integer-exponent `Math.pow(x,n)` / `x ** n` (|n| ≤ POW_FOLD_MAX)
|
|
186
288
|
// lower to inline square-and-multiply instead of a $math.pow call. The fold is
|
|
@@ -190,7 +292,7 @@ export default (ctx) => {
|
|
|
190
292
|
// both the exact bits and the result sign (negative iff x<0 ∧ n odd, which is exactly
|
|
191
293
|
// its `neg_base`). A program whose only pow use is folded then never pulls the
|
|
192
294
|
// math.pow/exp/log stdlib. `**`'s exponent is parsed as a bare number (incl. negatives).
|
|
193
|
-
const POW_FOLD_MAX =
|
|
295
|
+
const POW_FOLD_MAX = 16
|
|
194
296
|
const get = name => ['local.get', `$${name}`]
|
|
195
297
|
const constInt = b => {
|
|
196
298
|
const v = typeof b === 'number' ? b
|
|
@@ -226,18 +328,52 @@ export default (ctx) => {
|
|
|
226
328
|
const inner = typed(['block', ['result', 'f64'], ...stmts, result], 'f64')
|
|
227
329
|
return (minted && !neverNaN(a, baseIR)) ? canon(inner) : inner
|
|
228
330
|
}
|
|
229
|
-
const
|
|
230
|
-
|
|
331
|
+
const constNum = b => typeof b === 'number' ? b
|
|
332
|
+
: (Array.isArray(b) && b.length === 2 && b[0] == null && typeof b[1] === 'number') ? b[1]
|
|
333
|
+
: null
|
|
334
|
+
// `x ** 0.5` folds to f64.sqrt instead of the exp/log $math.pow call — saves the
|
|
335
|
+
// whole pow/exp/log stdlib (the headline `dist` example drops from ~1.0kB to 70B)
|
|
336
|
+
// and runs at hardware-sqrt speed. f64.sqrt is correctly-rounded, so for every
|
|
337
|
+
// normal input it is bit-identical to V8's `Math.pow(x, 0.5)`, and it agrees with
|
|
338
|
+
// jz's own `Math.sqrt(x)` by construction (mirrors the math.sqrt emit: always
|
|
339
|
+
// canon, since a negative finite base yields a NaN whose sign needs canonicalizing).
|
|
340
|
+
// Two exotic inputs follow sqrt rather than Math.pow semantics — a deliberate
|
|
341
|
+
// trade in the same class as jz's other boundary divergences: `(-0) ** 0.5` is -0
|
|
342
|
+
// (Math.pow: +0; and -0 === 0), `(-Infinity) ** 0.5` is NaN (Math.pow: +Infinity).
|
|
343
|
+
// `** -0.5` is intentionally NOT folded: 1/sqrt double-rounds and loses the last
|
|
344
|
+
// ULP vs Math.pow's single rounding, so it keeps the exact $math.pow path.
|
|
345
|
+
const powCall = emitter(['math.pow'], (a, b) => fn('math.pow', a, b))
|
|
346
|
+
// base-2 power → dedicated $math.exp2(y) (skips exp's ×ln2 / ÷ln2 round-trip)
|
|
347
|
+
const exp2Call = emitter(['math.exp2'], (exp) => typed(['call', '$math.exp2', toNumF64(exp, emit(exp))], 'f64'))
|
|
348
|
+
// Shared pow/** lowering.
|
|
349
|
+
const emitPow = (a, b, allowExpPos) => {
|
|
231
350
|
const n = constInt(b)
|
|
232
|
-
|
|
351
|
+
if (n !== null && Math.abs(n) <= POW_FOLD_MAX) return foldPow(a, n)
|
|
352
|
+
if (constNum(b) === 0.5) { const ir = typed(['f64.sqrt', toNumF64(a, emit(a))], 'f64'); return nonNegF64(ir[1]) ? ir : canon(ir) }
|
|
353
|
+
// Both args are compile-time constants: evaluate now, emit f64.const.
|
|
354
|
+
// Catches pow(2, -2/12) where the arithmetic folds emit f64.const for both sides.
|
|
355
|
+
const ca = constNum(a), cb = constNum(b)
|
|
356
|
+
if (ca !== null && cb !== null) return typed(['f64.const', Math.pow(ca, cb)], 'f64')
|
|
357
|
+
// IR-level fold: peek at emitted IR for both args — e.g. -2/12 emits f64.const -0.1666.
|
|
358
|
+
// We emit, check, and if not foldable, the emitted IR is used by the fallthrough paths.
|
|
359
|
+
const irA = toNumF64(a, emit(a)), irB = toNumF64(b, emit(b))
|
|
360
|
+
if (isLit(irA) && isLit(irB)) return typed(['f64.const', Math.pow(litVal(irA), litVal(irB))], 'f64')
|
|
361
|
+
// base 2 → dedicated 2^y (exp2 is exact for integer y, and skips exp's ×ln2/÷ln2).
|
|
362
|
+
// Every other literal base keeps $math.pow: `exp(y·ln base)` would lose ulps and,
|
|
363
|
+
// worse, miss Math.pow's integer-exponent semantics — e.g. `16 ** flen` with a
|
|
364
|
+
// runtime-integer flen must reproduce the exact square-and-multiply value (2⁵² for
|
|
365
|
+
// flen=13), which only $math.pow's integer fast path delivers.
|
|
366
|
+
if (allowExpPos && isLit(irA) && litVal(irA) === 2 && n === null)
|
|
367
|
+
return (inc('math.exp2'), typed(['call', '$math.exp2', irB], 'f64'))
|
|
368
|
+
return (inc('math.pow'), typed(['call', '$math.pow', irA, irB], 'f64'))
|
|
233
369
|
}
|
|
234
|
-
ctx.core.emit['math.pow']
|
|
235
|
-
ctx.core.emit['**'] =
|
|
236
|
-
|
|
237
|
-
|
|
370
|
+
ctx.core.emit['math.pow'] = tag((a, b) => emitPow(a, b, true), powCall.deps)
|
|
371
|
+
ctx.core.emit['**'] = tag((a, b) => emitPow(a, b, true), powCall.deps)
|
|
372
|
+
reg('math.cbrt', ['math.cbrt'], a => fn('math.cbrt', a))
|
|
373
|
+
reg('math.hypot', ['math.hypot'], (a, b, ...rest) => {
|
|
238
374
|
if (a === undefined) return typed(['f64.const', 0], 'f64')
|
|
239
375
|
if (b === undefined) return f('f64.abs', a)
|
|
240
|
-
let r =
|
|
376
|
+
let r = fn('math.hypot', a, b)
|
|
241
377
|
// ToNumber every rest arg too (matches min/max) — an object arg's valueOf
|
|
242
378
|
// must run and may throw, which Math.hypot propagates.
|
|
243
379
|
for (const x of rest) r = typed(['call', '$math.hypot', r, toNumF64(x, emit(x))], 'f64')
|
|
@@ -246,7 +382,7 @@ export default (ctx) => {
|
|
|
246
382
|
|
|
247
383
|
// Math.sumPrecise(iterable) — exact, correctly-rounded summation (ECMA-262).
|
|
248
384
|
// jz models the array case; the WAT routine sums via a fixed-point accumulator.
|
|
249
|
-
|
|
385
|
+
reg('math.sumPrecise', ['math.sumPrecise'], arr =>
|
|
250
386
|
typed(['call', '$math.sumPrecise', ['i64.reinterpret_f64', asF64(emit(arr))]], 'f64'))
|
|
251
387
|
|
|
252
388
|
// Integer/bit operations: return i32 directly. Consumers `asF64`-rebox at
|
|
@@ -258,99 +394,296 @@ export default (ctx) => {
|
|
|
258
394
|
ctx.core.emit['math.imul'] = (a, b) => typed(['i32.mul', toI32(emit(a)), toI32(emit(b))], 'i32')
|
|
259
395
|
|
|
260
396
|
// Random
|
|
261
|
-
|
|
397
|
+
reg('math.random', ['math.random'], () => {
|
|
398
|
+
// Entropy mode: pull the host randomness syscall on demand (only when
|
|
399
|
+
// Math.random is actually used) — env.rngSeed (JS host) or WASI random_get.
|
|
400
|
+
if (rngEntropy) {
|
|
401
|
+
if (ctx.transform.host === 'wasi')
|
|
402
|
+
hostImport('wasi_snapshot_preview1', 'random_get', ['func', '$__random_get', ['param', 'i32'], ['param', 'i32'], ['result', 'i32']])
|
|
403
|
+
else
|
|
404
|
+
hostImport('env', 'rngSeed', ['func', '$__env_rng_seed', ['result', 'i32']])
|
|
405
|
+
}
|
|
406
|
+
return typed(['call', '$math.random'], 'f64')
|
|
407
|
+
})
|
|
262
408
|
|
|
263
409
|
// ============================================
|
|
264
410
|
// WAT stdlib implementations
|
|
265
411
|
// ============================================
|
|
266
412
|
|
|
267
|
-
|
|
413
|
+
wat('math.sign', `(func $math.sign (param $x f64) (result f64)
|
|
268
414
|
;; sign(NaN) = NaN, sign(±0) = ±0 — both pass x through unchanged.
|
|
269
415
|
(if (f64.ne (local.get $x) (local.get $x)) (then (return (local.get $x))))
|
|
270
416
|
(if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
|
|
271
417
|
(if (result f64) (f64.gt (local.get $x) (f64.const 0.0))
|
|
272
418
|
(then (f64.const 1.0))
|
|
273
|
-
(else (f64.const -1.0))))`
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
(local $
|
|
320
|
-
(
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
(local.set $
|
|
325
|
-
(local.set $
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
(local.get $
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
(f64.
|
|
419
|
+
(else (f64.const -1.0))))`)
|
|
420
|
+
|
|
421
|
+
// sin/cos over the folded range [0, π/2] use a 5-term MINIMAX polynomial in x² (Horner
|
|
422
|
+
// form, generated below). It beats the prior 6-term Taylor on both counts: one fewer
|
|
423
|
+
// multiply (faster — the floatbeat synth is sin-bound), and lower error (sin ≤ 1.9e-8,
|
|
424
|
+
// cos ≤ 1.3e-7 vs Taylor's ~6e-8 / ~5e-7) — minimax spreads error evenly across the range
|
|
425
|
+
// instead of piling unused precision near 0. Coeffs fit by scripts/minimax-trig.mjs.
|
|
426
|
+
const horner = (cs, v) => cs.reduceRight((acc, c, i) =>
|
|
427
|
+
i === cs.length - 1 ? `(f64.const ${c})`
|
|
428
|
+
: `(f64.add (f64.const ${c}) (f64.mul (local.get ${v}) ${acc}))`, '')
|
|
429
|
+
const SIN_C = [1, -0.16666660296130772, 0.008333091744946387, -0.00019811771757028443, 0.000002611054662215034]
|
|
430
|
+
const COS_C = [1, -0.4999993043717576, 0.04166402742354027, -0.0013856638518363177, 0.00002321737177898552]
|
|
431
|
+
// 2^f over the reduced range f ∈ [-0.5, 0.5] for $math.exp2 (rel. err ≤ 6e-9). Lets the
|
|
432
|
+
// base-2 power `2**y` skip the ×ln2 / ÷ln2 round-trip exp(y·ln2) pays — see $math.exp2.
|
|
433
|
+
const EXP2_C = [1, 0.6931472000619209, 0.24022650999918949, 0.05550340682450019, 0.009618048870444599, 0.0013395279077191057, 0.00015463102004723134]
|
|
434
|
+
// Range-reduction constants embedded as exact round-trip decimal STRINGS, not
|
|
435
|
+
// `${Math.PI}`/`${1/Math.PI}` number interpolation. These multiply the (possibly
|
|
436
|
+
// astronomically large) argument, so any lost digit wrecks large-arg reduction. Under
|
|
437
|
+
// self-host the kernel formats `${number}` through __ftoa — a 9-significant-digit dtoa
|
|
438
|
+
// (module/number.js) — which would bake "3.14159265"/"0.318309886" into the WAT and
|
|
439
|
+
// throw the reduced quadrant off (Δ≈0.2 at x≈2267). A string interpolates verbatim, so
|
|
440
|
+
// watr parses the full-precision f64 in both legs. Values are native toString's shortest
|
|
441
|
+
// round-trip reprs of Math.PI, 1/Math.PI, Math.PI/2 — byte-identical to the old output.
|
|
442
|
+
const PI = '3.141592653589793', INV_PI = '0.3183098861837907', HALF_PI = '1.5707963267948966'
|
|
443
|
+
|
|
444
|
+
// Round-to-nearest reduction r = x − q·π ∈ [−π/2, π/2], in pure f64 — no int conversion,
|
|
445
|
+
// so it never traps and never saturates. A SECOND pass folds the q·π rounding error back
|
|
446
|
+
// in, keeping r bounded even for astronomically large x where the first pass loses all
|
|
447
|
+
// precision: Math.sin must return a value in [−1,1] for every finite input, not Inf/garbage.
|
|
448
|
+
// For |x| ≲ 1e15 the second pass is a no-op (q2 = 0) and the result is bit-identical to a
|
|
449
|
+
// single reduction. The odd poly r·P(r²) handles r<0 on its own (sin is odd); the sign is the
|
|
450
|
+
// parity of the total quotient, taken in f64 as q − 2·round(q/2). ×(1/π) avoids a divide.
|
|
451
|
+
// ±Infinity and NaN must return NaN. Guard before reduction instead of relying on
|
|
452
|
+
// Inf−Inf·π to mint one: that arithmetic NaN has platform-dependent bits and can
|
|
453
|
+
// escape as a non-canonical NaN-box on x86/Linux.
|
|
454
|
+
// The second reduction pass only corrects an r that the first pass left outside
|
|
455
|
+
// [−π/2, π/2]; for in-range r, q2 is 0 and the pass is a no-op, so gating it on
|
|
456
|
+
// |r| > π/2 is bit-identical for all finite inputs while sparing the common case
|
|
457
|
+
// ~6 ops. Both are generic wins for every sin/cos/tan/exp-via-trig call site.
|
|
458
|
+
wat('math.sin_core', `(func $math.sin_core (param $x f64) (result f64)
|
|
459
|
+
(local $q f64) (local $q2 f64) (local $r f64) (local $r2 f64)
|
|
460
|
+
(if (f64.ne (local.get $x) (local.get $x)) (then (return (f64.const nan))))
|
|
461
|
+
(if (f64.eq (f64.abs (local.get $x)) (f64.const inf)) (then (return (f64.const nan))))
|
|
462
|
+
;; |x| ≤ 2⁻²⁷: sin(x) = x to within a fraction of an ulp, and returning x preserves the
|
|
463
|
+
;; sign of ±0 (the range reduction below would turn -0 into +0: -0 − (-0·π) = +0).
|
|
464
|
+
(if (f64.lt (f64.abs (local.get $x)) (f64.const ${2 ** -27})) (then (return (local.get $x))))
|
|
465
|
+
(local.set $q (f64.nearest (f64.mul (local.get $x) (f64.const ${INV_PI}))))
|
|
466
|
+
(local.set $r (f64.sub (local.get $x) (f64.mul (local.get $q) (f64.const ${PI}))))
|
|
467
|
+
(if (f64.gt (f64.abs (local.get $r)) (f64.const ${HALF_PI}))
|
|
468
|
+
(then
|
|
469
|
+
(local.set $q2 (f64.nearest (f64.mul (local.get $r) (f64.const ${INV_PI}))))
|
|
470
|
+
(local.set $r (f64.sub (local.get $r) (f64.mul (local.get $q2) (f64.const ${PI}))))
|
|
471
|
+
(local.set $q (f64.add (local.get $q) (local.get $q2)))))
|
|
472
|
+
(local.set $q (f64.sub (local.get $q) (f64.mul (f64.const 2) (f64.nearest (f64.mul (local.get $q) (f64.const 0.5))))))
|
|
473
|
+
(local.set $r2 (f64.mul (local.get $r) (local.get $r)))
|
|
474
|
+
(local.set $r (f64.mul (local.get $r) ${horner(SIN_C, '$r2')}))
|
|
475
|
+
;; Negate for odd quasiperiods
|
|
476
|
+
(if (f64.gt (f64.abs (local.get $q)) (f64.const 0.5)) (then (local.set $r (f64.neg (local.get $r)))))
|
|
477
|
+
;; Clamp to [-1, 1]: polynomial approximation can overshoot by ~1e-8 near peaks.
|
|
478
|
+
;; Branchless (f64.min/f64.max) avoids branch misprediction near peaks.
|
|
479
|
+
(f64.min (f64.max (local.get $r) (f64.const -1.0)) (f64.const 1.0)))`)
|
|
480
|
+
|
|
481
|
+
wat('math.sin', `(func $math.sin (param $x f64) (result f64)
|
|
482
|
+
(call $math.sin_core (local.get $x)))`)
|
|
483
|
+
|
|
484
|
+
wat('math.cos_core', `(func $math.cos_core (param $x f64) (result f64)
|
|
485
|
+
(local $q f64) (local $q2 f64) (local $r f64) (local $r2 f64)
|
|
486
|
+
(if (f64.ne (local.get $x) (local.get $x)) (then (return (f64.const nan))))
|
|
487
|
+
(if (f64.eq (f64.abs (local.get $x)) (f64.const inf)) (then (return (f64.const nan))))
|
|
488
|
+
(local.set $q (f64.nearest (f64.mul (local.get $x) (f64.const ${INV_PI}))))
|
|
489
|
+
(local.set $r (f64.sub (local.get $x) (f64.mul (local.get $q) (f64.const ${PI}))))
|
|
490
|
+
(if (f64.gt (f64.abs (local.get $r)) (f64.const ${HALF_PI}))
|
|
491
|
+
(then
|
|
492
|
+
(local.set $q2 (f64.nearest (f64.mul (local.get $r) (f64.const ${INV_PI}))))
|
|
493
|
+
(local.set $r (f64.sub (local.get $r) (f64.mul (local.get $q2) (f64.const ${PI}))))
|
|
494
|
+
(local.set $q (f64.add (local.get $q) (local.get $q2)))))
|
|
495
|
+
(local.set $q (f64.sub (local.get $q) (f64.mul (f64.const 2) (f64.nearest (f64.mul (local.get $q) (f64.const 0.5))))))
|
|
496
|
+
(local.set $r2 (f64.mul (local.get $r) (local.get $r)))
|
|
497
|
+
(local.set $r ${horner(COS_C, '$r2')})
|
|
498
|
+
;; Negate for odd quasiperiods
|
|
499
|
+
(if (f64.gt (f64.abs (local.get $q)) (f64.const 0.5)) (then (local.set $r (f64.neg (local.get $r)))))
|
|
500
|
+
;; Clamp to [-1, 1]: polynomial approximation can overshoot by ~1e-8 near peaks.
|
|
501
|
+
;; Branchless (f64.min/f64.max) avoids branch misprediction near peaks.
|
|
502
|
+
(f64.min (f64.max (local.get $r) (f64.const -1.0)) (f64.const 1.0)))`)
|
|
503
|
+
|
|
504
|
+
wat('math.cos', `(func $math.cos (param $x f64) (result f64)
|
|
505
|
+
(call $math.cos_core (local.get $x)))`)
|
|
506
|
+
|
|
507
|
+
wat('math.tan', `(func $math.tan (param $x f64) (result f64)
|
|
508
|
+
(if (f64.ne (local.get $x) (local.get $x)) (then (return (f64.const nan))))
|
|
509
|
+
(if (f64.eq (f64.abs (local.get $x)) (f64.const inf)) (then (return (f64.const nan))))
|
|
510
|
+
(f64.div (call $math.sin (local.get $x)) (call $math.cos (local.get $x))))`)
|
|
511
|
+
|
|
512
|
+
// ── f64x2 SIMD sin/cos — both lanes through one polynomial ───────────────────
|
|
513
|
+
// The scalar sin_core/cos_core algorithm lifted to two f64 lanes: same
|
|
514
|
+
// round-to-nearest π reduction, same minimax poly (SIN_C/COS_C), same quadrant
|
|
515
|
+
// parity — but every branch becomes branchless so two independent angles cost one
|
|
516
|
+
// evaluation. A kernel computing sin and cos of distinct args (rotations, de Jong /
|
|
517
|
+
// Clifford maps, oscillator banks) packs them two-per-vector and ≈halves trig cost.
|
|
518
|
+
// • Both reduction passes run unconditionally: for an in-range r the second pass'
|
|
519
|
+
// q2 = nearest(r/π) = 0, so it's an exact no-op — no per-lane branch needed, and
|
|
520
|
+
// it still rescues |x| up to ~1e15 just like the scalar's gated pass.
|
|
521
|
+
// • NaN and ±∞ fall out as NaN through the arithmetic (∞ − ∞·π = NaN); a v128 lane
|
|
522
|
+
// is raw f64, not a NaN-box, so the canonical-NaN guard the scalar needs is moot.
|
|
523
|
+
// • Sign flip for odd quadrants is `r XOR (mask & −0.0)` (mask = |q|>0.5); final
|
|
524
|
+
// min/max clamps the ~1e-8 poly overshoot to [−1,1], same as scalar.
|
|
525
|
+
const splat = (c) => `(f64x2.splat (f64.const ${c}))`
|
|
526
|
+
const horner2 = (cs, v = '$r2') => cs.reduceRight((acc, c, i) =>
|
|
527
|
+
i === cs.length - 1 ? splat(c)
|
|
528
|
+
: `(f64x2.add ${splat(c)} (f64x2.mul (local.get ${v}) ${acc}))`, '')
|
|
529
|
+
// fdlibm log's even/odd split, as f64x2 coefficient arrays (the scalar $math.log inlines them):
|
|
530
|
+
// t1 = w·(L3 + w·(L5 + w·L7)), t2 = z·(L2 + w·(L4 + w·(L6 + w·L8))). Vectorized log_v reuses these.
|
|
531
|
+
const LOG_T1 = [0.3999999999940941908, 0.2222219843214978396, 0.1531383769920937332]
|
|
532
|
+
const LOG_T2 = [0.6666666666666735130, 0.2857142874366239149, 0.1818357216161805012, 0.1479819860511658591]
|
|
533
|
+
// Shared reduce → r ∈ [−π/2,π/2] in $r, quadrant parity in $q (branchless, 2 passes).
|
|
534
|
+
const reduce2 = `
|
|
535
|
+
(local.set $q (f64x2.nearest (f64x2.mul (local.get $x) ${splat(INV_PI)})))
|
|
536
|
+
(local.set $r (f64x2.sub (local.get $x) (f64x2.mul (local.get $q) ${splat(PI)})))
|
|
537
|
+
(local.set $q2 (f64x2.nearest (f64x2.mul (local.get $r) ${splat(INV_PI)})))
|
|
538
|
+
(local.set $r (f64x2.sub (local.get $r) (f64x2.mul (local.get $q2) ${splat(PI)})))
|
|
539
|
+
(local.set $q (f64x2.add (local.get $q) (local.get $q2)))
|
|
540
|
+
(local.set $q (f64x2.sub (local.get $q) (f64x2.mul ${splat(2)} (f64x2.nearest (f64x2.mul (local.get $q) ${splat(0.5)})))))
|
|
541
|
+
(local.set $r2 (f64x2.mul (local.get $r) (local.get $r)))`
|
|
542
|
+
// r XOR (|q|>0.5 ? −0.0 : 0), then clamp to [−1,1].
|
|
543
|
+
const signClamp = `
|
|
544
|
+
(local.set $r (v128.xor (local.get $r)
|
|
545
|
+
(v128.and (f64x2.gt (f64x2.abs (local.get $q)) ${splat(0.5)}) ${splat('-0.0')})))
|
|
546
|
+
(f64x2.min (f64x2.max (local.get $r) ${splat(-1)}) ${splat(1)})`
|
|
547
|
+
wat('math.sin2', `(func $math.sin2 (param $x v128) (result v128)
|
|
548
|
+
(local $q v128) (local $q2 v128) (local $r v128) (local $r2 v128)${reduce2}
|
|
549
|
+
(local.set $r (f64x2.mul (local.get $r) ${horner2(SIN_C)}))${signClamp})`)
|
|
550
|
+
wat('math.cos2', `(func $math.cos2 (param $x v128) (result v128)
|
|
551
|
+
(local $q v128) (local $q2 v128) (local $r v128) (local $r2 v128)${reduce2}
|
|
552
|
+
(local.set $r ${horner2(COS_C)})${signClamp})`)
|
|
553
|
+
// pow has no cheap 2-lane polynomial (it is exp(y·ln x) with cancellation-sensitive reductions),
|
|
554
|
+
// so the f64x2 mirror computes each lane with the scalar $math.pow and repacks — BIT-EXACT by
|
|
555
|
+
// construction. No transcendental speedup, but it keeps a pow-bearing pixel kernel's surrounding
|
|
556
|
+
// f64x2 arithmetic vectorized (the per-pixel-color pass only emits this when a truly-2-wide op —
|
|
557
|
+
// sin2/cos2/sqrt — already justifies the pair, so the extract/repack never makes a kernel slower).
|
|
558
|
+
wat('math.pow2', `(func $math.pow2 (param $x v128) (param $y v128) (result v128)
|
|
559
|
+
(f64x2.replace_lane 1
|
|
560
|
+
(f64x2.splat (call $math.pow (f64x2.extract_lane 0 (local.get $x)) (f64x2.extract_lane 0 (local.get $y))))
|
|
561
|
+
(call $math.pow (f64x2.extract_lane 1 (local.get $x)) (f64x2.extract_lane 1 (local.get $y)))))`, ['math.pow'])
|
|
562
|
+
|
|
563
|
+
// atan2/hypot/log have no cheap 2-lane polynomial (multi-`return` fdlibm bodies), so — like pow2 —
|
|
564
|
+
// each f64x2 mirror computes both lanes with the SCALAR helper and repacks: BIT-EXACT by
|
|
565
|
+
// construction. The per-pixel-color pass only emits these when a truly-2-wide op (sin2/cos2/sqrt)
|
|
566
|
+
// already justifies the f64x2 pair, so the extract/repack never makes a kernel slower.
|
|
567
|
+
// NOTE: names avoid the $math.log2/$math.exp2 collision (those are log-/exp-BASE-2).
|
|
568
|
+
wat('math.atan2_2', `(func $math.atan2_2 (param $y v128) (param $x v128) (result v128)
|
|
569
|
+
(f64x2.replace_lane 1
|
|
570
|
+
(f64x2.splat (call $math.atan2 (f64x2.extract_lane 0 (local.get $y)) (f64x2.extract_lane 0 (local.get $x))))
|
|
571
|
+
(call $math.atan2 (f64x2.extract_lane 1 (local.get $y)) (f64x2.extract_lane 1 (local.get $x)))))`, ['math.atan2'])
|
|
572
|
+
wat('math.hypot_2', `(func $math.hypot_2 (param $x v128) (param $y v128) (result v128)
|
|
573
|
+
(f64x2.replace_lane 1
|
|
574
|
+
(f64x2.splat (call $math.hypot (f64x2.extract_lane 0 (local.get $x)) (f64x2.extract_lane 0 (local.get $y))))
|
|
575
|
+
(call $math.hypot (f64x2.extract_lane 1 (local.get $x)) (f64x2.extract_lane 1 (local.get $y)))))`, ['math.hypot'])
|
|
576
|
+
// True f64x2 log — both lanes through one fdlibm poly (≈2× over two scalar calls). The HOT path
|
|
577
|
+
// (both lanes a normal finite x>0) mirrors $math.log's normal branch op-for-op: bit-exact (the
|
|
578
|
+
// sqrt2-center conditional becomes a per-lane bitselect; the i32 exponent k becomes an f64 via the
|
|
579
|
+
// 2^52 magic-add, identical to convert_i32_s for |k|≤1075). Any other lane (≤0/∞/NaN/denormal)
|
|
580
|
+
// routes BOTH lanes to the scalar fallback → bit-exact by construction, edges never lose precision.
|
|
581
|
+
wat('math.log_v', `(func $math.log_v (param $x v128) (result v128)
|
|
582
|
+
(local $k v128) (local $m v128) (local $mask v128) (local $f v128) (local $s v128) (local $z v128) (local $w v128) (local $hfsq v128)
|
|
583
|
+
(if (result v128)
|
|
584
|
+
(i64x2.all_true (v128.and
|
|
585
|
+
(f64x2.ge (local.get $x) (f64x2.splat (f64.const 0x1p-1022)))
|
|
586
|
+
(f64x2.lt (local.get $x) (f64x2.splat (f64.const inf)))))
|
|
587
|
+
(then
|
|
588
|
+
(local.set $k (f64x2.sub
|
|
589
|
+
(v128.or (v128.and (i64x2.shr_u (local.get $x) (i32.const 52)) (i64x2.splat (i64.const 0x7ff)))
|
|
590
|
+
(i64x2.splat (i64.const 0x4330000000000000)))
|
|
591
|
+
(f64x2.splat (f64.const 4503599627371519))))
|
|
592
|
+
(local.set $m (v128.or (v128.and (local.get $x) (i64x2.splat (i64.const 0x000fffffffffffff))) (i64x2.splat (i64.const 0x3ff0000000000000))))
|
|
593
|
+
(local.set $mask (f64x2.ge (local.get $m) (f64x2.splat (f64.const 1.4142135623730951))))
|
|
594
|
+
(local.set $m (v128.bitselect (f64x2.mul (local.get $m) (f64x2.splat (f64.const 0.5))) (local.get $m) (local.get $mask)))
|
|
595
|
+
(local.set $k (f64x2.add (local.get $k) (v128.and (local.get $mask) (f64x2.splat (f64.const 1.0)))))
|
|
596
|
+
(local.set $f (f64x2.sub (local.get $m) (f64x2.splat (f64.const 1.0))))
|
|
597
|
+
(local.set $s (f64x2.div (local.get $f) (f64x2.add (local.get $f) (f64x2.splat (f64.const 2.0)))))
|
|
598
|
+
(local.set $z (f64x2.mul (local.get $s) (local.get $s)))
|
|
599
|
+
(local.set $w (f64x2.mul (local.get $z) (local.get $z)))
|
|
600
|
+
(local.set $hfsq (f64x2.mul (f64x2.splat (f64.const 0.5)) (f64x2.mul (local.get $f) (local.get $f))))
|
|
601
|
+
(f64x2.add
|
|
602
|
+
(f64x2.mul (local.get $k) (f64x2.splat (f64.const ${Math.LN2})))
|
|
603
|
+
(f64x2.add (f64x2.sub (local.get $f) (local.get $hfsq))
|
|
604
|
+
(f64x2.mul (local.get $s) (f64x2.add (local.get $hfsq)
|
|
605
|
+
(f64x2.add
|
|
606
|
+
(f64x2.mul (local.get $w) ${horner2(LOG_T1, '$w')})
|
|
607
|
+
(f64x2.mul (local.get $z) ${horner2(LOG_T2, '$w')})))))))
|
|
608
|
+
(else
|
|
609
|
+
(f64x2.replace_lane 1
|
|
610
|
+
(f64x2.splat (call $math.log (f64x2.extract_lane 0 (local.get $x))))
|
|
611
|
+
(call $math.log (f64x2.extract_lane 1 (local.get $x)))))))`, ['math.log'])
|
|
612
|
+
|
|
613
|
+
// True f64x2 exp2 — hot path (round(y) ∈ (−1023,1024), the normal-result range) mirrors $math.exp2's
|
|
614
|
+
// single-IEEE-build branch op-for-op (Horner over f=y−round(y), 2^k via (k+1023)<<52); edges
|
|
615
|
+
// (overflow/underflow/denormal/NaN) route both lanes to the scalar fallback → bit-exact.
|
|
616
|
+
wat('math.exp2_v', `(func $math.exp2_v (param $y v128) (result v128)
|
|
617
|
+
(local $k v128) (local $f v128)
|
|
618
|
+
(local.set $k (f64x2.nearest (local.get $y)))
|
|
619
|
+
(if (result v128)
|
|
620
|
+
(i64x2.all_true (v128.and
|
|
621
|
+
(f64x2.gt (local.get $k) (f64x2.splat (f64.const -1023)))
|
|
622
|
+
(f64x2.lt (local.get $k) (f64x2.splat (f64.const 1024)))))
|
|
623
|
+
(then
|
|
624
|
+
(local.set $f (f64x2.sub (local.get $y) (local.get $k)))
|
|
625
|
+
(f64x2.mul ${horner2(EXP2_C, '$f')}
|
|
626
|
+
(i64x2.shl (i64x2.add
|
|
627
|
+
(i64x2.extend_low_i32x4_s (i32x4.trunc_sat_f64x2_s_zero (local.get $k)))
|
|
628
|
+
(i64x2.splat (i64.const 1023))) (i32.const 52))))
|
|
629
|
+
(else
|
|
630
|
+
(f64x2.replace_lane 1
|
|
631
|
+
(f64x2.splat (call $math.exp2 (f64x2.extract_lane 0 (local.get $y))))
|
|
632
|
+
(call $math.exp2 (f64x2.extract_lane 1 (local.get $y)))))))`, ['math.exp2'])
|
|
633
|
+
|
|
634
|
+
// e^x = 2^(x·log2e) — defers to exp2_v exactly as scalar $math.exp defers to $math.exp2. Bit-exact.
|
|
635
|
+
wat('math.exp_v', `(func $math.exp_v (param $x v128) (result v128)
|
|
636
|
+
(call $math.exp2_v (f64x2.mul (local.get $x) (f64x2.splat (f64.const ${Math.LOG2E})))))`, ['math.exp2_v'])
|
|
637
|
+
|
|
638
|
+
// e^x = 2^(x·log2 e) — defer to the faster $math.exp2 (one multiply, no division, and
|
|
639
|
+
// exp2's NaN/overflow/underflow guards cover exp's). Accurate to exp2's ~6e-9, better
|
|
640
|
+
// than the old 7-term Taylor, and it shares one code path with `2**`.
|
|
641
|
+
wat('math.exp', `(func $math.exp (param $x f64) (result f64)
|
|
642
|
+
(call $math.exp2 (f64.mul (local.get $x) (f64.const ${Math.LOG2E}))))`)
|
|
643
|
+
|
|
644
|
+
// 2^y, the dedicated base-2 power. `2**y` lowers here instead of exp(y·ln2): no ×ln2
|
|
645
|
+
// (so no reciprocal cancellation against exp's ÷ln2), a poly over the tighter [-0.5,0.5],
|
|
646
|
+
// and the same O(1) IEEE-exponent build of 2^k. ~6e-9 rel. error — well inside tolerance.
|
|
647
|
+
wat('math.exp2', `(func $math.exp2 (param $y f64) (result f64)
|
|
648
|
+
(local $k i32) (local $f f64) (local $k2 i32) (local $p f64)
|
|
649
|
+
(if (f64.ne (local.get $y) (local.get $y)) (then (return (local.get $y))))
|
|
650
|
+
(if (result f64) (f64.gt (local.get $y) (f64.const 1024.0)) (then (f64.const inf)) (else
|
|
651
|
+
(if (result f64) (f64.lt (local.get $y) (f64.const -1075.0)) (then (f64.const 0.0)) (else
|
|
652
|
+
(local.set $k (i32.trunc_f64_s (f64.nearest (local.get $y))))
|
|
653
|
+
(local.set $f (f64.sub (local.get $y) (f64.convert_i32_s (local.get $k))))
|
|
654
|
+
(local.set $p ${horner(EXP2_C, '$f')})
|
|
655
|
+
;; 2^k via a single IEEE-exponent build for the normal range (the hot path); the
|
|
656
|
+
;; two-factor split (2^k2 · 2^(k−k2)) is only needed at the denormal/overflow edges.
|
|
657
|
+
;; For normal k both are bit-identical (powers of two multiply exactly) — free speedup.
|
|
658
|
+
(if (result f64)
|
|
659
|
+
(i32.and (i32.gt_s (local.get $k) (i32.const -1023)) (i32.lt_s (local.get $k) (i32.const 1024)))
|
|
660
|
+
(then (f64.mul (local.get $p)
|
|
661
|
+
(f64.reinterpret_i64 (i64.shl (i64.extend_i32_s (i32.add (local.get $k) (i32.const 1023))) (i64.const 52)))))
|
|
662
|
+
(else
|
|
663
|
+
(local.set $k2 (i32.shr_s (local.get $k) (i32.const 1)))
|
|
664
|
+
(f64.mul (f64.mul (local.get $p)
|
|
665
|
+
(f64.reinterpret_i64 (i64.shl (i64.extend_i32_s (i32.add (local.get $k2) (i32.const 1023))) (i64.const 52))))
|
|
666
|
+
(f64.reinterpret_i64 (i64.shl (i64.extend_i32_s (i32.add (i32.sub (local.get $k) (local.get $k2)) (i32.const 1023))) (i64.const 52)))))))))))`)
|
|
667
|
+
|
|
668
|
+
// Maclaurin coefficients 1/1!…1/8! for e^x−1 = x·(1 + x/2! + x²/3! + …), Horner-nested
|
|
669
|
+
// (built with an explicit right-to-left fold so the parens stay balanced — and so the
|
|
670
|
+
// builder uses only constructs the self-host kernel can compile: Array.reduceRight is
|
|
671
|
+
// not in jz's runtime, so under the kernel it returns undefined and that token lands
|
|
672
|
+
// verbatim in the emitted WAT).
|
|
673
|
+
const expm1Coef = [1, 1 / 2, 1 / 6, 1 / 24, 1 / 120, 1 / 720, 1 / 5040, 1 / 40320]
|
|
674
|
+
let expm1Series = ''
|
|
675
|
+
for (let i = expm1Coef.length - 1; i >= 0; i--)
|
|
676
|
+
expm1Series = expm1Series
|
|
677
|
+
? `(f64.add (f64.const ${expm1Coef[i]}) (f64.mul (local.get $x) ${expm1Series}))`
|
|
678
|
+
: `(f64.const ${expm1Coef[i]})`
|
|
679
|
+
wat('math.expm1', `(func $math.expm1 (param $x f64) (result f64)
|
|
680
|
+
;; expm1(x) = e^x − 1. For |x| < 0.5 sum the series directly: there e^x is within ~1.6
|
|
681
|
+
;; of 1, so exp(x)−1 cancels the leading digits (the prior naive form lost up to ~11%
|
|
682
|
+
;; near 0); the series doesn't, and the leading x·(…) preserves the sign of ±0. Larger
|
|
683
|
+
;; |x| has no cancellation, so exp(x)−1 is accurate.
|
|
684
|
+
(if (result f64) (f64.lt (f64.abs (local.get $x)) (f64.const 0.5))
|
|
685
|
+
(then (f64.mul (local.get $x) ${expm1Series}))
|
|
686
|
+
(else (f64.sub (call $math.exp (local.get $x)) (f64.const 1.0)))))`)
|
|
354
687
|
|
|
355
688
|
// log(x) via bit-level frexp + sqrt(2)-centered split + atanh series.
|
|
356
689
|
// x = m * 2^k with bits-extracted k (no loop)
|
|
@@ -360,8 +693,9 @@ export default (ctx) => {
|
|
|
360
693
|
// With 9 polynomial terms and |s|≤0.172, truncation error ≈ 2|s|·z⁹/19 ≈ 4e-17,
|
|
361
694
|
// close to f64 ulp. The whole routine is branchless after edge cases.
|
|
362
695
|
// Edge cases: NaN→NaN, ≤0 distinguishes 0→-Inf, <0→NaN; +Inf passes through.
|
|
363
|
-
|
|
696
|
+
wat('math.log', `(func $math.log (param $x f64) (result f64)
|
|
364
697
|
(local $bits i64) (local $k i32) (local $m f64) (local $s f64) (local $z f64)
|
|
698
|
+
(local $f f64) (local $w f64) (local $t1 f64) (local $t2 f64) (local $hfsq f64)
|
|
365
699
|
(if (f64.ne (local.get $x) (local.get $x))
|
|
366
700
|
(then (return (local.get $x))))
|
|
367
701
|
(if (f64.le (local.get $x) (f64.const 0.0))
|
|
@@ -392,24 +726,30 @@ export default (ctx) => {
|
|
|
392
726
|
(then
|
|
393
727
|
(local.set $m (f64.mul (local.get $m) (f64.const 0.5)))
|
|
394
728
|
(local.set $k (i32.add (local.get $k) (i32.const 1)))))
|
|
395
|
-
|
|
396
|
-
|
|
729
|
+
;; s = f/(2+f) with f = m−1 (= (m−1)/(m+1)); then the fdlibm even/odd-split
|
|
730
|
+
;; polynomial. Two parallel Horner chains (t1 over even powers, t2 over odd)
|
|
731
|
+
;; cut the dependency chain ~in half vs one 9-deep Horner — more ILP, fewer
|
|
732
|
+
;; terms — and reconstruct log(m) = f − hfsq + s·(hfsq + t1 + t2). ~1 ulp.
|
|
733
|
+
(local.set $f (f64.sub (local.get $m) (f64.const 1.0)))
|
|
734
|
+
(local.set $s (f64.div (local.get $f) (f64.add (local.get $f) (f64.const 2.0))))
|
|
397
735
|
(local.set $z (f64.mul (local.get $s) (local.get $s)))
|
|
398
|
-
|
|
736
|
+
(local.set $w (f64.mul (local.get $z) (local.get $z)))
|
|
737
|
+
(local.set $t1 (f64.mul (local.get $w) (f64.add (f64.const 0.3999999999940941908)
|
|
738
|
+
(f64.mul (local.get $w) (f64.add (f64.const 0.2222219843214978396)
|
|
739
|
+
(f64.mul (local.get $w) (f64.const 0.1531383769920937332)))))))
|
|
740
|
+
(local.set $t2 (f64.mul (local.get $z) (f64.add (f64.const 0.6666666666666735130)
|
|
741
|
+
(f64.mul (local.get $w) (f64.add (f64.const 0.2857142874366239149)
|
|
742
|
+
(f64.mul (local.get $w) (f64.add (f64.const 0.1818357216161805012)
|
|
743
|
+
(f64.mul (local.get $w) (f64.const 0.1479819860511658591)))))))))
|
|
744
|
+
(local.set $hfsq (f64.mul (f64.const 0.5) (f64.mul (local.get $f) (local.get $f))))
|
|
399
745
|
(f64.add
|
|
400
746
|
(f64.mul (f64.convert_i32_s (local.get $k)) (f64.const ${Math.LN2}))
|
|
401
|
-
(f64.
|
|
402
|
-
(f64.mul (local.get $
|
|
403
|
-
(f64.
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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)))))))))))))))))))))`
|
|
410
|
-
|
|
411
|
-
ctx.core.stdlib['math.log2'] = `(func $math.log2 (param $x f64) (result f64)
|
|
412
|
-
(f64.div (call $math.log (local.get $x)) (f64.const ${Math.LN2})))`
|
|
747
|
+
(f64.add (f64.sub (local.get $f) (local.get $hfsq))
|
|
748
|
+
(f64.mul (local.get $s) (f64.add (local.get $hfsq)
|
|
749
|
+
(f64.add (local.get $t1) (local.get $t2))))))))`)
|
|
750
|
+
|
|
751
|
+
wat('math.log2', `(func $math.log2 (param $x f64) (result f64)
|
|
752
|
+
(f64.div (call $math.log (local.get $x)) (f64.const ${Math.LN2})))`)
|
|
413
753
|
|
|
414
754
|
// log10 via fdlibm's two-term decomposition: log10(x) = k*log10(2) + log10(m).
|
|
415
755
|
// A plain log(x)/ln(10) double-rounds (rounding of log itself, then of the
|
|
@@ -417,7 +757,7 @@ export default (ctx) => {
|
|
|
417
757
|
// Reducing x = m·2^k, splitting log10(2) and 1/ln(10) into hi/lo halves, and
|
|
418
758
|
// keeping the bulk term (k·log10_2hi, hi·ivln10hi) carry-free recovers the
|
|
419
759
|
// last ulps, so log10(10/100/1000/…) round-trips to exact integers.
|
|
420
|
-
|
|
760
|
+
wat('math.log10', `(func $math.log10 (param $x f64) (result f64)
|
|
421
761
|
(local $bits i64) (local $k i32) (local $m f64) (local $f f64)
|
|
422
762
|
(local $hfsq f64) (local $s f64) (local $z f64) (local $w f64)
|
|
423
763
|
(local $t1 f64) (local $t2 f64) (local $R f64)
|
|
@@ -481,12 +821,12 @@ export default (ctx) => {
|
|
|
481
821
|
(local.set $w (f64.add (local.get $y) (local.get $valhi)))
|
|
482
822
|
(local.set $vallo (f64.add (local.get $vallo)
|
|
483
823
|
(f64.add (f64.sub (local.get $y) (local.get $w)) (local.get $valhi))))
|
|
484
|
-
(f64.add (local.get $vallo) (local.get $w)))`
|
|
824
|
+
(f64.add (local.get $vallo) (local.get $w)))`)
|
|
485
825
|
|
|
486
826
|
// log1p(x) via Kahan's compensated trick: with u = 1+x, log(u) loses bits when x is
|
|
487
827
|
// small (because u rounds to ~1), but the ratio x/(u-1) is exactly the missing factor.
|
|
488
828
|
// For u==1 (x below ulp), result is just x; preserves -0 from x=-0 path.
|
|
489
|
-
|
|
829
|
+
wat('math.log1p', `(func $math.log1p (param $x f64) (result f64)
|
|
490
830
|
(local $u f64)
|
|
491
831
|
;; log1p(+Inf) = +Inf — the ratio trick below would compute Inf/Inf = NaN.
|
|
492
832
|
(if (f64.eq (local.get $x) (f64.const inf)) (then (return (f64.const inf))))
|
|
@@ -495,9 +835,9 @@ export default (ctx) => {
|
|
|
495
835
|
(then (return (local.get $x))))
|
|
496
836
|
(f64.div
|
|
497
837
|
(f64.mul (call $math.log (local.get $u)) (local.get $x))
|
|
498
|
-
(f64.sub (local.get $u) (f64.const 1.0))))`
|
|
838
|
+
(f64.sub (local.get $u) (f64.const 1.0))))`)
|
|
499
839
|
|
|
500
|
-
|
|
840
|
+
wat('math.pow', `(func $math.pow (param $x f64) (param $y f64) (result f64)
|
|
501
841
|
(local $result f64) (local $n i32) (local $neg_base i32) (local $abs_x f64)
|
|
502
842
|
;; y == 0 -> 1 (covers pow(NaN,0), pow(±0,0), pow(±Inf,0))
|
|
503
843
|
(if (f64.eq (local.get $y) (f64.const 0.0)) (then (return (f64.const 1.0))))
|
|
@@ -570,12 +910,12 @@ export default (ctx) => {
|
|
|
570
910
|
;; x < 0, non-integer finite y -> NaN
|
|
571
911
|
(if (f64.lt (local.get $x) (f64.const 0.0))
|
|
572
912
|
(then (return (f64.const nan))))
|
|
573
|
-
(call $math.exp (f64.mul (local.get $y) (call $math.log (local.get $x)))))`
|
|
913
|
+
(call $math.exp (f64.mul (local.get $y) (call $math.log (local.get $x)))))`)
|
|
574
914
|
|
|
575
915
|
// fdlibm atan: 4-region argument reduction onto |r| ≤ tan(π/16), then an
|
|
576
916
|
// 11-term odd polynomial split into even/odd parts. Accurate to <1 ulp —
|
|
577
917
|
// the old Taylor series was ~2e-6 off near |x|=0.5. Drives asin/acos/atan2.
|
|
578
|
-
|
|
918
|
+
wat('math.atan', `(func $math.atan (param $x f64) (result f64)
|
|
579
919
|
(local $abs_x f64) (local $id i32) (local $r f64) (local $z f64) (local $w f64)
|
|
580
920
|
(local $s1 f64) (local $s2 f64) (local $ahi f64) (local $alo f64) (local $res f64)
|
|
581
921
|
;; NaN passes through unchanged.
|
|
@@ -642,19 +982,20 @@ export default (ctx) => {
|
|
|
642
982
|
(local.set $res (f64.sub (local.get $ahi)
|
|
643
983
|
(f64.sub (f64.sub (f64.mul (local.get $r) (f64.add (local.get $s1) (local.get $s2))) (local.get $alo))
|
|
644
984
|
(local.get $r))))
|
|
645
|
-
(f64.copysign (local.get $res) (local.get $x)))`
|
|
985
|
+
(f64.copysign (local.get $res) (local.get $x)))`)
|
|
646
986
|
|
|
647
|
-
|
|
987
|
+
wat('math.asin', `(func $math.asin (param $x f64) (result f64)
|
|
648
988
|
;; Domain is [-1, 1]; outside it (including ±Infinity), Math.asin returns NaN.
|
|
989
|
+
;; sin/cos output is clamped to [-1, 1] by sin_core/cos_core, so no tolerance needed here.
|
|
649
990
|
(if (result f64) (f64.gt (f64.abs (local.get $x)) (f64.const 1.0))
|
|
650
991
|
(then (f64.const nan))
|
|
651
992
|
(else (call $math.atan (f64.div (local.get $x)
|
|
652
|
-
(f64.sqrt (f64.sub (f64.const 1.0) (f64.mul (local.get $x) (local.get $x)))))))))`
|
|
993
|
+
(f64.sqrt (f64.sub (f64.const 1.0) (f64.mul (local.get $x) (local.get $x)))))))))`)
|
|
653
994
|
|
|
654
|
-
|
|
655
|
-
(f64.sub (f64.const ${
|
|
995
|
+
wat('math.acos', `(func $math.acos (param $x f64) (result f64)
|
|
996
|
+
(f64.sub (f64.const ${HALF_PI}) (call $math.asin (local.get $x))))`)
|
|
656
997
|
|
|
657
|
-
|
|
998
|
+
wat('math.atan2', `(func $math.atan2 (param $y f64) (param $x f64) (result f64)
|
|
658
999
|
;; If either argument is NaN, the result is NaN (ECMA-262 21.3.2.5).
|
|
659
1000
|
(if (f64.ne (local.get $x) (local.get $x)) (then (return (local.get $x))))
|
|
660
1001
|
(if (f64.ne (local.get $y) (local.get $y)) (then (return (local.get $y))))
|
|
@@ -662,30 +1003,30 @@ export default (ctx) => {
|
|
|
662
1003
|
;; y is ±0 too: result is ±0 when x is +0, ±π when x is -0; sign taken from y.
|
|
663
1004
|
(if (result f64) (f64.eq (local.get $y) (f64.const 0.0))
|
|
664
1005
|
(then (f64.copysign
|
|
665
|
-
(select (f64.const ${
|
|
1006
|
+
(select (f64.const ${PI}) (f64.const 0.0)
|
|
666
1007
|
(f64.lt (f64.copysign (f64.const 1.0) (local.get $x)) (f64.const 0.0)))
|
|
667
1008
|
(local.get $y)))
|
|
668
1009
|
(else
|
|
669
|
-
(if (result f64) (f64.gt (local.get $y) (f64.const 0.0)) (then (f64.const ${
|
|
1010
|
+
(if (result f64) (f64.gt (local.get $y) (f64.const 0.0)) (then (f64.const ${HALF_PI})) (else (f64.neg (f64.const ${HALF_PI})))))))
|
|
670
1011
|
(else (if (result f64) (f64.ge (local.get $x) (f64.const 0.0))
|
|
671
1012
|
(then (call $math.atan (f64.div (local.get $y) (local.get $x))))
|
|
672
1013
|
(else (if (result f64) (f64.ge (local.get $y) (f64.const 0.0))
|
|
673
|
-
(then (f64.add (call $math.atan (f64.div (local.get $y) (local.get $x))) (f64.const ${
|
|
674
|
-
(else (f64.sub (call $math.atan (f64.div (local.get $y) (local.get $x))) (f64.const ${
|
|
1014
|
+
(then (f64.add (call $math.atan (f64.div (local.get $y) (local.get $x))) (f64.const ${PI})))
|
|
1015
|
+
(else (f64.sub (call $math.atan (f64.div (local.get $y) (local.get $x))) (f64.const ${PI})))))))))`)
|
|
675
1016
|
|
|
676
|
-
|
|
1017
|
+
wat('math.sinh', `(func $math.sinh (param $x f64) (result f64)
|
|
677
1018
|
(local $ex f64)
|
|
678
1019
|
;; Preserve sign of zero: sinh(±0) = ±0 (the f64.lt sign test below is false for -0).
|
|
679
1020
|
(if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
|
|
680
1021
|
(local.set $ex (call $math.exp (f64.abs (local.get $x))))
|
|
681
1022
|
(local.set $ex (f64.mul (f64.const 0.5) (f64.sub (local.get $ex) (f64.div (f64.const 1.0) (local.get $ex)))))
|
|
682
|
-
(if (result f64) (f64.lt (local.get $x) (f64.const 0.0)) (then (f64.neg (local.get $ex))) (else (local.get $ex))))`
|
|
1023
|
+
(if (result f64) (f64.lt (local.get $x) (f64.const 0.0)) (then (f64.neg (local.get $ex))) (else (local.get $ex))))`)
|
|
683
1024
|
|
|
684
|
-
|
|
1025
|
+
wat('math.cosh', `(func $math.cosh (param $x f64) (result f64)
|
|
685
1026
|
(local $ex f64) (local.set $ex (call $math.exp (f64.abs (local.get $x))))
|
|
686
|
-
(f64.mul (f64.const 0.5) (f64.add (local.get $ex) (f64.div (f64.const 1.0) (local.get $ex)))))`
|
|
1027
|
+
(f64.mul (f64.const 0.5) (f64.add (local.get $ex) (f64.div (f64.const 1.0) (local.get $ex)))))`)
|
|
687
1028
|
|
|
688
|
-
|
|
1029
|
+
wat('math.tanh', `(func $math.tanh (param $x f64) (result f64)
|
|
689
1030
|
(local $e2x f64)
|
|
690
1031
|
;; Preserve sign of zero: tanh(±0) = ±0 (the f64.lt sign test below is false for -0).
|
|
691
1032
|
(if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
|
|
@@ -693,30 +1034,30 @@ export default (ctx) => {
|
|
|
693
1034
|
(then (if (result f64) (f64.lt (local.get $x) (f64.const 0.0)) (then (f64.const -1.0)) (else (f64.const 1.0))))
|
|
694
1035
|
(else (local.set $e2x (call $math.exp (f64.mul (f64.const 2.0) (f64.abs (local.get $x)))))
|
|
695
1036
|
(local.set $e2x (f64.div (f64.sub (local.get $e2x) (f64.const 1.0)) (f64.add (local.get $e2x) (f64.const 1.0))))
|
|
696
|
-
(if (result f64) (f64.lt (local.get $x) (f64.const 0.0)) (then (f64.neg (local.get $e2x))) (else (local.get $e2x))))))`
|
|
1037
|
+
(if (result f64) (f64.lt (local.get $x) (f64.const 0.0)) (then (f64.neg (local.get $e2x))) (else (local.get $e2x))))))`)
|
|
697
1038
|
|
|
698
|
-
|
|
1039
|
+
wat('math.asinh', `(func $math.asinh (param $x f64) (result f64)
|
|
699
1040
|
;; ±Infinity and NaN pass through unchanged. (log(±Inf + sqrt(Inf²+1)) → NaN otherwise.)
|
|
700
1041
|
(if (i32.eqz (call $math.isFinite (local.get $x))) (then (return (local.get $x))))
|
|
701
1042
|
;; Preserve sign of zero: asinh(±0) = ±0.
|
|
702
1043
|
(if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
|
|
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))))))`
|
|
1044
|
+
(call $math.log (f64.add (local.get $x) (f64.sqrt (f64.add (f64.mul (local.get $x) (local.get $x)) (f64.const 1.0))))))`)
|
|
704
1045
|
|
|
705
|
-
|
|
1046
|
+
wat('math.acosh', `(func $math.acosh (param $x f64) (result f64)
|
|
706
1047
|
(if (f64.eq (local.get $x) (f64.const inf)) (then (return (f64.const inf))))
|
|
707
1048
|
;; acosh is defined only for x >= 1; everything below (incl. -Inf) is NaN.
|
|
708
1049
|
(if (result f64) (f64.lt (local.get $x) (f64.const 1.0)) (then (f64.const nan)) (else
|
|
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))))))))`
|
|
1050
|
+
(call $math.log (f64.add (local.get $x) (f64.sqrt (f64.sub (f64.mul (local.get $x) (local.get $x)) (f64.const 1.0))))))))`)
|
|
710
1051
|
|
|
711
|
-
|
|
1052
|
+
wat('math.atanh', `(func $math.atanh (param $x f64) (result f64)
|
|
712
1053
|
;; Preserve sign of zero: atanh(±0) = ±0.
|
|
713
1054
|
(if (f64.eq (local.get $x) (f64.const 0.0)) (then (return (local.get $x))))
|
|
714
1055
|
;; ±Infinity → NaN. Without this the (1+x)/(1-x) ratio is Inf/Inf, whose
|
|
715
1056
|
;; sign-nondeterministic arithmetic NaN would escape non-canonical on x86.
|
|
716
1057
|
(if (f64.eq (f64.abs (local.get $x)) (f64.const inf)) (then (return (f64.const nan))))
|
|
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))))))`
|
|
1058
|
+
(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))))))`)
|
|
718
1059
|
|
|
719
|
-
|
|
1060
|
+
wat('math.cbrt', `(func $math.cbrt (param $x f64) (result f64)
|
|
720
1061
|
(local $y f64)
|
|
721
1062
|
;; ±Infinity and NaN pass through; preserve sign of zero.
|
|
722
1063
|
(if (i32.eqz (call $math.isFinite (local.get $x))) (then (return (local.get $x))))
|
|
@@ -728,31 +1069,37 @@ export default (ctx) => {
|
|
|
728
1069
|
(local.set $y (call $math.pow (local.get $x) (f64.const 0.3333333333333333)))
|
|
729
1070
|
(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
1071
|
(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))))`
|
|
1072
|
+
(local.get $y))))`)
|
|
732
1073
|
|
|
733
1074
|
// Small finite-test helper (NaN→0, ±Inf→0, finite→1). Used by transcendental
|
|
734
1075
|
// functions that need to short-circuit on infinite inputs.
|
|
735
|
-
|
|
1076
|
+
wat('math.isFinite', `(func $math.isFinite (param $x f64) (result i32)
|
|
736
1077
|
(i32.and
|
|
737
1078
|
(f64.eq (local.get $x) (local.get $x))
|
|
738
|
-
(f64.lt (f64.abs (local.get $x)) (f64.const inf))))`
|
|
1079
|
+
(f64.lt (f64.abs (local.get $x)) (f64.const inf))))`)
|
|
739
1080
|
|
|
740
|
-
|
|
1081
|
+
wat('math.hypot', `(func $math.hypot (param $x f64) (param $y f64) (result f64)
|
|
741
1082
|
;; Any ±Infinity argument ⇒ +Infinity, even when the other is NaN (ECMA-262 21.3.2.18).
|
|
742
1083
|
(if (f64.eq (f64.abs (local.get $x)) (f64.const inf)) (then (return (f64.const inf))))
|
|
743
1084
|
(if (f64.eq (f64.abs (local.get $y)) (f64.const inf)) (then (return (f64.const inf))))
|
|
744
|
-
(f64.sqrt (f64.add (f64.mul (local.get $x) (local.get $x)) (f64.mul (local.get $y) (local.get $y)))))`
|
|
1085
|
+
(f64.sqrt (f64.add (f64.mul (local.get $x) (local.get $x)) (f64.mul (local.get $y) (local.get $y)))))`)
|
|
745
1086
|
|
|
746
|
-
|
|
1087
|
+
// xorshift32 → [0,1). In entropy mode a one-shot prologue replaces the fixed
|
|
1088
|
+
// initial state with host entropy on first call (branch is well-predicted after).
|
|
1089
|
+
const rngSeedPrologue = rngEntropy ? `(if (i32.eqz (global.get $math.rng_seeded))
|
|
1090
|
+
(then (global.set $math.rng_state (call $__rng_seed)) (global.set $math.rng_seeded (i32.const 1))))
|
|
1091
|
+
` : ``
|
|
1092
|
+
wat('math.random', `(func $math.random (result f64)
|
|
747
1093
|
(local $s i32)
|
|
748
|
-
(local.set $s (global.get $math.rng_state))
|
|
1094
|
+
${rngSeedPrologue}(local.set $s (global.get $math.rng_state))
|
|
749
1095
|
(local.set $s (i32.xor (local.get $s) (i32.shl (local.get $s) (i32.const 13))))
|
|
750
1096
|
(local.set $s (i32.xor (local.get $s) (i32.shr_u (local.get $s) (i32.const 17))))
|
|
751
1097
|
(local.set $s (i32.xor (local.get $s) (i32.shl (local.get $s) (i32.const 5))))
|
|
752
1098
|
(global.set $math.rng_state (local.get $s))
|
|
753
|
-
(f64.div (f64.convert_i32_u (i32.and (local.get $s) (i32.const 0x7FFFFFFF))) (f64.const 2147483647.0)))
|
|
1099
|
+
(f64.div (f64.convert_i32_u (i32.and (local.get $s) (i32.const 0x7FFFFFFF))) (f64.const 2147483647.0)))`,
|
|
1100
|
+
rngEntropy ? ['__rng_seed'] : [])
|
|
754
1101
|
|
|
755
|
-
|
|
1102
|
+
wat('math.sumPrecise', `(func $math.sumPrecise (param $arr i64) (result f64)
|
|
756
1103
|
;; Exact summation via a 2304-bit fixed-point accumulator (36 i64 words,
|
|
757
1104
|
;; little-endian two's complement) holding sum*2^1074. Every finite f64 is an
|
|
758
1105
|
;; integer multiple of 2^-1074, so the running sum carries zero rounding
|
|
@@ -911,8 +1258,25 @@ export default (ctx) => {
|
|
|
911
1258
|
(then (f64.reinterpret_i64 (i64.shl (i64.extend_i32_u (i32.add (local.get $k) (i32.const 1023))) (i64.const 52))))
|
|
912
1259
|
(else (f64.reinterpret_i64 (i64.shl (i64.const 1) (i64.extend_i32_u (i32.add (local.get $k) (i32.const 1074))))))))
|
|
913
1260
|
(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)))`
|
|
1261
|
+
(select (f64.neg (local.get $res)) (local.get $res) (local.get $resultNeg)))`)
|
|
915
1262
|
|
|
916
|
-
// Global for random state
|
|
917
|
-
|
|
1263
|
+
// Global for random state — seeded with the fixed constant (deterministic) or,
|
|
1264
|
+
// in entropy mode, overwritten from the host on first Math.random() call.
|
|
1265
|
+
declGlobal('math.rng_state', 'i32', rngSeedConst)
|
|
1266
|
+
if (rngEntropy) {
|
|
1267
|
+
declGlobal('math.rng_seeded', 'i32')
|
|
1268
|
+
// One i32 of host entropy, floored at 1 (xorshift32 is dead at state 0).
|
|
1269
|
+
wat('__rng_seed', ctx.transform.host === 'wasi'
|
|
1270
|
+
? `(func $__rng_seed (result i32)
|
|
1271
|
+
(local $buf i32) (local $s i32)
|
|
1272
|
+
(local.set $buf (call $__alloc (i32.const 4)))
|
|
1273
|
+
(drop (call $__random_get (local.get $buf) (i32.const 4)))
|
|
1274
|
+
(local.set $s (i32.load (local.get $buf)))
|
|
1275
|
+
(select (local.get $s) (i32.const 1) (local.get $s)))`
|
|
1276
|
+
: `(func $__rng_seed (result i32)
|
|
1277
|
+
(local $s i32)
|
|
1278
|
+
(local.set $s (call $__env_rng_seed))
|
|
1279
|
+
(select (local.get $s) (i32.const 1) (local.get $s)))`,
|
|
1280
|
+
ctx.transform.host === 'wasi' ? ['__alloc'] : [])
|
|
1281
|
+
}
|
|
918
1282
|
}
|