jz 0.8.0 → 0.9.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 (73) hide show
  1. package/README.md +56 -9
  2. package/bench/README.md +121 -50
  3. package/bench/bench.svg +27 -27
  4. package/cli.js +3 -1
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +7541 -6178
  7. package/index.js +165 -74
  8. package/interop.js +189 -17
  9. package/jzify/arguments.js +32 -1
  10. package/jzify/async.js +409 -0
  11. package/jzify/classes.js +136 -18
  12. package/jzify/generators.js +639 -0
  13. package/jzify/hoist-vars.js +73 -1
  14. package/jzify/index.js +123 -4
  15. package/jzify/names.js +1 -0
  16. package/jzify/transform.js +239 -1
  17. package/layout.js +48 -3
  18. package/module/array.js +318 -43
  19. package/module/atomics.js +144 -0
  20. package/module/collection.js +1429 -155
  21. package/module/core.js +431 -40
  22. package/module/date.js +142 -120
  23. package/module/fs.js +144 -0
  24. package/module/function.js +6 -3
  25. package/module/index.js +4 -1
  26. package/module/json.js +270 -49
  27. package/module/math.js +1032 -145
  28. package/module/number.js +532 -163
  29. package/module/object.js +353 -95
  30. package/module/regex.js +157 -7
  31. package/module/schema.js +100 -2
  32. package/module/string.js +428 -93
  33. package/module/typedarray.js +264 -72
  34. package/module/web.js +36 -0
  35. package/package.json +5 -5
  36. package/src/abi/string.js +82 -11
  37. package/src/ast.js +11 -5
  38. package/src/autoload.js +28 -1
  39. package/src/bridge.js +7 -2
  40. package/src/compile/analyze-scans.js +22 -7
  41. package/src/compile/analyze.js +136 -30
  42. package/src/compile/dyn-closure-tables.js +277 -0
  43. package/src/compile/emit-assign.js +281 -15
  44. package/src/compile/emit.js +1055 -70
  45. package/src/compile/index.js +277 -29
  46. package/src/compile/infer.js +42 -4
  47. package/src/compile/inplace-store.js +329 -0
  48. package/src/compile/loop-recurrence.js +167 -0
  49. package/src/compile/narrow.js +555 -18
  50. package/src/compile/peel-stencil.js +5 -0
  51. package/src/compile/plan/index.js +45 -3
  52. package/src/compile/plan/inline.js +8 -1
  53. package/src/compile/plan/literals.js +39 -0
  54. package/src/compile/plan/scope.js +430 -23
  55. package/src/compile/program-facts.js +732 -38
  56. package/src/ctx.js +64 -9
  57. package/src/helper-counters.js +7 -1
  58. package/src/ir.js +113 -5
  59. package/src/kind-traits.js +66 -3
  60. package/src/kind.js +84 -12
  61. package/src/op-policy.js +7 -4
  62. package/src/optimize/index.js +1179 -704
  63. package/src/optimize/recurse.js +2 -2
  64. package/src/optimize/vectorize.js +982 -67
  65. package/src/prepare/index.js +809 -67
  66. package/src/prepare/math-kernel.js +331 -0
  67. package/src/prepare/pre-eval.js +714 -0
  68. package/src/snapshot.js +194 -0
  69. package/src/type.js +1170 -56
  70. package/src/wat/assemble.js +403 -65
  71. package/src/wat/codegen.js +74 -14
  72. package/transform.js +113 -4
  73. package/wasi.js +3 -0
