jz 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /package/src/{codegen.js → wat/codegen.js} +0 -0
package/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * source (string)
7
7
  * ↓ parse (subscript/jessie) — lexing + expression-oriented AST
8
8
  * raw AST: nested arrays `[op, ...args]`, no ctx mutation
9
- * ↓ jzify (opt-in via opts.jzify) — lower full-JS subset (var/function/class/switch) to jz-native
9
+ * ↓ jzify (default-on; skipped under opts.strict) — lower full-JS subset (var/function/class/switch) to jz-native
10
10
  * desugared AST: arrow functions + let/const/if only
11
11
  * ↓ prepare — validate (reject disallowed ops), normalize (++/--→+=/-=, scope rename),
12
12
  * extract (functions→ctx.func.list with sig), resolve (imports→ctx.module.imports),
@@ -14,12 +14,12 @@
14
14
  * prepared AST: normalized, with `ctx.func.list` / `ctx.module.imports` / `ctx.schema.list`
15
15
  * populated. Arrow bodies carry no type info yet.
16
16
  * ↓ compile — drives per-function emit, interleaves analysis (locals/valTypes/captures/
17
- * narrowing fixpoint) with IR generation via the emitter table (src/emit.js).
17
+ * narrowing fixpoint) with IR generation via the emitter table (src/compile/emit.js).
18
18
  * Writes: `ctx.func.valTypes`/`.locals`, `ctx.types.*`, `ctx.runtime.*`, `ctx.core.includes`.
19
- * Also calls optimizeFunc (src/optimize.js): `hoistPtrType` + fused peephole/inline/memarg walk.
19
+ * Also calls optimizeFunc (src/optimize/index.js): `hoistPtrType` + fused peephole/inline/memarg walk.
20
20
  * WAT IR: watr S-expression `['module', ...sections]`, every instruction node carries `.type`.
21
- * ↓ watrOptimize (opt-out via opts.optimize=false) — CSE, DCE, const folding at WAT level
22
- * ↓ optimizeFunc 2nd pass — re-folds rebox/unbox roundtrips that watrOptimize's inliner
21
+ * ↓ watOptimize (opt-out via opts.optimize=false) — CSE, DCE, const folding at WAT level
22
+ * ↓ optimizeFunc 2nd pass — re-folds rebox/unbox roundtrips that watOptimize's inliner
23
23
  * re-introduces at inline boundaries (caller's boxPtrIR meets callee's
24
24
  * i32.wrap_i64(i64.reinterpret_f64 __env)). watr's peephole doesn't cover this.
25
25
  * ↓ watrPrint (opts.wat=true) → WAT text, or watrCompile → Uint8Array binary
@@ -40,16 +40,24 @@
40
40
  * @module jz
41
41
  */
42
42
 
43
- import { parse } from 'subscript/feature/jessie'
44
- import { compile as watrCompile, print as watrPrint, optimize as watrOptimize } from "watr";
45
- import { ctx, reset, err } from './src/ctx.js'
46
- import prepare, { GLOBALS } from './src/prepare.js'
47
- import compile from './src/compile.js'
48
- import { emitter } from './src/emit.js'
49
- import { optimizeFunc, collectVolatileGlobals, resolveOptimize } from './src/optimize.js'
50
- import jzify from './src/jzify.js'
43
+ import { parse } from './src/parse.js'
44
+ import watrCompile from "watr/compile";
45
+ import watrPrint from "watr/print";
46
+ import watOptimize from "./src/wat/optimize.js";
47
+ import { appendLateStdlib } from './src/wat/assemble.js'
48
+ import { ctx, reset, err, initWarnings, assertCtxInvariants } from './src/ctx.js'
49
+ import prepare, { GLOBALS } from './src/prepare/index.js'
50
+ import { liftIIFEs } from './src/prepare/lift-iife.js'
51
+ import compile from './src/compile/index.js'
52
+ import { resetProgramFactsCache } from './src/compile/program-facts.js'
53
+ import { emit, emitter, emitVoid as flat, emitBlockBody as body, emitBoolStr as bool, emitIndex as idx, buildArrayWithSpreads as spread } from './src/compile/emit.js'
54
+ import { optimizeFunc, foldStrDispatchF64, collectVolatileGlobals, collectReachableGlobalWrites, hoistGlobalPtrOffset, stablePtrGlobalNames, resolveOptimize } from './src/optimize/index.js'
55
+ import { findBodyStart } from './src/ir.js'
56
+ import { VAL } from './src/reps.js'
57
+ import jzify from './jzify/index.js'
58
+ import { T } from './src/ast.js'
51
59
  import {
52
- memory as enhanceMemory, instantiate as instantiateRuntime,
60
+ memory as enhanceMemory, instantiate as instantiateRuntime, toModule,
53
61
  } from './interop.js'
