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.
Files changed (63) hide show
  1. package/README.md +99 -72
  2. package/bench/README.md +253 -100
  3. package/bench/bench.svg +58 -81
  4. package/cli.js +85 -12
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6196 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +222 -35
  9. package/interop.js +240 -141
  10. package/layout.js +34 -18
  11. package/module/array.js +99 -111
  12. package/module/collection.js +124 -30
  13. package/module/console.js +1 -1
  14. package/module/core.js +163 -19
  15. package/module/json.js +3 -3
  16. package/module/math.js +162 -3
  17. package/module/number.js +268 -13
  18. package/module/object.js +37 -5
  19. package/module/regex.js +8 -7
  20. package/module/simd.js +37 -5
  21. package/module/string.js +203 -168
  22. package/module/typedarray.js +565 -112
  23. package/package.json +26 -7
  24. package/src/abi/string.js +29 -29
  25. package/src/ast.js +19 -2
  26. package/src/autoload.js +3 -0
  27. package/src/compile/analyze-scans.js +174 -11
  28. package/src/compile/analyze.js +101 -4
  29. package/src/compile/cse-load.js +200 -0
  30. package/src/compile/emit-assign.js +82 -20
  31. package/src/compile/emit.js +592 -51
  32. package/src/compile/index.js +504 -89
  33. package/src/compile/infer.js +36 -1
  34. package/src/compile/loop-divmod.js +109 -0
  35. package/src/compile/loop-model.js +91 -0
  36. package/src/compile/loop-square.js +102 -0
  37. package/src/compile/narrow.js +275 -41
  38. package/src/compile/peel-stencil.js +215 -0
  39. package/src/compile/plan/advise.js +55 -1
  40. package/src/compile/plan/common.js +29 -0
  41. package/src/compile/plan/index.js +8 -1
  42. package/src/compile/plan/inline.js +180 -22
  43. package/src/compile/plan/literals.js +313 -24
  44. package/src/compile/plan/scope.js +115 -39
  45. package/src/compile/program-facts.js +21 -2
  46. package/src/ctx.js +96 -19
  47. package/src/helper-counters.js +137 -0
  48. package/src/ir.js +157 -20
  49. package/src/kind-traits.js +34 -3
  50. package/src/kind.js +79 -4
  51. package/src/op-policy.js +5 -2
  52. package/src/ops.js +119 -0
  53. package/src/optimize/index.js +1274 -151
  54. package/src/optimize/recurse.js +182 -0
  55. package/src/optimize/vectorize.js +4187 -253
  56. package/src/prepare/index.js +75 -14
  57. package/src/prepare/lift-iife.js +149 -0
  58. package/src/reps.js +6 -2
  59. package/src/static.js +9 -0
  60. package/src/type.js +63 -51
  61. package/src/wat/assemble.js +286 -21
  62. package/src/widen.js +21 -0
  63. 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 "./src/wat/optimize.js";
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; use { profile: { names: true } } to emit a wasm name section')
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] - Shared memory import, or
172
- * initial page count. Prefer `memory: N` for owned memory.
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 {boolean|number|object} [opts.optimize] - Optimization level/config.
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
- * - `{ level?: 0|1|2|3, watr?: bool, hoistAddrBase?: bool, ... }`: per-pass
185
- * overrides on top of the chosen level. See PASS_NAMES in src/optimize.js.
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
- * Set `profile.names = true` to also emit a standard wasm `name` custom section
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
- if (opts.profile?.names) bytes = appendFunctionNames(bytes, optimized)
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 }