package/index.js CHANGED
@@ -16,12 +16,13 @@
16
16
  * ↓ compile — drives per-function emit, interleaves analysis (locals/valTypes/captures/
17
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/index.js): `hoistPtrType` + fused peephole/inline/memarg walk.
19
+ * The emit phase (src/wat/assemble.js optimizeModule) then runs jz's ONLY optimizer pass —
20
+ * optimizeFunc (src/optimize/index.js): `hoistPtrType` + fused peephole/inline/memarg walk +
21
+ * auto-vectorization. All lowering, incl. SIMD, happens here — BEFORE watr.
20
22
  * WAT IR: watr S-expression `['module', ...sections]`, every instruction node carries `.type`.
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
- * re-introduces at inline boundaries (caller's boxPtrIR meets callee's
24
- * i32.wrap_i64(i64.reinterpret_f64 __env)). watr's peephole doesn't cover this.
23
+ * ↓ watOptimize (opt-out via opts.optimize=false) — the SOLE, FINAL optimizer: CSE, DCE, const
24
+ * fold, inline, coalesce. Runs ONCE, as a fixpoint. No jz pass touches WAT after it (bar the
25
+ * stable-global-offset hoist, a phase-2 watr-migration candidate). See .work/research.md.
25
26
  * ↓ watrPrint (opts.wat=true) → WAT text, or watrCompile → Uint8Array binary
26
27
  *
27
28
  * # State
@@ -42,17 +43,17 @@
42
43
 
43
44
  import { parse } from './src/parse.js'
44
45
  import watrCompile from "watr/compile";
46
+ import { snapshotInit } from "./src/snapshot.js";
45
47
  import watrPrint from "watr/print";
46
48
  import watOptimize from "watr/optimize";
47
- import { appendLateStdlib } from './src/wat/assemble.js'
48
49
  import { ctx, reset, err, initWarnings, assertCtxInvariants } from './src/ctx.js'
49
50
  import prepare, { GLOBALS } from './src/prepare/index.js'
50
51
  import { liftIIFEs } from './src/prepare/lift-iife.js'
52
+ import { preEval } from './src/prepare/pre-eval.js'
51
53
  import compile from './src/compile/index.js'
52
54
  import { resetProgramFactsCache } from './src/compile/program-facts.js'
53
55
  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, SIMD_PINNED } from './src/optimize/index.js'
55
- import { findBodyStart } from './src/ir.js'
56
+ import { collectReachableGlobalWrites, hoistGlobalPtrOffset, stablePtrGlobalNames, resolveOptimize, SIMD_PINNED } from './src/optimize/index.js'
56
57
  import { VAL } from './src/reps.js'
57
58
  import jzify from './jzify/index.js'
58
59
  import { T } from './src/ast.js'