54
62
 
55
63
  // A host import that's a JS function may hand back any value, including a host
@@ -58,11 +66,52 @@ const importsMayReturnExternal = (imports) =>
58
66
  !!imports && Object.values(imports).some(mod =>
59
67
  Object.values(mod || {}).some(spec => typeof spec === 'function'))
60
68
 
69
+ // WHATWG URL resolution for compile-time import.meta lowering. Injected into
70
+ // ctx.transform (like parse/jzify) so prepare never references the `URL` global
71
+ // directly — keeps the self-host kernel free of host-only built-ins.
72
+ const resolveUrl = (spec, base) => new URL(spec, base).href
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
+
61
110
  const nowMs = () => globalThis.performance?.now ? globalThis.performance.now() : Date.now()
62
111
 
63
112
  const compileProfiler = (profile) => {
64
113
  if (profile == null) return null
65
- 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')
66
115
  profile.entries ||= []
67
116
  profile.totals ||= {}
68
117
  return {
@@ -154,15 +203,24 @@ jz.memory = enhanceMemory
154
203
  * @param {string} code - jz source
155
204
  * @param {object} [opts]
156
205
  * @param {boolean} [opts.wat] - Return WAT text instead of binary
157
- * @param {boolean} [opts.strict] - Reject dynamic features (obj[k], for-in, unknown
158
- * receiver method calls) at compile time. Avoids pulling dynamic-dispatch stdlib
159
- * into output; large size win for static programs.
160
- * @param {WebAssembly.Memory|number} [opts.memory] - Shared memory import, or
161
- * initial page count. Prefer `memory: N` for owned memory.
206
+ * @param {boolean} [opts.strict] - Enforce the pure canonical subset: skip jzify
207
+ * (so full-JS syntax like var/function/class is rejected, not lowered) and reject
208
+ * dynamic features (obj[k], for-in, unknown receiver method calls) at compile time.
209
+ * Avoids pulling dynamic-dispatch stdlib into output; large size win for static programs.
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.
162
217
  * @param {boolean} [opts.alloc=true] - Export raw allocator helpers
163
218
  * (`_alloc`, `_clear`) for JS memory wrapping. Set false for standalone host-run
164
219
  * modules that only call exported wasm functions.
165
- * @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.
166
224
  * - `false` / `0`: nothing. Fastest compile, largest output (live coding).
167
225
  * - `1`: encoding-compactness only (treeshake + sortLocalsByUse + fusedRewrite-inline).
168
226
  * - `true` / `2` (default): every stable jz pass + full watr (inlineOnce +
@@ -170,15 +228,46 @@ jz.memory = enhanceMemory
170
228
  * - `3` / `'speed'`: level 2 + larger array/hash initial caps (`arrayMinCap`,
171
229
  * `hashSmallInitCap`) + `hoistConstantPool` off (inline `f64.const` over
172
230
  * mutable globals); trades size for speed.
173
- * - `{ level?: 0|1|2|3, watr?: bool, hoistAddrBase?: bool, ... }`: per-pass
174
- * 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.
249
+ * @param {object} [opts.warnings] - Optional mutable warning sink populated with
250
+ * `entries: [{ code, message, fn?, line?, column? }]`. Heap-growth advisories
251
+ * fire when a module uses the bump allocator and an export or loop retains
252
+ * allocations without a host-side memory.reset().
175
253
  * @param {object} [opts.profile] - Optional mutable profile sink populated with
176
254
  * `entries` and `totals` for parse / jzify / prepare / compile / plan / watr phases.
177
- * Set `profile.names = true` to also emit a standard wasm `name` custom section
178
- * for profiler/debugger symbolication.
179
- * @param {boolean} [opts.profileNames] - Legacy alias for `profile.names`.
255
+ * @param {boolean} [opts.names] - Emit a standard wasm `name` custom section (function
256
+ * symbols) for profiler/debugger symbolication. (Legacy: `profile.names = true`.)
257
+ * @param {Object<string,string>} [opts.modules] - Map of module specifier → source
258
+ * for compile-time `import`/`export` bundling: jz resolves the module graph
259
+ * in-process from this map instead of reading from disk.
260
+ * @param {boolean} [opts.noTailCall] - Disable proper-tail-call emission (self/mutual
261
+ * recursion uses ordinary call frames). For engines/tools without the tail-call proposal.
262
+ * @param {boolean} [opts.nativeTimers] - Emit a blocking `__timer_loop` in `_start` so
263
+ * setTimeout/setInterval fire under a standalone runtime (e.g. the wasmtime CLI) that
264
+ * has no host event loop. Default: timers defer to the JS host.
180
265
  * @param {string} [opts.importMetaUrl] - Module URL used to lower `import.meta.url`
181
266
  * and static `import.meta.resolve("...")` expressions.
267
+ * @param {number|boolean} [opts.randomSeed] - Seed for `Math.random`. Default: seeded
268
+ * once from host entropy on first use (crypto under `host:'js'`, `random_get` under
269
+ * WASI) — non-reproducible. Pass a number for a fixed, reproducible seed; `true` forces
270
+ * entropy explicitly. The randomness syscall is emitted only when `Math.random` is used.
182
271
  * @param {boolean} [opts.inspect] - When true, return `{ wasm, inspect }`
183
272
  * (or `{ wat, inspect }` with `opts.wat`) instead of the bare output.
184
273
  * `inspect` carries per-function inferred shapes (params, locals, JSON shapes,
@@ -186,7 +275,14 @@ jz.memory = enhanceMemory
186
275
  * without re-running the analyzer. Pays a small serialization cost; off by default.
187
276
  * @returns {Uint8Array|string|{wasm: Uint8Array, inspect: object}|{wat: string, inspect: object}}
188
277
  */
278
+ // Test-only compile target. When set (by test/index.js under JZ_TEST_TARGET=jz.wasm)
279
+ // every jz.compile / compile / jz() call routes through it instead of the in-process
280
+ // compiler — used to run the whole suite against dist/jz.wasm (the jz compiler
281
+ // compiled to wasm by jz). null in production: one boolean check on a cold path.
282
+ let compileTarget = null
283
+ export const _setCompileTarget = (fn) => { compileTarget = fn }
189
284
  jz.compile = (code, opts = {}) => {
285
+ if (compileTarget) return compileTarget(code, opts)
190
286
  try {
191
287
  return jzCompileInner(code, opts)
192
288
  } catch (e) {
@@ -194,7 +290,8 @@ jz.compile = (code, opts = {}) => {
194
290
  // codegen leak — surface it as an internal compile error with the source
195
291
  // location we were standing on. err()-thrown errors already pass through.
196
292
  if (e?.name === 'TypeError' || e?.name === 'ReferenceError' || e?.name === 'RangeError') {
197
- err(`internal: ${e.message} (jz hit an unsupported case while compiling${ctx.error.node ? '; the AST node above shows the trigger' : ''}). This is a jz bug — please report.`)
293
+ // Pass `e` as the cause so the original stack (the real codegen site) survives.
294
+ err(`internal: ${e.message} (jz hit an unsupported case while compiling${ctx.error.node ? '; the AST node above shows the trigger' : ''}). This is a jz bug — please report.`, e)
198
295
  }
199
296
  throw e
200
297
  }
@@ -271,10 +368,16 @@ const detectOptimizeConfig = (ast, code) => {
271
368
 
272
369
  const cfg = {}
273
370
  // Machine-generated or large code: watr's WAT-level CSE/DCE/inline fights
274
- // 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
275
378
  const isLarge = s.sourceChars > 4000 || s.funcCount > 40 || s.maxFuncBodySize > 300
276
379
  const isMachineLike = s.callSites > 300 && s.stringLiteralCount < 10
277
- if (isLarge || isMachineLike) cfg.watr = false
380
+ if ((isLarge || isMachineLike) && !usesSimd) { cfg.watr = false; cfg.splitCharScan = false }
278
381
  // Typed-array heavy: tighten scalarization thresholds when we see large
279
382
  // fixed-size arrays; keep defaults for small/dynamic ones.
280
383
  if (s.typedArrayCount > 0 && s.maxTypedArrayLen > 0) {
@@ -292,23 +395,64 @@ const detectOptimizeConfig = (ast, code) => {
292
395
  return cfg
293
396
  }
294
397
 
295
- const jzCompileInner = (code, opts = {}) => {
296
- const profiler = compileProfiler(opts.profile)
297
- const time = (name, fn) => profiler ? profiler.time(name, fn) : fn()
298
-
299
- reset(emitter, GLOBALS)
398
+ // Test-matrix bridge: when JZ_TEST_* env vars are set, inject them as default
399
+ // opts so the npm test suite can be re-run under varying configurations (opt
400
+ // levels, host, jzify, ) without source changes. User-supplied opts always win
401
+ // — env defaults fill only what the caller left unset. Resolved once at module
402
+ // load; no-op (single boolean check) when the env is empty (production path).
403
+ const TEST_ENV_DEFAULTS = (() => {
404
+ // Guard bare `process` — the compiler bundle must load in browsers/Workers too
405
+ // (this IIFE runs at module import; an unguarded ref throws ReferenceError there).
406
+ const e = (typeof process !== 'undefined' && process.env) || {}
407
+ const out = {}
408
+ if (e.JZ_TEST_OPTIMIZE != null) {
409
+ const v = e.JZ_TEST_OPTIMIZE
410
+ out.optimize = /^-?\d+$/.test(v) ? Number(v) : v === 'false' ? false : v
411
+ }
412
+ if (e.JZ_TEST_HOST) out.host = e.JZ_TEST_HOST
413
+ if (e.JZ_TEST_STRICT) out.strict = e.JZ_TEST_STRICT === '1'
414
+ return out
415
+ })()
416
+ const HAS_TEST_ENV = Object.keys(TEST_ENV_DEFAULTS).length > 0
417
+
418
+ // Shared front-half: reset ctx, wire opts → ctx.transform/memory/module/features,
419
+ // and inject parse/resolveUrl. Called by `jzCompileInner` (the only entry point
420
+ // today). The self-host entry (scripts/self.js) drives reset itself rather than
421
+ // going through this path, since it needs only a minimal, interop-free setup.
422
+ const setupCtx = (code, opts) => {
423
+ if (HAS_TEST_ENV) {
424
+ const merged = { ...opts }
425
+ for (const k of Object.keys(TEST_ENV_DEFAULTS)) if (merged[k] == null) merged[k] = TEST_ENV_DEFAULTS[k]
426
+ opts = merged
427
+ }
428
+ reset(emitter, GLOBALS, { emit, flat, body, bool, idx, spread })
429
+ resetProgramFactsCache()
300
430
  ctx.error.src = code
301
-
431
+ initWarnings(opts.warnings)
302
432
  if (typeof opts.memory === 'number') ctx.memory.pages = opts.memory
303
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
+ }
304
443
  if (opts.modules) ctx.module.importSources = opts.modules
305
444
  if (opts.imports) {
306
445
  ctx.module.hostImports = opts.imports
307
446
  if (importsMayReturnExternal(opts.imports)) ctx.features.external = true
308
447
  }
309
- // jzify: true accept full JS subset (function/var/switch lowered to arrows/let/if).
310
- // Default: strict jz (prepare rejects disallowed JS features). subscript handles ASI natively.
311
- if (opts.jzify) ctx.transform.jzify = jzify
448
+ // Parser for compile-time import bundling (prepareModule). Injected, not
449
+ // imported by prepare see ctx.transform.parse note in prepare/index.js.
450
+ ctx.transform.parse = parse
451
+ ctx.transform.resolveUrl = resolveUrl
452
+ // jzify runs by default — accept the full JS subset (function/var/switch lowered to
453
+ // arrows/let/if). `strict: true` skips it, so prepare rejects disallowed JS features
454
+ // and the pure canonical subset is enforced. subscript handles ASI natively.
455
+ if (!opts.strict) ctx.transform.jzify = jzify
312
456
  if (opts.noTailCall) ctx.transform.noTailCall = true
313
457
  if (opts.strict) ctx.transform.strict = true
314
458
  if (opts.host) {
@@ -319,38 +463,106 @@ const jzCompileInner = (code, opts = {}) => {
319
463
  if (opts.alloc === false) ctx.transform.alloc = false
320
464
  if (opts.inspect) ctx.transform.inspect = true
321
465
  if (opts.importMetaUrl) ctx.transform.importMetaUrl = String(opts.importMetaUrl)
466
+ if (opts.randomSeed !== undefined) {
467
+ if (opts.randomSeed !== true && !Number.isFinite(opts.randomSeed))
468
+ err(`opts.randomSeed must be a finite number (fixed seed — reproducible) or true (seed Math.random from host entropy on first use); got ${typeof opts.randomSeed}`)
469
+ ctx.transform.randomSeed = opts.randomSeed
470
+ }
322
471
  if (opts.nativeTimers) ctx.features.blockingTimers = true // wasmtime CLI: include __timer_loop in _start
323
472
  ctx.transform.optimize = resolveOptimize(opts.optimize)
324
-
325
473
  if (opts._interp) {
326
474
  for (const [name, fn] of Object.entries(opts._interp)) {
327
- if (name.startsWith('__ext_')) continue;
475
+ if (name.startsWith('__ext_')) continue
328
476
  if (ctx.transform.host === 'wasi') throw new Error(`host:'wasi' does not support _interp['${name}']: env imports are unavailable in WASI. Implement it natively.`)
329
477
  ctx.features.external = true
330
478
  const params = Array(fn.length).fill(['param', 'f64'])
331
479
  ctx.module.imports.push(['import', '"env"', `"${name}"`, ['func', `$${name}`, ...params, ['result', 'f64']]])
332
480
  }
333
481
  }
482
+ }
483
+
484
+ // U+E000 (T) prefixes every jz-generated local. The JS spec forbids it in
485
+ // identifiers, but subscript's parser is lenient and accepts it — so a user name
486
+ // carrying it could silently alias a compiler temp. Reject it in identifier
487
+ // position on the RAW parse (before jzify, which legitimately mints T-prefixed
488
+ // temps of its own). String-literal nodes are `[null, …]` and skipped, so
489
+ // `"……"` data is fine; only walked when the char is present in source.
490
+ const rejectReservedPrefix = (node) => {
491
+ if (!Array.isArray(node)) return
492
+ if (node.length === 2 && node[0] == null) return // [null, X] — value literal, not an identifier
493
+ for (let i = 1; i < node.length; i++) {
494
+ const v = node[i]
495
+ if (typeof v === 'string') {
496
+ if (v.includes(T)) err(`identifier '${v.split(T).join('\\uE000')}' contains the reserved compiler prefix (U+E000) — jz uses it for generated locals; rename it`)
497
+ } else rejectReservedPrefix(v)
498
+ }
499
+ }
500
+
501
+ const jzCompileInner = (code, opts = {}) => {
502
+ if (opts.define) code = defineBindings(opts.define) + code
503
+ const profiler = compileProfiler(opts.profile)
504
+ const time = (name, fn) => profiler ? profiler.time(name, fn) : fn()
505
+
506
+ setupCtx(code, opts)
507
+ assertCtxInvariants('post-reset')
334
508
 
335
509
  let parsed = time('parse', () => parse(code))
336
- if (opts.jzify) parsed = time('jzify', () => jzify(parsed))
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))
517
+ if (!opts.strict) parsed = time('jzify', () => jzify(parsed))
337
518
  const ast = time('prepare', () => prepare(parsed))
519
+ assertCtxInvariants('post-prepare')
338
520
 
339
521
  // Auto-detect optimization tuning from source characteristics when the user
340
- // hasn't provided any optimize option. At the default level its only *live*
341
- // output is the typed-array scalarization thresholds (every other override —
342
- // watr off, sortStrPool/hoistPtr on already matches the level-2 preset), so
343
- // skip the AST scan entirely unless the program touches typed arrays.
344
- // NOTE: if the level-2 default ever flips watr on, drop this guard so the
345
- // machine-generated-code heuristic in detectOptimizeConfig can take effect.
346
- if (opts.optimize == null && ctx.module.modules.typedarray) {
522
+ // hasn't provided any optimize option. detectOptimizeConfig has two *live*
523
+ // overrides over the level-2 preset: the typed-array scalarization thresholds,
524
+ // and `watr: false` for large/machine-generated code (whose WAT-level CSE/DCE
525
+ // fights jz's already-optimized IR and inflates output). watr is ON at level 2,
526
+ // so that switch is a real override run the scan when the program either
527
+ // touches typed arrays or is large enough for the machine-code heuristic to
528
+ // bite. `code.length` is the one signal free without scanning; gating on it
529
+ // keeps small programs (the common case) on the no-scan fast path.
530
+ if (opts.optimize == null && (ctx.module.modules.typedarray || code.length > 4000)) {
347
531
  const autoCfg = detectOptimizeConfig(ast, code)
348
532
  if (Object.keys(autoCfg).length) {
349
533
  ctx.transform.optimize = resolveOptimize(autoCfg)
350
534
  }
351
535
  }
352
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
+
353
564
  const module = time('compile', () => compile(ast, profiler))
565
+ assertCtxInvariants('post-compile')
354
566
 
355
567
  // host: 'wasi' — error if the wasm would import any env.__ext_* helper. Those exist
356
568
  // only to defer to a JS host's value-aware semantics; in a wasmtime/wasmer/deno
@@ -368,25 +580,87 @@ const jzCompileInner = (code, opts = {}) => {
368
580
  }
369
581
 
370
582
  const cfg = ctx.transform.optimize
371
- // watr's `loopify` collapses the `(block $brk (loop (br_if $brk !cond) … (br $loop)))`
372
- // idiom into `(loop … (if cond …))` — sound, but destroys the exact shape jz's
373
- // post-watr vectorizer scans for. Disable loopify when vectorize is going to run;
374
- // otherwise hand watr its full default pass set (inlineOnce + coalesce on, inline off).
375
- const watrOpts = cfg.vectorizeLaneLocal ? { loopify: false } : true
376
- const optimized = cfg.watr ? time('watrOptimize', () => watrOptimize(module, watrOpts)) : module
377
- // Final peephole pass: watrOptimize's inliner can re-introduce rebox/unbox at boundaries
583
+ let watrOpts = typeof cfg.watr === 'object' ? { ...cfg.watr } : true
584
+ if (cfg.vectorizeLaneLocal) {
585
+ if (watrOpts === true) watrOpts = { loopify: false }
586
+ else if (typeof watrOpts === 'object' && watrOpts.loopify === undefined) watrOpts.loopify = false
587
+ }
588
+ if (cfg.devirtIndirect) {
589
+ if (watrOpts === true) watrOpts = { devirt: true }
590
+ else if (typeof watrOpts === 'object' && watrOpts.devirt === undefined) watrOpts.devirt = true
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
+ }
600
+ const optimized = cfg.watr ? time('watOptimize', () => watOptimize(module, watrOpts)) : module
601
+ // Stable-pointee module globals: resolve the __ptr_offset once per function.
602
+ // Never-forwarding kinds — every PTR tag outside __ptr_offset's forwarding
603
+ // set {ARRAY, HASH, SET, MAP} — give the same offset for the same bits, so
604
+ // the snapshot only needs the global's VALUE stable through the function:
605
+ // the reachable-writes call graph proves that precisely. Independent of watr
606
+ // (the auto-config turns watr off for large sources — exactly the
607
+ // module-global DSP-state programs this pass exists for: rfft, diffusion);
608
+ // when watr DID run, it goes before the post leaf passes so the snapped
609
+ // local participates in hoistAddrBase/cseScalarLoad.
610
+ if (cfg.hoistGlobalPtrOffset !== false) {
611
+ const stableGlobals = stablePtrGlobalNames()
612
+ if (stableGlobals.size) {
613
+ const funcs = optimized.filter(node => Array.isArray(node) && node[0] === 'func')
614
+ const reach = collectReachableGlobalWrites(funcs)
615
+ for (const node of funcs) hoistGlobalPtrOffset(node, stableGlobals, reach)
616
+ }
617
+ }
618
+ // Final peephole pass: watOptimize's inliner can re-introduce rebox/unbox at boundaries
378
619
  // (e.g. inlined closure body's `i32.wrap_i64 (i64.reinterpret_f64 __env)` next to caller's
379
620
  // `boxPtrIR(g)` rebox). Our fusedRewrite folds these, watr's peephole doesn't.
380
621
  // Only valuable to re-run when watr ran (watr is what re-introduces the boundaries).
381
622
  if (cfg.watr) {
382
- const postCfg = { ...cfg, __phase: 'post' }
383
623
  // Build global name→type map from ctx.scope.globalTypes for promoteGlobals
384
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
+ }
385
655
  time('watrReopt', () => {
386
656
  const funcs = optimized.filter(node => Array.isArray(node) && node[0] === 'func')
387
657
  const volatileGlobals = collectVolatileGlobals(funcs)
388
- for (const node of funcs) optimizeFunc(node, postCfg, globalTypesMap, volatileGlobals)
658
+ const reach = collectReachableGlobalWrites(funcs)
659
+ for (const node of funcs) optimizeFunc(node, cfg, globalTypesMap, volatileGlobals, 'post', reach)
389
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)
390
664
  }
391
665
  try {
392
666
  if (opts.wat) {
@@ -395,7 +669,9 @@ const jzCompileInner = (code, opts = {}) => {
395
669
  }
396
670
  const wasm = time('watrCompile', () => watrCompile(optimized))
397
671
  let bytes = wasm
398
- if (opts.profileNames || opts.profile?.names) bytes = appendFunctionNames(bytes, optimized)
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)
399
675
  return opts.inspect ? { wasm: bytes, inspect: ctx.inspect } : bytes
400
676
  } catch (e) {
401
677
  // watr surfaces dangling identifiers as "Unknown local|func|global|table|memory $X".
@@ -425,25 +701,6 @@ export default function jz(code, ...args) {
425
701
  if (Array.isArray(code)) {
426
702
  const interp = {}, data = {}, hoisted = []
427
703
 
428
- // Serialize JS value to jz source literal. Returns null if not serializable.
429
- const serialize = (v) => {
430
- if (typeof v === 'number' || typeof v === 'boolean') return String(v)
431
- if (v === null) return 'null'
432
- if (typeof v === 'string') return JSON.stringify(v)
433
- if (Array.isArray(v)) {
434
- const elems = v.map(serialize)
435
- return elems.every(e => e !== null) ? `[${elems.join(', ')}]` : null
436
- }
437
- if (typeof v === 'object') {
438
- const props = Object.keys(v).map(k => {
439
- const s = serialize(v[k])
440
- return s !== null ? `${k}: ${s}` : null
441
- })
442
- return props.every(p => p !== null) ? `{${props.join(', ')}}` : null
443
- }
444
- return null
445
- }
446
-
447
704
  let src = code[0]
448
705
  for (let i = 0; i < args.length; i++) {
449
706
  const v = args[i]
@@ -484,15 +741,36 @@ export default function jz(code, ...args) {
484
741
  // String call: jz('code', opts?) — compile + instantiate + wrap
485
742
  const callOpts = args[0] || {}
486
743
  const out = jz.compile(code, callOpts)
487
- // inspect:true returns { wasm, inspect } from jz.compile unwrap the bytes for
488
- // instantiation and surface inspect on the runtime result alongside exports/memory.
489
- if (callOpts.inspect && out && typeof out === 'object' && 'wasm' in out) {
490
- const result = instantiateRuntime(out.wasm, callOpts)
491
- return Object.assign(result, { inspect: out.inspect })
492
- }
493
- return instantiateRuntime(out, callOpts)
744
+ const wasm = out && typeof out === 'object' && 'wasm' in out ? out.wasm : out
745
+ const result = instantiateRuntime(wasm, callOpts)
746
+ const extra = {}
747
+ if (callOpts.inspect && out && typeof out === 'object' && 'inspect' in out) extra.inspect = out.inspect
748
+ if (callOpts.warnings) extra.warnings = callOpts.warnings.entries
749
+ return Object.keys(extra).length ? Object.assign(result, extra) : result
494
750
  }
495
751
 
496
752
  export { jz }
497
753
  const jzCompile = jz.compile
498
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 }