jz 0.6.0 → 0.8.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 +99 -72
- package/bench/README.md +253 -100
- package/bench/bench.svg +58 -81
- package/cli.js +85 -12
- package/dist/interop.js +1 -0
- package/dist/jz.js +6196 -0
- package/index.d.ts +126 -0
- package/index.js +222 -35
- package/interop.js +240 -141
- package/layout.js +34 -18
- package/module/array.js +99 -111
- package/module/collection.js +124 -30
- package/module/console.js +1 -1
- package/module/core.js +163 -19
- package/module/json.js +3 -3
- package/module/math.js +162 -3
- package/module/number.js +268 -13
- package/module/object.js +37 -5
- package/module/regex.js +8 -7
- package/module/simd.js +37 -5
- package/module/string.js +203 -168
- package/module/typedarray.js +565 -112
- package/package.json +26 -7
- package/src/abi/string.js +29 -29
- package/src/ast.js +19 -2
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +101 -4
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +82 -20
- package/src/compile/emit.js +592 -51
- package/src/compile/index.js +504 -89
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +109 -0
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +275 -41
- package/src/compile/peel-stencil.js +215 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +8 -1
- package/src/compile/plan/inline.js +180 -22
- package/src/compile/plan/literals.js +313 -24
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +96 -19
- package/src/helper-counters.js +137 -0
- package/src/ir.js +157 -20
- package/src/kind-traits.js +34 -3
- package/src/kind.js +79 -4
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1274 -151
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +4187 -253
- package/src/prepare/index.js +75 -14
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +6 -2
- package/src/static.js +9 -0
- package/src/type.js +63 -51
- package/src/wat/assemble.js +286 -21
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3760
package/index.d.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Type declarations for jz — minimal functional JS subset compiling to WASM.
|
|
2
|
+
// The public surface is small: `jz` (compile + instantiate), `compile` (raw bytes),
|
|
3
|
+
// `compileModule` (compile once), `instantiate` (run a module), and `jz.memory`.
|
|
4
|
+
// See README for semantics and the JS↔WASM value ABI.
|
|
5
|
+
|
|
6
|
+
/** Optimization level / preset. `2` is the default (all stable passes). */
|
|
7
|
+
export type OptimizeLevel = boolean | 0 | 1 | 2 | 3 | 'speed' | 'size'
|
|
8
|
+
|
|
9
|
+
/** Runtime-service lowering target. */
|
|
10
|
+
export type Host = 'js' | 'wasi'
|
|
11
|
+
|
|
12
|
+
/** Value injectable as a compile-time `define` constant. */
|
|
13
|
+
export type DefineValue = number | boolean | string | null | DefineValue[] | { [k: string]: DefineValue }
|
|
14
|
+
|
|
15
|
+
export interface CompileOptions {
|
|
16
|
+
/** Static ES imports to bundle: `{ './dep.js': 'export let x = 1' }`. */
|
|
17
|
+
modules?: Record<string, string>
|
|
18
|
+
/** Host imports wired at runtime: `{ math: Math, host: { log: console.log } }`. */
|
|
19
|
+
imports?: Record<string, unknown>
|
|
20
|
+
/** `N` initial pages of owned memory, or a shared `jz.memory()` / `WebAssembly.Memory`. */
|
|
21
|
+
memory?: number | WebAssembly.Memory | JzMemory
|
|
22
|
+
/** Cap memory growth at this many 64 KiB pages (default: unbounded). */
|
|
23
|
+
maxMemory?: number
|
|
24
|
+
/** Import `env.memory` instead of exporting own memory. */
|
|
25
|
+
importMemory?: boolean
|
|
26
|
+
/** Runtime-service lowering. Default `'js'`. */
|
|
27
|
+
host?: Host
|
|
28
|
+
/** Optimization level or named preset. Default `2`. */
|
|
29
|
+
optimize?: OptimizeLevel
|
|
30
|
+
/** Compile-time constants injected as top-level bindings. */
|
|
31
|
+
define?: Record<string, DefineValue>
|
|
32
|
+
/** Enforce the pure canonical subset: skip jzify lowering and reject dynamic fallbacks. */
|
|
33
|
+
strict?: boolean
|
|
34
|
+
/** Omit `_alloc`/`_clear` allocator exports for standalone scalar modules. */
|
|
35
|
+
alloc?: boolean
|
|
36
|
+
/** Disable auto-vectorization (no jz-emitted `v128`). Explicit intrinsics still compile. */
|
|
37
|
+
noSimd?: boolean
|
|
38
|
+
/** Use ordinary call frames instead of `return_call` tail calls. */
|
|
39
|
+
tailCall?: boolean
|
|
40
|
+
/** `Math.random` seeding: a number fixes the stream; `true` forces host entropy. */
|
|
41
|
+
randomSeed?: number | boolean
|
|
42
|
+
/** Emit a WASM `name` section (function symbols) for profilers/debuggers. */
|
|
43
|
+
names?: boolean
|
|
44
|
+
/** `compile()` returns WAT text instead of a WASM binary. */
|
|
45
|
+
wat?: boolean
|
|
46
|
+
/** Resolve bare specifiers via Node.js module resolution (CLI/build use). */
|
|
47
|
+
resolve?: boolean
|
|
48
|
+
/** Mutable sink that collects per-stage compile timings. */
|
|
49
|
+
profile?: { entries?: unknown[]; totals?: Record<string, number> } & Record<string, unknown>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* An enhanced `WebAssembly.Memory` that marshals JS values across the WASM boundary.
|
|
54
|
+
* Allocators (`String`/`Array`/`Object`/typed-array ctors) return NaN-boxed `f64`
|
|
55
|
+
* pointers; `read` decodes one back. The heap never frees implicitly — call `reset`
|
|
56
|
+
* between independent batches (all previously returned pointers become invalid).
|
|
57
|
+
*/
|
|
58
|
+
export interface JzMemory extends WebAssembly.Memory {
|
|
59
|
+
/** Allocate a UTF-8 string; returns a pointer. */
|
|
60
|
+
String(str: string): number
|
|
61
|
+
/** Allocate an array (numbers, strings, nested arrays/objects); returns a pointer. */
|
|
62
|
+
Array(data: ArrayLike<unknown>): number
|
|
63
|
+
/** Allocate a fixed-layout object (keys must match a compiled schema); returns a pointer. */
|
|
64
|
+
Object(obj: Record<string, unknown>): number
|
|
65
|
+
Float64Array(data: ArrayLike<number>): number
|
|
66
|
+
Float32Array(data: ArrayLike<number>): number
|
|
67
|
+
Int32Array(data: ArrayLike<number>): number
|
|
68
|
+
Uint32Array(data: ArrayLike<number>): number
|
|
69
|
+
Int16Array(data: ArrayLike<number>): number
|
|
70
|
+
Uint16Array(data: ArrayLike<number>): number
|
|
71
|
+
Int8Array(data: ArrayLike<number>): number
|
|
72
|
+
Uint8Array(data: ArrayLike<number>): number
|
|
73
|
+
/** Decode one pointer (or a multi-value tuple of pointers) back to a JS value. */
|
|
74
|
+
read(ptr: number | number[]): unknown
|
|
75
|
+
/** Reserve `bytes` of raw heap; returns the offset. */
|
|
76
|
+
alloc(bytes: number): number
|
|
77
|
+
/** Rewind the bump pointer — drops every allocation since the last reset. */
|
|
78
|
+
reset(): void
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A compiled-and-instantiated jz module. */
|
|
82
|
+
export interface JzInstance {
|
|
83
|
+
/** JS-wrapped exports: marshals arguments in, decodes pointer returns, throws real `Error`s. */
|
|
84
|
+
exports: Record<string, (...args: any[]) => any> & Record<string, any>
|
|
85
|
+
/** The value codec, or `null` for a pure-scalar module with no heap. */
|
|
86
|
+
memory: JzMemory | null
|
|
87
|
+
/** The raw `WebAssembly.Instance` (numbers pass through; pointers come back NaN-boxed). */
|
|
88
|
+
instance: WebAssembly.Instance
|
|
89
|
+
/** The underlying `WebAssembly.Module`. */
|
|
90
|
+
module: WebAssembly.Module
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Create a shared memory that modules compile into (schemas accumulate across modules). */
|
|
94
|
+
export interface MemoryFactory {
|
|
95
|
+
(src?: WebAssembly.Memory): JzMemory
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The default export: compile + instantiate, as a call or a tagged template. */
|
|
99
|
+
export interface Jz {
|
|
100
|
+
/** Compile and instantiate a source string. */
|
|
101
|
+
(code: string, opts?: CompileOptions): JzInstance
|
|
102
|
+
/** Tagged-template form: interpolations are baked into the source at compile time. */
|
|
103
|
+
(strings: TemplateStringsArray, ...values: unknown[]): JzInstance
|
|
104
|
+
/** Compile only — raw WASM binary (or WAT text with `{ wat: true }`). */
|
|
105
|
+
compile: typeof compile
|
|
106
|
+
/** Shared-memory factory: `jz.memory()` or `jz.memory(existing)`. */
|
|
107
|
+
memory: MemoryFactory
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
declare const jz: Jz
|
|
111
|
+
export default jz
|
|
112
|
+
export { jz }
|
|
113
|
+
|
|
114
|
+
/** Compile to a raw WASM binary. */
|
|
115
|
+
export function compile(code: string, opts?: CompileOptions & { wat?: false }): Uint8Array
|
|
116
|
+
/** Compile to WAT text. */
|
|
117
|
+
export function compile(code: string, opts: CompileOptions & { wat: true }): string
|
|
118
|
+
|
|
119
|
+
/** Compile once to a `WebAssembly.Module` (pays AOT + validate cost once); instantiate many. */
|
|
120
|
+
export function compileModule(code: string, opts?: CompileOptions): WebAssembly.Module
|
|
121
|
+
|
|
122
|
+
/** Instantiate a compiled module or raw WASM bytes, wiring the allocator and value codec. */
|
|
123
|
+
export function instantiate(
|
|
124
|
+
module: WebAssembly.Module | Uint8Array | ArrayBuffer,
|
|
125
|
+
opts?: CompileOptions,
|
|
126
|
+
): JzInstance
|
package/index.js
CHANGED
|
@@ -43,18 +43,21 @@
|
|
|
43
43
|
import { parse } from './src/parse.js'
|
|
44
44
|
import watrCompile from "watr/compile";
|
|
45
45
|
import watrPrint from "watr/print";
|
|
46
|
-
import watOptimize from "
|
|
46
|
+
import watOptimize from "watr/optimize";
|
|
47
|
+
import { appendLateStdlib } from './src/wat/assemble.js'
|
|
47
48
|
import { ctx, reset, err, initWarnings, assertCtxInvariants } from './src/ctx.js'
|
|
48
49
|
import prepare, { GLOBALS } from './src/prepare/index.js'
|
|
50
|
+
import { liftIIFEs } from './src/prepare/lift-iife.js'
|
|
49
51
|
import compile from './src/compile/index.js'
|
|
50
52
|
import { resetProgramFactsCache } from './src/compile/program-facts.js'
|
|
51
53
|
import { emit, emitter, emitVoid as flat, emitBlockBody as body, emitBoolStr as bool, emitIndex as idx, buildArrayWithSpreads as spread } from './src/compile/emit.js'
|
|
52
|
-
import { optimizeFunc, collectVolatileGlobals, collectReachableGlobalWrites, hoistGlobalPtrOffset, stablePtrGlobalNames, resolveOptimize } from './src/optimize/index.js'
|
|
54
|
+
import { optimizeFunc, foldStrDispatchF64, collectVolatileGlobals, collectReachableGlobalWrites, hoistGlobalPtrOffset, stablePtrGlobalNames, resolveOptimize, SIMD_PINNED } from './src/optimize/index.js'
|
|
55
|
+
import { findBodyStart } from './src/ir.js'
|
|
53
56
|
import { VAL } from './src/reps.js'
|
|
54
57
|
import jzify from './jzify/index.js'
|
|
55
58
|
import { T } from './src/ast.js'
|
|
56
59
|
import {
|
|
57
|
-
memory as enhanceMemory, instantiate as instantiateRuntime,
|
|
60
|
+
memory as enhanceMemory, instantiate as instantiateRuntime, toModule,
|
|
58
61
|
} from './interop.js'
|
|
59
62
|
|
|
60
63
|
// A host import that's a JS function may hand back any value, including a host
|
|
@@ -68,11 +71,47 @@ const importsMayReturnExternal = (imports) =>
|
|
|
68
71
|
// directly — keeps the self-host kernel free of host-only built-ins.
|
|
69
72
|
const resolveUrl = (spec, base) => new URL(spec, base).href
|
|
70
73
|
|
|
74
|
+
// Serialize a JS value to a jz source literal (numbers/booleans/strings/null/
|
|
75
|
+
// undefined + literal arrays/objects). Returns null for anything not expressible
|
|
76
|
+
// as a compile-time literal (functions, host objects, circular). Shared by the
|
|
77
|
+
// `jz\`…${val}\`` template tag (hoists complex args) and opts.define.
|
|
78
|
+
const serialize = (v) => {
|
|
79
|
+
if (v === undefined) return 'undefined'
|
|
80
|
+
if (typeof v === 'number' || typeof v === 'boolean') return String(v)
|
|
81
|
+
if (v === null) return 'null'
|
|
82
|
+
if (typeof v === 'string') return JSON.stringify(v)
|
|
83
|
+
if (Array.isArray(v)) {
|
|
84
|
+
const elems = v.map(serialize)
|
|
85
|
+
return elems.every(e => e !== null) ? `[${elems.join(', ')}]` : null
|
|
86
|
+
}
|
|
87
|
+
if (typeof v === 'object') {
|
|
88
|
+
const props = Object.keys(v).map(k => {
|
|
89
|
+
const s = serialize(v[k])
|
|
90
|
+
return s !== null ? `${k}: ${s}` : null
|
|
91
|
+
})
|
|
92
|
+
return props.every(p => p !== null) ? `{${props.join(', ')}}` : null
|
|
93
|
+
}
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// opts.define → a one-line `let K = V; …` prelude prepended to source. Kept on a
|
|
98
|
+
// single line (no trailing newline) so user line numbers past line 1 stay exact —
|
|
99
|
+
// same convention the template tag uses for hoisted literals.
|
|
100
|
+
const defineBindings = (define) => {
|
|
101
|
+
const parts = []
|
|
102
|
+
for (const [k, v] of Object.entries(define)) {
|
|
103
|
+
const s = serialize(v)
|
|
104
|
+
if (s === null) err(`opts.define['${k}'] is not a compile-time constant — use a number, boolean, string, null, or a literal array/object`)
|
|
105
|
+
parts.push(`let ${k} = ${s}`)
|
|
106
|
+
}
|
|
107
|
+
return parts.length ? parts.join('; ') + '; ' : ''
|
|
108
|
+
}
|
|
109
|
+
|
|
71
110
|
const nowMs = () => globalThis.performance?.now ? globalThis.performance.now() : Date.now()
|
|
72
111
|
|
|
73
112
|
const compileProfiler = (profile) => {
|
|
74
113
|
if (profile == null) return null
|
|
75
|
-
if (typeof profile !== 'object') throw new TypeError('opts.profile must be an object sink
|
|
114
|
+
if (typeof profile !== 'object') throw new TypeError('opts.profile must be an object sink (populated with compile-phase entries/totals); for a wasm name section use opts.names')
|
|
76
115
|
profile.entries ||= []
|
|
77
116
|
profile.totals ||= {}
|
|
78
117
|
return {
|
|
@@ -168,12 +207,20 @@ jz.memory = enhanceMemory
|
|
|
168
207
|
* (so full-JS syntax like var/function/class is rejected, not lowered) and reject
|
|
169
208
|
* dynamic features (obj[k], for-in, unknown receiver method calls) at compile time.
|
|
170
209
|
* Avoids pulling dynamic-dispatch stdlib into output; large size win for static programs.
|
|
171
|
-
* @param {WebAssembly.Memory|number} [opts.memory] -
|
|
172
|
-
*
|
|
210
|
+
* @param {WebAssembly.Memory|number} [opts.memory] - Owned memory's initial page
|
|
211
|
+
* count (`memory: N`, 64 KiB/page), or a `WebAssembly.Memory` to share across modules.
|
|
212
|
+
* @param {number} [opts.maxMemory] - Maximum memory pages — emits a ceiling on the
|
|
213
|
+
* memory type so growth traps past it (sandbox cap). Must be ≥ the initial size.
|
|
214
|
+
* Default: unbounded.
|
|
215
|
+
* @param {boolean} [opts.importMemory] - Import `env.memory` instead of exporting an
|
|
216
|
+
* owned memory. For embedding into a host that provides the memory.
|
|
173
217
|
* @param {boolean} [opts.alloc=true] - Export raw allocator helpers
|
|
174
218
|
* (`_alloc`, `_clear`) for JS memory wrapping. Set false for standalone host-run
|
|
175
219
|
* modules that only call exported wasm functions.
|
|
176
|
-
* @param {
|
|
220
|
+
* @param {Object<string,*>} [opts.define] - Compile-time constants injected as
|
|
221
|
+
* top-level bindings before parse, e.g. `{ DEBUG: false, PORT: 8080 }`. Values may
|
|
222
|
+
* be numbers, booleans, strings, null, or literal arrays/objects.
|
|
223
|
+
* @param {boolean|number|string|object} [opts.optimize] - Optimization level/config.
|
|
177
224
|
* - `false` / `0`: nothing. Fastest compile, largest output (live coding).
|
|
178
225
|
* - `1`: encoding-compactness only (treeshake + sortLocalsByUse + fusedRewrite-inline).
|
|
179
226
|
* - `true` / `2` (default): every stable jz pass + full watr (inlineOnce +
|
|
@@ -181,16 +228,32 @@ jz.memory = enhanceMemory
|
|
|
181
228
|
* - `3` / `'speed'`: level 2 + larger array/hash initial caps (`arrayMinCap`,
|
|
182
229
|
* `hashSmallInitCap`) + `hoistConstantPool` off (inline `f64.const` over
|
|
183
230
|
* mutable globals); trades size for speed.
|
|
184
|
-
* - `
|
|
185
|
-
*
|
|
231
|
+
* - `'size'`: full passes with unrolling/SIMD off and tight scalar caps — smallest wasm.
|
|
232
|
+
* - `{ level?, <pass>?: bool, ... }`: per-pass overrides on top of a base level.
|
|
233
|
+
* INTERNAL/unstable — pass names track compiler internals (PASS_NAMES in
|
|
234
|
+
* src/optimize/index.js) and change between versions; prefer the level/string forms.
|
|
235
|
+
* @param {boolean} [opts.noSimd] - Disable auto-vectorization (no jz-emitted v128) for
|
|
236
|
+
* engines without the SIMD proposal. Explicit f32x4/i32x4 intrinsics still compile.
|
|
237
|
+
* @param {boolean} [opts.whyNotSimd] - Diagnostic: emit a `simd-why-not` warning (via
|
|
238
|
+
* opts.warnings) for each canonical loop the auto-vectorizer declined, naming the
|
|
239
|
+
* first blocking op. Finds loops one op away from SIMD. Noisy — off by default.
|
|
240
|
+
* @param {boolean} [opts.experimentalStencil] - Opt-in: vectorize neighbour-load
|
|
241
|
+
* stencils (`b[i]=f(a[i-1],a[i],a[i+1])`, 2-D 5-point) to f64x2. Bit-exact vs scalar.
|
|
242
|
+
* Unstable — off by default until proven across the corpus.
|
|
243
|
+
* @param {boolean} [opts.experimentalOuterStrip] - Opt-in: strip-mine a pixel loop whose
|
|
244
|
+
* per-pixel value is an inner reduction (metaballs-shape) into f64x2 lanes (2 pixels at
|
|
245
|
+
* once). Bit-exact vs scalar. Unstable — off by default.
|
|
246
|
+
* @param {boolean} [opts.experimentalToneMap] - Mixed-lane log-tonemap vectorizer: a flat
|
|
247
|
+
* `i32 dens[i] → f64 Math.log → i32 pack → px[i]` loop lifts to a 2-wide f64x2 island
|
|
248
|
+
* (fern/bifurcation/attractors). Bit-exact vs scalar; default-on at speed, pass `false` to disable.
|
|
186
249
|
* @param {object} [opts.warnings] - Optional mutable warning sink populated with
|
|
187
250
|
* `entries: [{ code, message, fn?, line?, column? }]`. Heap-growth advisories
|
|
188
251
|
* fire when a module uses the bump allocator and an export or loop retains
|
|
189
252
|
* allocations without a host-side memory.reset().
|
|
190
253
|
* @param {object} [opts.profile] - Optional mutable profile sink populated with
|
|
191
254
|
* `entries` and `totals` for parse / jzify / prepare / compile / plan / watr phases.
|
|
192
|
-
*
|
|
193
|
-
* for profiler/debugger symbolication.
|
|
255
|
+
* @param {boolean} [opts.names] - Emit a standard wasm `name` custom section (function
|
|
256
|
+
* symbols) for profiler/debugger symbolication. (Legacy: `profile.names = true`.)
|
|
194
257
|
* @param {Object<string,string>} [opts.modules] - Map of module specifier → source
|
|
195
258
|
* for compile-time `import`/`export` bundling: jz resolves the module graph
|
|
196
259
|
* in-process from this map instead of reading from disk.
|
|
@@ -305,10 +368,16 @@ const detectOptimizeConfig = (ast, code) => {
|
|
|
305
368
|
|
|
306
369
|
const cfg = {}
|
|
307
370
|
// Machine-generated or large code: watr's WAT-level CSE/DCE/inline fights
|
|
308
|
-
// jz's already-optimized IR and inflates output. Disable it automatically
|
|
371
|
+
// jz's already-optimized IR and inflates output. Disable it automatically —
|
|
372
|
+
// EXCEPT when the module uses SIMD intrinsics. A v128 helper call (e.g.
|
|
373
|
+
// f64x2.sin → $math.sin2) leaves v128 params/results across the call boundary;
|
|
374
|
+
// watr's inliner folds the helper into the loop and coalesces those vectors,
|
|
375
|
+
// and without it the v128 spills to memory every iteration (~2× slower on the
|
|
376
|
+
// trig-bound attractors kernel). Explicit SIMD is a perf opt-in — keep watr on.
|
|
377
|
+
const usesSimd = !!ctx.module?.modules?.simd
|
|
309
378
|
const isLarge = s.sourceChars > 4000 || s.funcCount > 40 || s.maxFuncBodySize > 300
|
|
310
379
|
const isMachineLike = s.callSites > 300 && s.stringLiteralCount < 10
|
|
311
|
-
if (isLarge || isMachineLike) { cfg.watr = false; cfg.splitCharScan = false }
|
|
380
|
+
if ((isLarge || isMachineLike) && !usesSimd) { cfg.watr = false; cfg.splitCharScan = false }
|
|
312
381
|
// Typed-array heavy: tighten scalarization thresholds when we see large
|
|
313
382
|
// fixed-size arrays; keep defaults for small/dynamic ones.
|
|
314
383
|
if (s.typedArrayCount > 0 && s.maxTypedArrayLen > 0) {
|
|
@@ -362,6 +431,15 @@ const setupCtx = (code, opts) => {
|
|
|
362
431
|
initWarnings(opts.warnings)
|
|
363
432
|
if (typeof opts.memory === 'number') ctx.memory.pages = opts.memory
|
|
364
433
|
else if (opts.memory) ctx.memory.shared = true
|
|
434
|
+
if (opts.importMemory) ctx.memory.shared = true // import env.memory instead of exporting own
|
|
435
|
+
if (opts.maxMemory != null) {
|
|
436
|
+
if (!Number.isInteger(opts.maxMemory) || opts.maxMemory < 1)
|
|
437
|
+
err(`opts.maxMemory must be a positive integer page count (each page is 64 KiB); got ${opts.maxMemory}`)
|
|
438
|
+
const initialPages = ctx.memory.pages || 1
|
|
439
|
+
if (opts.maxMemory < initialPages)
|
|
440
|
+
err(`opts.maxMemory (${opts.maxMemory}) is below the initial memory size (${initialPages} pages)`)
|
|
441
|
+
ctx.memory.max = opts.maxMemory
|
|
442
|
+
}
|
|
365
443
|
if (opts.modules) ctx.module.importSources = opts.modules
|
|
366
444
|
if (opts.imports) {
|
|
367
445
|
ctx.module.hostImports = opts.imports
|
|
@@ -384,6 +462,8 @@ const setupCtx = (code, opts) => {
|
|
|
384
462
|
}
|
|
385
463
|
if (opts.alloc === false) ctx.transform.alloc = false
|
|
386
464
|
if (opts.inspect) ctx.transform.inspect = true
|
|
465
|
+
if (opts.helperCounters) ctx.transform.helperCounters = true
|
|
466
|
+
if (opts.helperCallsites) ctx.transform.helperCallsites = opts.helperCallsites
|
|
387
467
|
if (opts.importMetaUrl) ctx.transform.importMetaUrl = String(opts.importMetaUrl)
|
|
388
468
|
if (opts.randomSeed !== undefined) {
|
|
389
469
|
if (opts.randomSeed !== true && !Number.isFinite(opts.randomSeed))
|
|
@@ -421,6 +501,7 @@ const rejectReservedPrefix = (node) => {
|
|
|
421
501
|
}
|
|
422
502
|
|
|
423
503
|
const jzCompileInner = (code, opts = {}) => {
|
|
504
|
+
if (opts.define) code = defineBindings(opts.define) + code
|
|
424
505
|
const profiler = compileProfiler(opts.profile)
|
|
425
506
|
const time = (name, fn) => profiler ? profiler.time(name, fn) : fn()
|
|
426
507
|
|
|
@@ -429,6 +510,12 @@ const jzCompileInner = (code, opts = {}) => {
|
|
|
429
510
|
|
|
430
511
|
let parsed = time('parse', () => parse(code))
|
|
431
512
|
if (typeof code === 'string' && code.includes(T)) rejectReservedPrefix(parsed)
|
|
513
|
+
// Lambda-lift immediately-invoked arrow literals to typed direct calls — lets SIMD
|
|
514
|
+
// flow through the f64-only closure ABI and drops the closure for every IIFE. Runs
|
|
515
|
+
// BEFORE jzify so it only sees USER arrow IIFEs, not jzify's synthetic wrapper IIFEs
|
|
516
|
+
// (named/recursive function expressions, method shorthand), which keep the closure
|
|
517
|
+
// path. A no-op when there are none.
|
|
518
|
+
parsed = time('liftIIFE', () => liftIIFEs(parsed))
|
|
432
519
|
if (!opts.strict) parsed = time('jzify', () => jzify(parsed))
|
|
433
520
|
const ast = time('prepare', () => prepare(parsed))
|
|
434
521
|
assertCtxInvariants('post-prepare')
|
|
@@ -449,6 +536,39 @@ const jzCompileInner = (code, opts = {}) => {
|
|
|
449
536
|
}
|
|
450
537
|
}
|
|
451
538
|
|
|
539
|
+
// opts.noSimd: force auto-vectorization off regardless of opt level — a
|
|
540
|
+
// portability escape hatch for engines without the SIMD proposal (parallels
|
|
541
|
+
// opts.noTailCall). Must suppress EVERY jz-emitted v128, which is two passes:
|
|
542
|
+
// vectorizeLaneLocal (lane maps, reductions incl. reduceUnroll, byte scans) AND
|
|
543
|
+
// the SLP store-pair packer (within-iteration f64x2). Both off, so `noSimd` is a
|
|
544
|
+
// true scalar baseline — the oracle SIMD-vs-scalar correctness tests compare
|
|
545
|
+
// against (missing one let an SLP miscompile pass as "SIMD == scalar"). Explicit
|
|
546
|
+
// f32x4/i32x4 intrinsics in source are the user's own opt-in and stay. Applied
|
|
547
|
+
// after auto-config so it wins over any re-resolved preset.
|
|
548
|
+
if (opts.noSimd) {
|
|
549
|
+
ctx.transform.optimize.vectorizeLaneLocal = false
|
|
550
|
+
ctx.transform.optimize.experimentalSlp = false
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// opts.whyNotSimd (CLI --why-not-simd): emit a `simd-why-not` warning per
|
|
554
|
+
// canonical loop that the auto-vectorizer declined, naming the blocking op —
|
|
555
|
+
// a diagnostic to find loops that are "one op away" from SIMD. Rides the
|
|
556
|
+
// resolved optimize cfg to the vectorizer; off by default (the report is noisy).
|
|
557
|
+
if (opts.whyNotSimd && ctx.transform.optimize) ctx.transform.optimize.whyNotSimd = true
|
|
558
|
+
|
|
559
|
+
// opts.experimentalStencil: the neighbour-load stencil vectorizer (a[i±1] / 2-D 5-point).
|
|
560
|
+
// Now default-on at optimize:'speed' (proven bit-exact corpus-wide); the opt is two-way so an
|
|
561
|
+
// explicit `false` can still disable it (e.g. to A/B against the scalar path).
|
|
562
|
+
if (opts.experimentalStencil !== undefined && ctx.transform.optimize) ctx.transform.optimize.experimentalStencil = !!opts.experimentalStencil
|
|
563
|
+
|
|
564
|
+
// opts.experimentalOuterStrip: the outer-loop strip-mine vectorizer (2 adjacent pixels in f64x2
|
|
565
|
+
// lanes over an inner reduction). Default-on at speed; two-way like experimentalStencil.
|
|
566
|
+
if (opts.experimentalOuterStrip !== undefined && ctx.transform.optimize) ctx.transform.optimize.experimentalOuterStrip = !!opts.experimentalOuterStrip
|
|
567
|
+
|
|
568
|
+
// opts.experimentalToneMap: the mixed-lane log-tonemap vectorizer (i32 dens[i] → f64 log →
|
|
569
|
+
// i32 pack → px[i], 2-wide f64x2 island). Default-on at speed; two-way like the others.
|
|
570
|
+
if (opts.experimentalToneMap !== undefined && ctx.transform.optimize) ctx.transform.optimize.experimentalToneMap = !!opts.experimentalToneMap
|
|
571
|
+
|
|
452
572
|
const module = time('compile', () => compile(ast, profiler))
|
|
453
573
|
assertCtxInvariants('post-compile')
|
|
454
574
|
|
|
@@ -470,6 +590,10 @@ const jzCompileInner = (code, opts = {}) => {
|
|
|
470
590
|
const cfg = ctx.transform.optimize
|
|
471
591
|
let watrOpts = typeof cfg.watr === 'object' ? { ...cfg.watr } : true
|
|
472
592
|
if (cfg.vectorizeLaneLocal) {
|
|
593
|
+
// Off at the speed tier: watr's loopify collapses the while-idiom to
|
|
594
|
+
// `loop { if C { …; br } }` (an UNfused back-jump — no win) AND would mangle the
|
|
595
|
+
// lane-vectorizer's loop shape. jz's own `rotateLoops` (optimize/index.js, post
|
|
596
|
+
// phase) does the rotation instead, emitting the FUSED `br_if $loop C` back-edge.
|
|
473
597
|
if (watrOpts === true) watrOpts = { loopify: false }
|
|
474
598
|
else if (typeof watrOpts === 'object' && watrOpts.loopify === undefined) watrOpts.loopify = false
|
|
475
599
|
}
|
|
@@ -477,7 +601,34 @@ const jzCompileInner = (code, opts = {}) => {
|
|
|
477
601
|
if (watrOpts === true) watrOpts = { devirt: true }
|
|
478
602
|
else if (typeof watrOpts === 'object' && watrOpts.devirt === undefined) watrOpts.devirt = true
|
|
479
603
|
}
|
|
604
|
+
// Multi-caller small-function inlining (size-for-speed): on at the 'speed'/level-3
|
|
605
|
+
// tier only, like devirt above. Removes call overhead from hot inner loops at the
|
|
606
|
+
// cost of duplicating tiny bodies; level 2 (and the size budgets measured there)
|
|
607
|
+
// stay untouched.
|
|
608
|
+
if (cfg.inlineFns) {
|
|
609
|
+
if (watrOpts === true) watrOpts = { inline: 'simd' }
|
|
610
|
+
else if (typeof watrOpts === 'object' && watrOpts.inline === undefined) watrOpts.inline = 'simd'
|
|
611
|
+
}
|
|
612
|
+
// guardRefine folds jz's NaN-box tag reads — default-off in watr (general WAT
|
|
613
|
+
// never matches it), so jz always enables it explicitly.
|
|
614
|
+
if (watrOpts === true) watrOpts = { guardRefine: true }
|
|
615
|
+
else if (typeof watrOpts === 'object' && watrOpts.guardRefine === undefined) watrOpts.guardRefine = true
|
|
616
|
+
// Pin jz's scalar transcendentals (the PPC_CALL2 keys the auto-vectorizer rewrites to f64x2
|
|
617
|
+
// mirrors) so watr's inline passes don't dissolve the call nodes the lift needs. The protection
|
|
618
|
+
// policy lives here in jz — watr just honours the `pin` list (no jz names hardcoded in watr).
|
|
619
|
+
if (cfg.watr) {
|
|
620
|
+
if (watrOpts === true) watrOpts = {}
|
|
621
|
+
watrOpts.pin = watrOpts.pin ? [...watrOpts.pin, ...SIMD_PINNED] : SIMD_PINNED
|
|
622
|
+
}
|
|
623
|
+
// Capture the per-func alias facts (param-distinctness) the emit phase stamped as JS
|
|
624
|
+
// expandos — watOptimize round-trips the module through watr's printer/parser, which
|
|
625
|
+
// rebuilds plain arrays and DROPS every non-index property. Without re-attaching, the
|
|
626
|
+
// 'post' leaf passes (splitLoopPrivateScratch, hoistInvariantLoop) lose the proven
|
|
627
|
+
// alias model and fall back to weaker/heuristic invariance — the exact reason raytrace's
|
|
628
|
+
// read-only sphere loads couldn't be SOUNDLY proven loop-invariant after watr ran.
|
|
629
|
+
const aliasFacts = cfg.watr ? new Map(module.filter(n => Array.isArray(n) && n[0] === 'func' && n.distinctParams).map(n => [n[1], n.distinctParams])) : null
|
|
480
630
|
const optimized = cfg.watr ? time('watOptimize', () => watOptimize(module, watrOpts)) : module
|
|
631
|
+
if (aliasFacts?.size) for (const n of optimized) if (Array.isArray(n) && n[0] === 'func' && aliasFacts.has(n[1])) n.distinctParams = aliasFacts.get(n[1])
|
|
481
632
|
// Stable-pointee module globals: resolve the __ptr_offset once per function.
|
|
482
633
|
// Never-forwarding kinds — every PTR tag outside __ptr_offset's forwarding
|
|
483
634
|
// set {ARRAY, HASH, SET, MAP} — give the same offset for the same bits, so
|
|
@@ -502,12 +653,45 @@ const jzCompileInner = (code, opts = {}) => {
|
|
|
502
653
|
if (cfg.watr) {
|
|
503
654
|
// Build global name→type map from ctx.scope.globalTypes for promoteGlobals
|
|
504
655
|
const globalTypesMap = ctx.scope.globalTypes ? new Map([...ctx.scope.globalTypes].map(([k, v]) => [`$${k}`, v])) : null
|
|
656
|
+
// Build pure-function map for Phase 2 user-function inline in tryPerPixelColor.
|
|
657
|
+
// A function is "pure for SIMD inline" if its body contains no side effects:
|
|
658
|
+
// no global.set, no memory stores, no calls outside $math.*.
|
|
659
|
+
// foldStrDispatchF64 is run first (idempotent) so the purity check sees the
|
|
660
|
+
// folded body — dead __is_str_key dispatch would otherwise look impure.
|
|
661
|
+
if (cfg.vectorizeLaneLocal === true) {
|
|
662
|
+
const allFuncs = optimized.filter(node => Array.isArray(node) && node[0] === 'func')
|
|
663
|
+
const pureFuncMap = new Map()
|
|
664
|
+
const hasSideEffect = (node) => {
|
|
665
|
+
if (!Array.isArray(node)) return false
|
|
666
|
+
const op = node[0]
|
|
667
|
+
if (op === 'global.set') return true
|
|
668
|
+
if (typeof op === 'string' && (op.endsWith('.store') || op.startsWith('memory.'))) return true
|
|
669
|
+
if (op === 'call' && typeof node[1] === 'string' && !node[1].startsWith('$math.')) return true
|
|
670
|
+
if (op === 'call_indirect' || op === 'call_ref') return true
|
|
671
|
+
return node.some((c, i) => i > 0 && hasSideEffect(c))
|
|
672
|
+
}
|
|
673
|
+
for (const fn of allFuncs) {
|
|
674
|
+
const name = fn[1]
|
|
675
|
+
if (typeof name !== 'string' || name.startsWith('$math.') || name.startsWith('$__')) continue
|
|
676
|
+
// Fold dead str-dispatch blocks so purity check sees the clean form.
|
|
677
|
+
foldStrDispatchF64(fn)
|
|
678
|
+
const bodyStart = findBodyStart(fn)
|
|
679
|
+
if (bodyStart < 0) continue
|
|
680
|
+
let pure = true
|
|
681
|
+
for (let i = bodyStart; i < fn.length; i++) if (hasSideEffect(fn[i])) { pure = false; break }
|
|
682
|
+
if (pure) pureFuncMap.set(name, fn)
|
|
683
|
+
}
|
|
684
|
+
if (pureFuncMap.size) cfg._pureFuncMap = pureFuncMap
|
|
685
|
+
}
|
|
505
686
|
time('watrReopt', () => {
|
|
506
687
|
const funcs = optimized.filter(node => Array.isArray(node) && node[0] === 'func')
|
|
507
688
|
const volatileGlobals = collectVolatileGlobals(funcs)
|
|
508
689
|
const reach = collectReachableGlobalWrites(funcs)
|
|
509
690
|
for (const node of funcs) optimizeFunc(node, cfg, globalTypesMap, volatileGlobals, 'post', reach)
|
|
510
691
|
})
|
|
692
|
+
// The 'post' lane vectorizer can inject stdlib calls (e.g. the f64x2 trig mirror $math.cos2)
|
|
693
|
+
// absent from the already-pulled+treeshaken module — append any now-referenced helper body.
|
|
694
|
+
appendLateStdlib(optimized)
|
|
511
695
|
}
|
|
512
696
|
try {
|
|
513
697
|
if (opts.wat) {
|
|
@@ -516,7 +700,9 @@ const jzCompileInner = (code, opts = {}) => {
|
|
|
516
700
|
}
|
|
517
701
|
const wasm = time('watrCompile', () => watrCompile(optimized))
|
|
518
702
|
let bytes = wasm
|
|
519
|
-
|
|
703
|
+
// opts.names emits a wasm `name` custom section (symbols for profilers/
|
|
704
|
+
// debuggers). opts.profile.names is the older spelling — still honored.
|
|
705
|
+
if (opts.names || opts.profile?.names) bytes = appendFunctionNames(bytes, optimized)
|
|
520
706
|
return opts.inspect ? { wasm: bytes, inspect: ctx.inspect } : bytes
|
|
521
707
|
} catch (e) {
|
|
522
708
|
// watr surfaces dangling identifiers as "Unknown local|func|global|table|memory $X".
|
|
@@ -546,27 +732,6 @@ export default function jz(code, ...args) {
|
|
|
546
732
|
if (Array.isArray(code)) {
|
|
547
733
|
const interp = {}, data = {}, hoisted = []
|
|
548
734
|
|
|
549
|
-
// Serialize JS value to jz source literal. Returns null if not serializable.
|
|
550
|
-
const serialize = (v) => {
|
|
551
|
-
if (v === undefined) return 'undefined' // else falls through → treated as a
|
|
552
|
-
// host object → memory.Object(undefined) → Object.keys(undefined) crash
|
|
553
|
-
if (typeof v === 'number' || typeof v === 'boolean') return String(v)
|
|
554
|
-
if (v === null) return 'null'
|
|
555
|
-
if (typeof v === 'string') return JSON.stringify(v)
|
|
556
|
-
if (Array.isArray(v)) {
|
|
557
|
-
const elems = v.map(serialize)
|
|
558
|
-
return elems.every(e => e !== null) ? `[${elems.join(', ')}]` : null
|
|
559
|
-
}
|
|
560
|
-
if (typeof v === 'object') {
|
|
561
|
-
const props = Object.keys(v).map(k => {
|
|
562
|
-
const s = serialize(v[k])
|
|
563
|
-
return s !== null ? `${k}: ${s}` : null
|
|
564
|
-
})
|
|
565
|
-
return props.every(p => p !== null) ? `{${props.join(', ')}}` : null
|
|
566
|
-
}
|
|
567
|
-
return null
|
|
568
|
-
}
|
|
569
|
-
|
|
570
735
|
let src = code[0]
|
|
571
736
|
for (let i = 0; i < args.length; i++) {
|
|
572
737
|
const v = args[i]
|
|
@@ -618,3 +783,25 @@ export default function jz(code, ...args) {
|
|
|
618
783
|
export { jz }
|
|
619
784
|
const jzCompile = jz.compile
|
|
620
785
|
export { jzCompile as compile }
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Compile source to a cached `WebAssembly.Module` (compile + validate once).
|
|
789
|
+
* Pair with `instantiate(module, opts)` to spin up many instances without
|
|
790
|
+
* re-validating the bytes each time — the AOT compile is paid once, so repeated
|
|
791
|
+
* instantiation is cheap. Returns a `WebAssembly.Module`, built with the native
|
|
792
|
+
* `wasm:js-string` builtins fast path where the engine supports it.
|
|
793
|
+
*
|
|
794
|
+
* import { compileModule, instantiate } from 'jz'
|
|
795
|
+
* const mod = compileModule('export let f = x => x * 2')
|
|
796
|
+
* const { exports } = instantiate(mod) // repeat cheaply, no recompile
|
|
797
|
+
*
|
|
798
|
+
* @param {string} code
|
|
799
|
+
* @param {object} [opts] - same options as `compile()`
|
|
800
|
+
* @returns {WebAssembly.Module}
|
|
801
|
+
*/
|
|
802
|
+
export const compileModule = (code, opts = {}) => {
|
|
803
|
+
const out = jzCompile(code, opts)
|
|
804
|
+
return toModule(out && typeof out === 'object' && 'wasm' in out ? out.wasm : out)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export { instantiateRuntime as instantiate }
|