@@ -198,6 +199,80 @@ const appendFunctionNames = (wasm, module) => {
198
199
  */
199
200
  jz.memory = enhanceMemory
200
201
 
202
+ /**
203
+ * jz.pool(source, opts) — SPMD worker pool over ONE shared memory (Workers v1).
204
+ *
205
+ * Compiles `source` with { sharedMemory: true }, instantiates it on the main
206
+ * thread AND in `threads` node worker_threads, all linked to the same
207
+ * WebAssembly.Memory({ shared: true }). Kernels coordinate through shared
208
+ * typed arrays + Atomics (module/atomics.js); strings/objects stay
209
+ * thread-local by the v1 contract.
210
+ *
211
+ * Worker instances link `env.memory` ONLY — a kernel needing other host
212
+ * imports fails instantiation loudly (pure-compute contract).
213
+ *
214
+ * const p = await jz.pool(src, { threads: 4, pages: 16, maxPages: 256 })
215
+ * p.exports.setup(...) // main-thread instance
216
+ * await p.run('tile') // every worker: tile(workerIndex, threads)
217
+ * await p.run('tile', a, b) // extra args broadcast to each worker
218
+ * p.memory // the shared, jz-enhanced memory
219
+ * await p.terminate()
220
+ *
221
+ * @param {string} source - jz source (compiled once; module shared with workers)
222
+ * @param {object} [opts] - { threads=4, pages=16, maxPages=16384, ...compile opts }
223
+ * @returns {Promise<{exports, memory, module, threads, run, terminate}>}
224
+ */
225
+ jz.pool = async function pool(source, opts = {}) {
226
+ const { threads = 4, pages = 16, maxPages = 16384, ...rest } = opts
227
+ const memory = new WebAssembly.Memory({ initial: pages, maximum: maxPages, shared: true })
228
+ const main = jz(source, { ...rest, sharedMemory: true, memory })
229
+ // Computed specifier keeps bundlers (esbuild web dist) from resolving the
230
+ // node builtin statically; browsers get a clear v1 error instead.
231
+ const { Worker } = await import('node:' + 'worker_threads').catch(() => {
232
+ throw new Error('jz.pool v1 runs on node worker_threads; browser Worker support is a follow-up')
233
+ })
234
+ // The worker shim honors the jz:i64exp lane map (exact-bits ABI): an i64 lane
235
+ // takes a BigInt — scalars convert to their f64 bits, boxed pointers (BigInt
236
+ // args from jz.memory.* on the main thread) pass through; i64 results
237
+ // reinterpret back to numbers (v1 kernels return scalars).
238
+ const workerSrc = `
239
+ const { parentPort, workerData } = require('node:worker_threads')
240
+ const inst = new WebAssembly.Instance(workerData.module, { env: { memory: workerData.memory } })
241
+ const dv = new DataView(new ArrayBuffer(8))
242
+ const bits = (x) => (dv.setFloat64(0, x), dv.getBigUint64(0))
243
+ const unbits = (b) => (dv.setBigUint64(0, BigInt.asUintN(64, b)), dv.getFloat64(0))
244
+ const lanes = new Map()
245
+ for (const s of WebAssembly.Module.customSections(workerData.module, 'jz:i64exp'))
246
+ try { for (const e of JSON.parse(new TextDecoder().decode(s))) lanes.set(e.name, e) } catch {}
247
+ parentPort.on('message', (m) => {
248
+ try {
249
+ const sig = lanes.get(m.fn), p = new Set(sig?.p || [])
250
+ const args = m.args.map((x, i) => p.has(i) && typeof x === 'number' ? bits(x) : x)
251
+ let r = inst.exports[m.fn](...args)
252
+ if (sig?.r && typeof r === 'bigint') r = unbits(r)
253
+ parentPort.postMessage({ id: m.id, r })
254
+ } catch (e) { parentPort.postMessage({ id: m.id, e: String(e) }) }
255
+ })
256
+ parentPort.postMessage({ ready: true })`
257
+ const workers = Array.from({ length: threads }, () =>
258
+ new Worker(workerSrc, { eval: true, workerData: { module: main.module, memory } }))
259
+ await Promise.all(workers.map(w => new Promise((res, rej) => {
260
+ w.once('message', res); w.once('error', rej)
261
+ })))
262
+ let seq = 0
263
+ const call = (w, fn, args) => new Promise((res, rej) => {
264
+ const id = seq++
265
+ const on = (m) => { if (m.id !== id) return; w.off('message', on); m.e ? rej(new Error(m.e)) : res(m.r) }
266
+ w.on('message', on)
267
+ w.postMessage({ id, fn, args })
268
+ })
269
+ return {
270
+ exports: main.exports, memory: main.memory, module: main.module, threads,
271
+ run: (fn, ...args) => Promise.all(workers.map((w, i) => call(w, fn, [i, threads, ...args]))),
272
+ terminate: () => Promise.all(workers.map(w => w.terminate())),
273
+ }
274
+ }
275
+
201
276
  /**
202
277
  * Compile jz source to WASM binary or WAT text. Low-level — no instantiation.
203
278
  * @param {string} code - jz source
@@ -212,6 +287,9 @@ jz.memory = enhanceMemory
212
287
  * @param {number} [opts.maxMemory] - Maximum memory pages — emits a ceiling on the
213
288
  * memory type so growth traps past it (sandbox cap). Must be ≥ the initial size.
214
289
  * Default: unbounded.
290
+ * @param {boolean} [opts.sharedMemory] - Import `env.memory` as a SHARED memory (wasm
291
+ * threads): atomic heap bump, `shared` memtype (max defaults to the 4 GiB ceiling).
292
+ * Link with `new WebAssembly.Memory({ initial, maximum, shared: true })`.
215
293
  * @param {boolean} [opts.importMemory] - Import `env.memory` instead of exporting an
216
294
  * owned memory. For embedding into a host that provides the memory.
217
295
  * @param {boolean} [opts.alloc=true] - Export raw allocator helpers
@@ -385,8 +463,6 @@ const detectOptimizeConfig = (ast, code) => {
385
463
  cfg.scalarTypedLoopUnroll = s.maxLoopDepth > 1 ? 8 : 16
386
464
  cfg.scalarTypedNestedUnroll = s.maxLoopDepth > 1 ? 32 : 128
387
465
  }
388
- // String-heavy: ensure pool sorting is on (already default, but explicit).
389
- if (s.stringLiteralCount > 30) cfg.sortStrPoolByFreq = true
390
466
  // Closure-heavy: ptr hoists pay off.
391
467
  if (s.closureCount > 4) {
392
468
  cfg.hoistPtrType = true
@@ -432,6 +508,11 @@ const setupCtx = (code, opts) => {
432
508
  if (typeof opts.memory === 'number') ctx.memory.pages = opts.memory
433
509
  else if (opts.memory) ctx.memory.shared = true
434
510
  if (opts.importMemory) ctx.memory.shared = true // import env.memory instead of exporting own
511
+ // True cross-thread sharing (Workers v1): import env.memory declared with the
512
+ // wasm `shared` memtype and switch the heap bump to atomic RMW. Distinct from
513
+ // importMemory — a plain imported (non-shared) Memory must NOT declare shared
514
+ // or linking fails in the other direction.
515
+ if (opts.sharedMemory) { ctx.memory.shared = true; ctx.memory.atomic = true }
435
516
  if (opts.maxMemory != null) {
436
517
  if (!Number.isInteger(opts.maxMemory) || opts.maxMemory < 1)
437
518
  err(`opts.maxMemory must be a positive integer page count (each page is 64 KiB); got ${opts.maxMemory}`)
@@ -517,8 +598,12 @@ const jzCompileInner = (code, opts = {}) => {
517
598
  // path. A no-op when there are none.
518
599
  parsed = time('liftIIFE', () => liftIIFEs(parsed))
519
600
  if (!opts.strict) parsed = time('jzify', () => jzify(parsed))
520
- const ast = time('prepare', () => prepare(parsed))
601
+ let ast = time('prepare', () => prepare(parsed))
521
602
  assertCtxInvariants('post-prepare')
603
+ // preEval: fold every statically-evaluable construct (numeric/string/bool chains,
604
+ // pure Math.* calls, zero-arg pure calls incl. lift-iife's IIFEs) down to literals,
605
+ // over the prepared AST + every ctx.func.list body, before compile ever sees them.
606
+ ast = time('preEval', () => preEval(ast))
522
607
 
523
608
  // Auto-detect optimization tuning from source characteristics when the user
524
609
  // hasn't provided any optimize option. detectOptimizeConfig has two *live*
@@ -609,35 +694,65 @@ const jzCompileInner = (code, opts = {}) => {
609
694
  if (watrOpts === true) watrOpts = { inline: 'simd' }
610
695
  else if (typeof watrOpts === 'object' && watrOpts.inline === undefined) watrOpts.inline = 'simd'
611
696
  }
697
+ // Wrapper elision (speed tier): dissolve adapter frames — closure-ABI
698
+ // trampolines for functions-as-values, thin dispatch heads like
699
+ // __dyn_get_expr — into their target at the wrapper site. The recorded
700
+ // "table entries native against ftN" restructure, done mechanically in the
701
+ // optimizer instead of per-case emit surgery (one wrapper = one duplicated
702
+ // target; treeshake collects whichever copy ends up unreferenced).
703
+ if (cfg.inlineFns) {
704
+ if (typeof watrOpts === 'object' && watrOpts.inlineWrappers === undefined) watrOpts.inlineWrappers = true
705
+ }
706
+ // watr LICM (speed tier): hoist loop-invariant pure arithmetic AFTER watr's inlining exposes it
707
+ // (the raytrace per-iteration NaN/Inf guard pairs). Mechanical — lives in watr, jz just enables it.
708
+ if (cfg.watrLicm) {
709
+ if (watrOpts === true) watrOpts = { licm: true }
710
+ else if (typeof watrOpts === 'object' && watrOpts.licm === undefined) watrOpts.licm = true
711
+ }
612
712
  // guardRefine folds jz's NaN-box tag reads — default-off in watr (general WAT
613
713
  // never matches it), so jz always enables it explicitly.
614
714
  if (watrOpts === true) watrOpts = { guardRefine: true }
615
715
  else if (typeof watrOpts === 'object' && watrOpts.guardRefine === undefined) watrOpts.guardRefine = true
716
+ // watr's ifset (one-armed conditional update → select) rides jz's select
717
+ // tiering: an unconditional-update trade belongs to the speed tier exactly
718
+ // like boolConvertToSelect. jz's DEFAULT tier also passes watrProfile:'speed'
719
+ // (for the outline/tailmerge shape), which would enable ifset via the
720
+ // profile — the explicit flag here overrides it in both directions.
721
+ if (typeof watrOpts === 'object' && watrOpts.ifset === undefined)
722
+ watrOpts.ifset = cfg.boolConvertToSelect === true || cfg.watrIfset === true
723
+ // jz's promise is runtime speed, but watr's OWN profile default leans size — outline/
724
+ // tailmerge/rettail fold repeated sequences into out-of-line calls (measured 1.433→1.316
725
+ // on the self-host kernel with them off, watr ≥5.2.0). Every speed-tier preset carries
726
+ // watrProfile:'speed' (src/optimize/index.js LEVEL_PRESETS); the 'size' preset keeps
727
+ // watr's size-leaning default. An explicit user profile always wins.
728
+ if (watrOpts.profile === undefined && cfg.watrProfile) watrOpts.profile = cfg.watrProfile
729
+ // Speed tier waives watr's size-revert guard (two full binary encodes per
730
+ // optimize — its costliest fixed overhead): inlining growth is the shape this
731
+ // tier asks for, and jz's own perf/size gates own the size budget. Level 2
732
+ // (default) and 'size' keep the guard — watr's never-inflate contract stands
733
+ // where the user didn't opt into speed-for-size.
734
+ if (watrOpts.guard === undefined && cfg.watrGuard === false) watrOpts.guard = false
616
735
  // Pin jz's scalar transcendentals (the PPC_CALL2 keys the auto-vectorizer rewrites to f64x2
617
736
  // mirrors) so watr's inline passes don't dissolve the call nodes the lift needs. The protection
618
737
  // policy lives here in jz — watr just honours the `pin` list (no jz names hardcoded in watr).
619
738
  if (cfg.watr) {
620
739
  if (watrOpts === true) watrOpts = {}
621
740
  watrOpts.pin = watrOpts.pin ? [...watrOpts.pin, ...SIMD_PINNED] : SIMD_PINNED
741
+ // Boundary-wrapped functions (`$name`/`$name$exp`) the vectorizer just lifted: keep
742
+ // watr's default inlineOnce from merging $name into $name$exp, which would otherwise
743
+ // rename every $__ppc*/$__esc*/... local & label the lift produced with its own
744
+ // $__inl{N}_ prefix, burying the marker's leading `$` (cosmetic-only cost: one
745
+ // un-eliminated wrapper call per top-level invocation, not per loop iteration).
746
+ if (cfg._vectorizedFnNames?.size) watrOpts.pin = [...watrOpts.pin, ...cfg._vectorizedFnNames]
622
747
  }
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
630
748
  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])
