jz 0.6.0 → 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 +66 -43
- package/bench/README.md +128 -78
- package/bench/bench.svg +32 -42
- package/cli.js +73 -7
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +190 -34
- package/interop.js +71 -27
- package/layout.js +5 -0
- package/module/array.js +71 -39
- package/module/collection.js +41 -5
- package/module/core.js +2 -4
- package/module/math.js +138 -2
- package/module/number.js +21 -0
- package/module/object.js +26 -0
- package/module/simd.js +37 -5
- package/module/string.js +41 -12
- package/module/typedarray.js +415 -26
- package/package.json +21 -6
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +38 -3
- package/src/compile/emit-assign.js +9 -6
- package/src/compile/emit.js +347 -36
- package/src/compile/index.js +307 -29
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +155 -0
- package/src/compile/narrow.js +108 -11
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/index.js +4 -0
- package/src/compile/plan/inline.js +5 -2
- package/src/compile/plan/literals.js +220 -5
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +45 -7
- package/src/ir.js +55 -7
- package/src/kind-traits.js +27 -0
- package/src/kind.js +65 -3
- package/src/optimize/index.js +344 -45
- package/src/optimize/vectorize.js +2968 -182
- package/src/prepare/index.js +64 -7
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +3 -2
- package/src/static.js +9 -0
- package/src/type.js +10 -6
- package/src/wat/assemble.js +195 -13
- package/src/wat/optimize.js +363 -185
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
|
@@ -44,17 +44,20 @@ import { parse } from './src/parse.js'
|
|
|
44
44
|
import watrCompile from "watr/compile";
|
|
45
45
|
import watrPrint from "watr/print";
|
|
46
46
|
import watOptimize from "./src/wat/optimize.js";
|
|
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 } 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
|
|
@@ -421,6 +499,7 @@ const rejectReservedPrefix = (node) => {
|
|
|
421
499
|
}
|
|
422
500
|
|
|
423
501
|
const jzCompileInner = (code, opts = {}) => {
|
|
502
|
+
if (opts.define) code = defineBindings(opts.define) + code
|
|
424
503
|
const profiler = compileProfiler(opts.profile)
|
|
425
504
|
const time = (name, fn) => profiler ? profiler.time(name, fn) : fn()
|
|
426
505
|
|
|
@@ -429,6 +508,12 @@ const jzCompileInner = (code, opts = {}) => {
|
|
|
429
508
|
|
|
430
509
|
let parsed = time('parse', () => parse(code))
|
|
431
510
|
if (typeof code === 'string' && code.includes(T)) rejectReservedPrefix(parsed)
|
|
511
|
+
// Lambda-lift immediately-invoked arrow literals to typed direct calls — lets SIMD
|
|
512
|
+
// flow through the f64-only closure ABI and drops the closure for every IIFE. Runs
|
|
513
|
+
// BEFORE jzify so it only sees USER arrow IIFEs, not jzify's synthetic wrapper IIFEs
|
|
514
|
+
// (named/recursive function expressions, method shorthand), which keep the closure
|
|
515
|
+
// path. A no-op when there are none.
|
|
516
|
+
parsed = time('liftIIFE', () => liftIIFEs(parsed))
|
|
432
517
|
if (!opts.strict) parsed = time('jzify', () => jzify(parsed))
|
|
433
518
|
const ast = time('prepare', () => prepare(parsed))
|
|
434
519
|
assertCtxInvariants('post-prepare')
|
|
@@ -449,6 +534,33 @@ const jzCompileInner = (code, opts = {}) => {
|
|
|
449
534
|
}
|
|
450
535
|
}
|
|
451
536
|
|
|
537
|
+
// opts.noSimd: force auto-vectorization off regardless of opt level — a
|
|
538
|
+
// portability escape hatch for engines without the SIMD proposal (parallels
|
|
539
|
+
// opts.noTailCall). Disabling the vectorizeLaneLocal pass suppresses every
|
|
540
|
+
// jz-emitted v128 (lane maps, reductions incl. reduceUnroll, byte scans);
|
|
541
|
+
// explicit f32x4/i32x4 intrinsics in source are the user's own opt-in and stay.
|
|
542
|
+
// Applied after auto-config so it wins over any re-resolved preset.
|
|
543
|
+
if (opts.noSimd) ctx.transform.optimize.vectorizeLaneLocal = false
|
|
544
|
+
|
|
545
|
+
// opts.whyNotSimd (CLI --why-not-simd): emit a `simd-why-not` warning per
|
|
546
|
+
// canonical loop that the auto-vectorizer declined, naming the blocking op —
|
|
547
|
+
// a diagnostic to find loops that are "one op away" from SIMD. Rides the
|
|
548
|
+
// resolved optimize cfg to the vectorizer; off by default (the report is noisy).
|
|
549
|
+
if (opts.whyNotSimd && ctx.transform.optimize) ctx.transform.optimize.whyNotSimd = true
|
|
550
|
+
|
|
551
|
+
// opts.experimentalStencil: the neighbour-load stencil vectorizer (a[i±1] / 2-D 5-point).
|
|
552
|
+
// Now default-on at optimize:'speed' (proven bit-exact corpus-wide); the opt is two-way so an
|
|
553
|
+
// explicit `false` can still disable it (e.g. to A/B against the scalar path).
|
|
554
|
+
if (opts.experimentalStencil !== undefined && ctx.transform.optimize) ctx.transform.optimize.experimentalStencil = !!opts.experimentalStencil
|
|
555
|
+
|
|
556
|
+
// opts.experimentalOuterStrip: the outer-loop strip-mine vectorizer (2 adjacent pixels in f64x2
|
|
557
|
+
// lanes over an inner reduction). Default-on at speed; two-way like experimentalStencil.
|
|
558
|
+
if (opts.experimentalOuterStrip !== undefined && ctx.transform.optimize) ctx.transform.optimize.experimentalOuterStrip = !!opts.experimentalOuterStrip
|
|
559
|
+
|
|
560
|
+
// opts.experimentalToneMap: the mixed-lane log-tonemap vectorizer (i32 dens[i] → f64 log →
|
|
561
|
+
// i32 pack → px[i], 2-wide f64x2 island). Default-on at speed; two-way like the others.
|
|
562
|
+
if (opts.experimentalToneMap !== undefined && ctx.transform.optimize) ctx.transform.optimize.experimentalToneMap = !!opts.experimentalToneMap
|
|
563
|
+
|
|
452
564
|
const module = time('compile', () => compile(ast, profiler))
|
|
453
565
|
assertCtxInvariants('post-compile')
|
|
454
566
|
|
|
@@ -477,6 +589,14 @@ const jzCompileInner = (code, opts = {}) => {
|
|
|
477
589
|
if (watrOpts === true) watrOpts = { devirt: true }
|
|
478
590
|
else if (typeof watrOpts === 'object' && watrOpts.devirt === undefined) watrOpts.devirt = true
|
|
479
591
|
}
|
|
592
|
+
// Multi-caller small-function inlining (size-for-speed): on at the 'speed'/level-3
|
|
593
|
+
// tier only, like devirt above. Removes call overhead from hot inner loops at the
|
|
594
|
+
// cost of duplicating tiny bodies; level 2 (and the size budgets measured there)
|
|
595
|
+
// stay untouched.
|
|
596
|
+
if (cfg.inlineFns) {
|
|
597
|
+
if (watrOpts === true) watrOpts = { inline: true }
|
|
598
|
+
else if (typeof watrOpts === 'object' && watrOpts.inline === undefined) watrOpts.inline = true
|
|
599
|
+
}
|
|
480
600
|
const optimized = cfg.watr ? time('watOptimize', () => watOptimize(module, watrOpts)) : module
|
|
481
601
|
// Stable-pointee module globals: resolve the __ptr_offset once per function.
|
|
482
602
|
// Never-forwarding kinds — every PTR tag outside __ptr_offset's forwarding
|
|
@@ -502,12 +622,45 @@ const jzCompileInner = (code, opts = {}) => {
|
|
|
502
622
|
if (cfg.watr) {
|
|
503
623
|
// Build global name→type map from ctx.scope.globalTypes for promoteGlobals
|
|
504
624
|
const globalTypesMap = ctx.scope.globalTypes ? new Map([...ctx.scope.globalTypes].map(([k, v]) => [`$${k}`, v])) : null
|
|
625
|
+
// Build pure-function map for Phase 2 user-function inline in tryPerPixelColor.
|
|
626
|
+
// A function is "pure for SIMD inline" if its body contains no side effects:
|
|
627
|
+
// no global.set, no memory stores, no calls outside $math.*.
|
|
628
|
+
// foldStrDispatchF64 is run first (idempotent) so the purity check sees the
|
|
629
|
+
// folded body — dead __is_str_key dispatch would otherwise look impure.
|
|
630
|
+
if (cfg.vectorizeLaneLocal === true) {
|
|
631
|
+
const allFuncs = optimized.filter(node => Array.isArray(node) && node[0] === 'func')
|
|
632
|
+
const pureFuncMap = new Map()
|
|
633
|
+
const hasSideEffect = (node) => {
|
|
634
|
+
if (!Array.isArray(node)) return false
|
|
635
|
+
const op = node[0]
|
|
636
|
+
if (op === 'global.set') return true
|
|
637
|
+
if (typeof op === 'string' && (op.endsWith('.store') || op.startsWith('memory.'))) return true
|
|
638
|
+
if (op === 'call' && typeof node[1] === 'string' && !node[1].startsWith('$math.')) return true
|
|
639
|
+
if (op === 'call_indirect' || op === 'call_ref') return true
|
|
640
|
+
return node.some((c, i) => i > 0 && hasSideEffect(c))
|
|
641
|
+
}
|
|
642
|
+
for (const fn of allFuncs) {
|
|
643
|
+
const name = fn[1]
|
|
644
|
+
if (typeof name !== 'string' || name.startsWith('$math.') || name.startsWith('$__')) continue
|
|
645
|
+
// Fold dead str-dispatch blocks so purity check sees the clean form.
|
|
646
|
+
foldStrDispatchF64(fn)
|
|
647
|
+
const bodyStart = findBodyStart(fn)
|
|
648
|
+
if (bodyStart < 0) continue
|
|
649
|
+
let pure = true
|
|
650
|
+
for (let i = bodyStart; i < fn.length; i++) if (hasSideEffect(fn[i])) { pure = false; break }
|
|
651
|
+
if (pure) pureFuncMap.set(name, fn)
|
|
652
|
+
}
|
|
653
|
+
if (pureFuncMap.size) cfg._pureFuncMap = pureFuncMap
|
|
654
|
+
}
|
|
505
655
|
time('watrReopt', () => {
|
|
506
656
|
const funcs = optimized.filter(node => Array.isArray(node) && node[0] === 'func')
|
|
507
657
|
const volatileGlobals = collectVolatileGlobals(funcs)
|
|
508
658
|
const reach = collectReachableGlobalWrites(funcs)
|
|
509
659
|
for (const node of funcs) optimizeFunc(node, cfg, globalTypesMap, volatileGlobals, 'post', reach)
|
|
510
660
|
})
|
|
661
|
+
// The 'post' lane vectorizer can inject stdlib calls (e.g. the f64x2 trig mirror $math.cos2)
|
|
662
|
+
// absent from the already-pulled+treeshaken module — append any now-referenced helper body.
|
|
663
|
+
appendLateStdlib(optimized)
|
|
511
664
|
}
|
|
512
665
|
try {
|
|
513
666
|
if (opts.wat) {
|
|
@@ -516,7 +669,9 @@ const jzCompileInner = (code, opts = {}) => {
|
|
|
516
669
|
}
|
|
517
670
|
const wasm = time('watrCompile', () => watrCompile(optimized))
|
|
518
671
|
let bytes = wasm
|
|
519
|
-
|
|
672
|
+
// opts.names emits a wasm `name` custom section (symbols for profilers/
|
|
673
|
+
// debuggers). opts.profile.names is the older spelling — still honored.
|
|
674
|
+
if (opts.names || opts.profile?.names) bytes = appendFunctionNames(bytes, optimized)
|
|
520
675
|
return opts.inspect ? { wasm: bytes, inspect: ctx.inspect } : bytes
|
|
521
676
|
} catch (e) {
|
|
522
677
|
// watr surfaces dangling identifiers as "Unknown local|func|global|table|memory $X".
|
|
@@ -546,27 +701,6 @@ export default function jz(code, ...args) {
|
|
|
546
701
|
if (Array.isArray(code)) {
|
|
547
702
|
const interp = {}, data = {}, hoisted = []
|
|
548
703
|
|
|
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
704
|
let src = code[0]
|
|
571
705
|
for (let i = 0; i < args.length; i++) {
|
|
572
706
|
const v = args[i]
|
|
@@ -618,3 +752,25 @@ export default function jz(code, ...args) {
|
|
|
618
752
|
export { jz }
|
|
619
753
|
const jzCompile = jz.compile
|
|
620
754
|
export { jzCompile as compile }
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Compile source to a cached `WebAssembly.Module` (compile + validate once).
|
|
758
|
+
* Pair with `instantiate(module, opts)` to spin up many instances without
|
|
759
|
+
* re-validating the bytes each time — the AOT compile is paid once, so repeated
|
|
760
|
+
* instantiation is cheap. Returns a `WebAssembly.Module`, built with the native
|
|
761
|
+
* `wasm:js-string` builtins fast path where the engine supports it.
|
|
762
|
+
*
|
|
763
|
+
* import { compileModule, instantiate } from 'jz'
|
|
764
|
+
* const mod = compileModule('export let f = x => x * 2')
|
|
765
|
+
* const { exports } = instantiate(mod) // repeat cheaply, no recompile
|
|
766
|
+
*
|
|
767
|
+
* @param {string} code
|
|
768
|
+
* @param {object} [opts] - same options as `compile()`
|
|
769
|
+
* @returns {WebAssembly.Module}
|
|
770
|
+
*/
|
|
771
|
+
export const compileModule = (code, opts = {}) => {
|
|
772
|
+
const out = jzCompile(code, opts)
|
|
773
|
+
return toModule(out && typeof out === 'object' && 'wasm' in out ? out.wasm : out)
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export { instantiateRuntime as instantiate }
|
package/interop.js
CHANGED
|
@@ -132,10 +132,24 @@ _u32[1] = ATOM_HI[ATOM.TRUE]; _u32[0] = 0; export const TRUE_NAN = _f64[0]
|
|
|
132
132
|
// Coerce JS null/undefined → NaN-boxed sentinels for WASM boundary
|
|
133
133
|
export const coerce = v => v === null ? NULL_NAN : v === undefined ? UNDEF_NAN : v
|
|
134
134
|
|
|
135
|
-
//
|
|
135
|
+
// Memory-free decode of an f64 boundary value: numbers pass through, the reserved
|
|
136
|
+
// atoms become null/undefined/false/true, and an SSO string unpacks its ≤4 inline
|
|
137
|
+
// ASCII bytes straight from the NaN payload. These are exactly the value forms a
|
|
138
|
+
// *memoryless* module can carry across the boundary — a heap string/array/object
|
|
139
|
+
// would need linear memory to exist, so it can't reach here. Heap-carrying modules
|
|
140
|
+
// route through `mem.read` instead (which also handles these, via memory).
|
|
136
141
|
const decode = v => {
|
|
137
|
-
if (v === v) return v // fast path: non-NaN
|
|
142
|
+
if (v === v) return v // fast path: non-NaN number
|
|
138
143
|
_f64[0] = v
|
|
144
|
+
// STRING (type 4) + SSO bit: content is the low 32 bits, length the low aux bits.
|
|
145
|
+
if (decodePtrType(_u32[1]) === 4) {
|
|
146
|
+
const a = decodePtrAux(_u32[1])
|
|
147
|
+
if (a & LAYOUT.SSO_BIT) {
|
|
148
|
+
const len = a & 0x7, off = _u32[0]; let s = ''
|
|
149
|
+
for (let i = 0; i < len; i++) s += String.fromCharCode((off >>> (i * 8)) & 0xFF)
|
|
150
|
+
return s
|
|
151
|
+
}
|
|
152
|
+
}
|
|
139
153
|
if (_u32[0] !== 0) return v
|
|
140
154
|
if (_u32[1] === ATOM_HI[ATOM.NULL]) return null
|
|
141
155
|
if (_u32[1] === ATOM_HI[ATOM.UNDEF]) return undefined
|
|
@@ -144,6 +158,14 @@ const decode = v => {
|
|
|
144
158
|
return v
|
|
145
159
|
}
|
|
146
160
|
|
|
161
|
+
// Decode a boundary value that arrives as i64 bits (BigInt carrier) into a JS
|
|
162
|
+
// value. Heap-carrying modules go through `mem.read`; a memoryless module's value
|
|
163
|
+
// is a number/atom/SSO string, decodable from the bits alone.
|
|
164
|
+
const readArgBits = (state, big) => {
|
|
165
|
+
const f = i64ToF64(big)
|
|
166
|
+
return state.mem ? state.mem.read(f) : decode(f)
|
|
167
|
+
}
|
|
168
|
+
|
|
147
169
|
export const ptr = (type, aux, offset) => {
|
|
148
170
|
_u32[1] = encodePtrHi(type, aux)
|
|
149
171
|
_u32[0] = offset >>> 0; return _f64[0]
|
|
@@ -196,7 +218,11 @@ export const memory = (src) => {
|
|
|
196
218
|
// Instance result: { module, instance, exports, extMap }
|
|
197
219
|
const raw = src?.instance?.exports || src?.exports || src
|
|
198
220
|
mem = src?.exports?.memory || raw.memory
|
|
199
|
-
|
|
221
|
+
// Memoryless module (SSO strings / atoms / numbers only — no linear memory):
|
|
222
|
+
// hand back a minimal reader instead of null so callers can still decode its
|
|
223
|
+
// boundary values from bits. `read`/`wrapVal` cover the value forms that exist
|
|
224
|
+
// without memory; `scalar` flags the fast path that skips heap marshaling.
|
|
225
|
+
if (!mem) return { read: decode, wrapVal: coerce, scalar: true }
|
|
200
226
|
wasmExports = { ...raw, memory: mem }
|
|
201
227
|
extMap = src.extMap || null
|
|
202
228
|
mod = src.module || null
|
|
@@ -249,8 +275,7 @@ export const memory = (src) => {
|
|
|
249
275
|
if (_enhanced.has(mem)) {
|
|
250
276
|
mem.schemas = schemas
|
|
251
277
|
if (wasmExports?._alloc) { alloc = wasmExports._alloc; mem.alloc = alloc }
|
|
252
|
-
|
|
253
|
-
else if (!mem.reset) mem.reset = jsReset
|
|
278
|
+
mem.reset = jsReset // post-init rewind — see the note at the first-enhance path
|
|
254
279
|
if (extMap) mem._extMap = extMap
|
|
255
280
|
return mem
|
|
256
281
|
}
|
|
@@ -471,7 +496,14 @@ export const memory = (src) => {
|
|
|
471
496
|
}
|
|
472
497
|
|
|
473
498
|
mem.alloc = alloc
|
|
474
|
-
|
|
499
|
+
// Rewind to the JS-captured post-init heap mark, NOT the wasm `_clear` (which
|
|
500
|
+
// rewinds to the static-data end and would clobber module-global heap values —
|
|
501
|
+
// a top-level `let o = {…}` — on the first alloc after reset). `jsReset`'s base
|
|
502
|
+
// is `$__heap` read after instantiation (start ran), i.e. exactly the high-water
|
|
503
|
+
// mark above all module-init allocations. Both share `$__heap`, so a wasm `_alloc`
|
|
504
|
+
// and this reset stay consistent. (Shared memory has no `$__heap` global → base
|
|
505
|
+
// is the fixed start, preserving prior behavior.)
|
|
506
|
+
mem.reset = jsReset
|
|
475
507
|
|
|
476
508
|
// TypedArray constructors: memory.Float64Array(data), etc.
|
|
477
509
|
// Bulk-copy path: when input is a TypedArray whose element type matches
|
|
@@ -536,7 +568,9 @@ export const wrap = (memSrc, inst) => {
|
|
|
536
568
|
const bits = lastErrBits.value
|
|
537
569
|
_u32[0] = Number(bits & 0xffffffffn)
|
|
538
570
|
_u32[1] = Number((bits >> 32n) & 0xffffffffn)
|
|
539
|
-
|
|
571
|
+
// Memoryless module: the thrown value is a number/atom/SSO string — decode it
|
|
572
|
+
// from bits. (A heap Error/string can only exist when the module has memory.)
|
|
573
|
+
const value = mem ? mem.read(_f64[0]) : decode(_f64[0])
|
|
540
574
|
if (value instanceof Error) throw value
|
|
541
575
|
const wrapped = new Error(typeof value === 'string' ? value : String(value))
|
|
542
576
|
wrapped.cause = error
|
|
@@ -554,7 +588,7 @@ export const wrap = (memSrc, inst) => {
|
|
|
554
588
|
ext?.has(i) ? (x === undefined && ext.def?.has(i) ? ext.def.get(i) : x) : box(x)
|
|
555
589
|
|
|
556
590
|
// Pure scalar module (no memory): pass f64 values directly, no marshaling
|
|
557
|
-
if (!mem) {
|
|
591
|
+
if (!mem || mem.scalar) {
|
|
558
592
|
for (const [name, fn] of Object.entries(realInst.exports)) {
|
|
559
593
|
if (typeof fn !== 'function') { exports[name] = fn; continue }
|
|
560
594
|
const ext = extExp.get(name)
|
|
@@ -649,7 +683,7 @@ const installDefaultEnvImports = (mod, imports, state) => {
|
|
|
649
683
|
// across the wasm→JS boundary (see module/console.js header). Reinterpret
|
|
650
684
|
// the BigInt's bits as f64 here so mem.read sees the original NaN-box.
|
|
651
685
|
const write = (valBig, fd, sep) => {
|
|
652
|
-
const v = state
|
|
686
|
+
const v = readArgBits(state, valBig)
|
|
653
687
|
buf[fd] += String(v)
|
|
654
688
|
if (sep === 32) buf[fd] += ' '
|
|
655
689
|
else if (sep === 10) flush(fd)
|
|
@@ -679,13 +713,13 @@ const installDefaultEnvImports = (mod, imports, state) => {
|
|
|
679
713
|
}
|
|
680
714
|
if (envFns.has('parseFloat') && !imports.env.parseFloat) {
|
|
681
715
|
imports.env.parseFloat = (valBig) => {
|
|
682
|
-
const s = state
|
|
716
|
+
const s = readArgBits(state, valBig)
|
|
683
717
|
return parseFloat(s)
|
|
684
718
|
}
|
|
685
719
|
}
|
|
686
720
|
if (envFns.has('parseInt') && !imports.env.parseInt) {
|
|
687
721
|
imports.env.parseInt = (valBig, radix) => {
|
|
688
|
-
const s = state
|
|
722
|
+
const s = readArgBits(state, valBig)
|
|
689
723
|
return parseInt(s, radix || undefined)
|
|
690
724
|
}
|
|
691
725
|
}
|
|
@@ -834,7 +868,11 @@ const finishInstantiation = (mod, inst, imports, needsWasi, opts, state) => {
|
|
|
834
868
|
const enhanced = memory(memSrc)
|
|
835
869
|
state.mem = enhanced
|
|
836
870
|
state.flushPrint?.()
|
|
837
|
-
|
|
871
|
+
// A memoryless module keeps a minimal reader internally (state.mem, for decoding
|
|
872
|
+
// its SSO/atom boundary values), but the result's `.memory` stays null — the
|
|
873
|
+
// module genuinely exposes no linear memory. `jz.memory(result)` still hands back
|
|
874
|
+
// a usable reader on demand.
|
|
875
|
+
return { exports: wrap(memSrc), memory: enhanced?.scalar ? null : enhanced, instance: inst, module: mod }
|
|
838
876
|
}
|
|
839
877
|
|
|
840
878
|
/**
|
|
@@ -849,23 +887,29 @@ const finishInstantiation = (mod, inst, imports, needsWasi, opts, state) => {
|
|
|
849
887
|
* @param {object} [opts] host options: imports, memory, _interp, host-shape flags
|
|
850
888
|
* @returns {{ exports, memory, instance, module }}
|
|
851
889
|
*/
|
|
890
|
+
/**
|
|
891
|
+
* Compile wasm bytes to a `WebAssembly.Module`, preferring native
|
|
892
|
+
* `wasm:js-string` builtins when the engine honors the option (V8 17+/Safari
|
|
893
|
+
* 18.4+; older engines throw or ignore it — try-fallback handles both). A
|
|
894
|
+
* `WebAssembly.Module` passed in is returned as-is. Factor it out so callers
|
|
895
|
+
* can compile once and instantiate many times without re-validating the bytes
|
|
896
|
+
* (`instantiate(toModule(wasm))` skips the per-call compile on hot loops).
|
|
897
|
+
*
|
|
898
|
+
* @param {Uint8Array|ArrayBuffer|WebAssembly.Module} wasm
|
|
899
|
+
* @returns {WebAssembly.Module}
|
|
900
|
+
*/
|
|
901
|
+
export const toModule = (wasm) => {
|
|
902
|
+
if (wasm instanceof WebAssembly.Module) return wasm
|
|
903
|
+
if (jssProbeNative()) {
|
|
904
|
+
try { return new WebAssembly.Module(wasm, { builtins: ['js-string'] }) }
|
|
905
|
+
catch { return new WebAssembly.Module(wasm) }
|
|
906
|
+
}
|
|
907
|
+
return new WebAssembly.Module(wasm)
|
|
908
|
+
}
|
|
909
|
+
|
|
852
910
|
export const instantiate = (wasm, opts = {}) => {
|
|
853
911
|
const state = prepareInterop(opts)
|
|
854
|
-
|
|
855
|
-
// The option is silently accepted by V8 17+/Safari 18.4+; older engines that
|
|
856
|
-
// don't recognize it either throw or ignore it — try-fallback handles both.
|
|
857
|
-
let mod
|
|
858
|
-
if (wasm instanceof WebAssembly.Module) {
|
|
859
|
-
mod = wasm
|
|
860
|
-
} else if (jssProbeNative()) {
|
|
861
|
-
try {
|
|
862
|
-
mod = new WebAssembly.Module(wasm, { builtins: ['js-string'] })
|
|
863
|
-
} catch {
|
|
864
|
-
mod = new WebAssembly.Module(wasm)
|
|
865
|
-
}
|
|
866
|
-
} else {
|
|
867
|
-
mod = new WebAssembly.Module(wasm)
|
|
868
|
-
}
|
|
912
|
+
const mod = toModule(wasm)
|
|
869
913
|
const { imports, needsWasi } = buildImports(mod, opts, state)
|
|
870
914
|
const hasImports = Object.keys(imports).some(k => k !== '_setMemory')
|
|
871
915
|
const inst = new WebAssembly.Instance(mod, hasImports ? imports : undefined)
|