632
- // Stable-pointee module globals: resolve the __ptr_offset once per function.
633
- // Never-forwarding kinds every PTR tag outside __ptr_offset's forwarding
634
- // set {ARRAY, HASH, SET, MAP} give the same offset for the same bits, so
635
- // the snapshot only needs the global's VALUE stable through the function:
636
- // the reachable-writes call graph proves that precisely. Independent of watr
637
- // (the auto-config turns watr off for large sources exactly the
638
- // module-global DSP-state programs this pass exists for: rfft, diffusion);
639
- // when watr DID run, it goes before the post leaf passes so the snapped
640
- // local participates in hoistAddrBase/cseScalarLoad.
749
+ // Stable-pointee module globals: resolve the __ptr_offset once per function. Never-forwarding
750
+ // kinds every PTR tag outside __ptr_offset's forwarding set {ARRAY, HASH, SET, MAP} — give the
751
+ // same offset for the same bits, so the snapshot only needs the global's VALUE stable through the
752
+ // function: the reachable-writes call graph proves that precisely. This is the ONE jz pass still
753
+ // touching WAT after watr; it exists because watr has no stable-global-offset hoist of its own
754
+ // (the module-global DSP programs it serves rfft, diffusion — run with watr auto-OFF anyway).
755
+ // TODO(phase-2): migrate into watr so the "watr is the sole optimizer" invariant holds fully.
641
756
  if (cfg.hoistGlobalPtrOffset !== false) {
642
757
  const stableGlobals = stablePtrGlobalNames()
643
758
  if (stableGlobals.size) {
@@ -646,52 +761,20 @@ const jzCompileInner = (code, opts = {}) => {
646
761
  for (const node of funcs) hoistGlobalPtrOffset(node, stableGlobals, reach)
647
762
  }
648
763
  }
649
- // Final peephole pass: watOptimize's inliner can re-introduce rebox/unbox at boundaries
650
- // (e.g. inlined closure body's `i32.wrap_i64 (i64.reinterpret_f64 __env)` next to caller's
651
- // `boxPtrIR(g)` rebox). Our fusedRewrite folds these, watr's peephole doesn't.
652
- // Only valuable to re-run when watr ran (watr is what re-introduces the boundaries).
653
- if (cfg.watr) {
654
- // Build global name→type map from ctx.scope.globalTypes for promoteGlobals
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
- }
686
- time('watrReopt', () => {
687
- const funcs = optimized.filter(node => Array.isArray(node) && node[0] === 'func')
688
- const volatileGlobals = collectVolatileGlobals(funcs)
689
- const reach = collectReachableGlobalWrites(funcs)
690
- for (const node of funcs) optimizeFunc(node, cfg, globalTypesMap, volatileGlobals, 'post', reach)
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)
764
+ // NO post-watr optimizer. jz does ALL lowering — including auto-vectorization BEFORE watr
765
+ // (src/wat/assemble.js optimizeModule optimizeFunc 'pre'); watr is the sole optimizer and runs
766
+ // exactly ONCE, as a fixpoint. Re-running jz's leaf passes on watr's output (the former 'post'
767
+ // phase) both violated that architecture AND miscompiled: its propagate/fold sweep dropped a
768
+ // reassigned-param `local.tee` write and corrupted the divergent-escape SIMD frame (mandelbrot).
769
+ // The f64x2 stdlib mirrors the pre-watr vectorizer injects are appended in the emit phase
770
+ // (optimizeModule's appendLateStdlib); nothing post-watr needs to touch the module.
771
+ // Pre-eval tier 3 module-init snapshotting (src/snapshot.js): run __start once
772
+ // NOW, bake the post-init heap image + global values into the artifact, delete
773
+ // __start. Opt-in (optimize.snapshotInit); declined cleanly (dynamically-proven
774
+ // hermeticity) when init touches the host, loops forever, or memory is shared.
775
+ if (cfg.snapshotInit) {
776
+ const took = time('snapshotInit', () => snapshotInit(optimized, watrCompile))
777
+ if (!took && opts.warnings) ctx.warnings?.push?.({ code: 'snapshot-declined', message: 'init snapshot declined (host-touching, timer, or shared-memory init) compiled without it' })
695
778
  }
696
779
  try {
697
780
  if (opts.wat) {
@@ -805,3 +888,11 @@ export const compileModule = (code, opts = {}) => {
805
888
  }
806
889
 
807
890
  export { instantiateRuntime as instantiate }
891
+
892
+ /**
893
+ * jzify as a standalone source→source transform: full JS in, canonical jz out.
894
+ * Same jzify/ module the compiler runs on every parse (default-on) — re-exported
895
+ * here so browser bundles (dist/jz.js: REPL auto-jzify on paste) and node users
896
+ * (`import { transform } from 'jz'`, also `jz/transform`) share one lowering.
897
+ */
898
+ export { default as transform } from './transform.js'