jz 0.5.1 → 0.6.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 (80) hide show
  1. package/README.md +288 -314
  2. package/bench/README.md +319 -0
  3. package/bench/bench.svg +112 -0
  4. package/cli.js +32 -24
  5. package/index.js +177 -55
  6. package/interop.js +88 -159
  7. package/jz.svg +5 -0
  8. package/jzify/arguments.js +97 -0
  9. package/jzify/bundler.js +382 -0
  10. package/jzify/classes.js +328 -0
  11. package/jzify/hoist-vars.js +177 -0
  12. package/jzify/index.js +51 -0
  13. package/jzify/names.js +37 -0
  14. package/jzify/switch.js +106 -0
  15. package/jzify/transform.js +349 -0
  16. package/layout.js +179 -0
  17. package/module/array.js +322 -153
  18. package/module/collection.js +603 -145
  19. package/module/console.js +55 -43
  20. package/module/core.js +266 -153
  21. package/module/date.js +15 -3
  22. package/module/function.js +73 -6
  23. package/module/index.js +2 -1
  24. package/module/json.js +226 -61
  25. package/module/math.js +414 -186
  26. package/module/number.js +306 -60
  27. package/module/object.js +448 -184
  28. package/module/regex.js +255 -25
  29. package/module/schema.js +24 -6
  30. package/module/simd.js +85 -0
  31. package/module/string.js +586 -220
  32. package/module/symbol.js +1 -1
  33. package/module/timer.js +9 -14
  34. package/module/typedarray.js +45 -48
  35. package/package.json +41 -12
  36. package/src/abi/index.js +39 -23
  37. package/src/abi/string.js +38 -41
  38. package/src/ast.js +460 -0
  39. package/src/autoload.js +26 -24
  40. package/src/bridge.js +111 -0
  41. package/src/compile/analyze-scans.js +661 -0
  42. package/src/compile/analyze.js +1565 -0
  43. package/src/compile/emit-assign.js +408 -0
  44. package/src/compile/emit.js +3201 -0
  45. package/src/compile/flow-types.js +103 -0
  46. package/src/{compile.js → compile/index.js} +497 -125
  47. package/src/{infer.js → compile/infer.js} +27 -98
  48. package/src/{narrow.js → compile/narrow.js} +302 -96
  49. package/src/compile/plan/advise.js +316 -0
  50. package/src/compile/plan/common.js +150 -0
  51. package/src/compile/plan/index.js +118 -0
  52. package/src/compile/plan/inline.js +679 -0
  53. package/src/compile/plan/literals.js +984 -0
  54. package/src/compile/plan/loops.js +472 -0
  55. package/src/compile/plan/scope.js +573 -0
  56. package/src/compile/program-facts.js +404 -0
  57. package/src/ctx.js +176 -58
  58. package/src/ir.js +540 -171
  59. package/src/kind-traits.js +105 -0
  60. package/src/kind.js +462 -0
  61. package/src/op-policy.js +57 -0
  62. package/src/{optimize.js → optimize/index.js} +1106 -446
  63. package/src/optimize/vectorize.js +1874 -0
  64. package/src/param-reps.js +65 -0
  65. package/src/parse.js +44 -0
  66. package/src/{prepare.js → prepare/index.js} +589 -202
  67. package/src/reps.js +115 -0
  68. package/src/resolve.js +12 -3
  69. package/src/static.js +199 -0
  70. package/src/type.js +647 -0
  71. package/src/{assemble.js → wat/assemble.js} +86 -48
  72. package/src/wat/optimize.js +3760 -0
  73. package/transform.js +21 -0
  74. package/wasi.js +47 -5
  75. package/src/analyze.js +0 -3818
  76. package/src/emit.js +0 -3040
  77. package/src/jzify.js +0 -1580
  78. package/src/plan.js +0 -2132
  79. package/src/vectorize.js +0 -1088
  80. /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,14 +40,19 @@
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 { ctx, reset, err, initWarnings, assertCtxInvariants } from './src/ctx.js'
48
+ import prepare, { GLOBALS } from './src/prepare/index.js'
49
+ import compile from './src/compile/index.js'
50
+ import { resetProgramFactsCache } from './src/compile/program-facts.js'
51
+ 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'
53
+ import { VAL } from './src/reps.js'
54
+ import jzify from './jzify/index.js'
55
+ import { T } from './src/ast.js'
51
56
  import {
52
57
  memory as enhanceMemory, instantiate as instantiateRuntime,
53
58
  } from './interop.js'
@@ -58,6 +63,11 @@ const importsMayReturnExternal = (imports) =>
58
63
  !!imports && Object.values(imports).some(mod =>
59
64
  Object.values(mod || {}).some(spec => typeof spec === 'function'))
60
65
 
66
+ // WHATWG URL resolution for compile-time import.meta lowering. Injected into
67
+ // ctx.transform (like parse/jzify) so prepare never references the `URL` global
68
+ // directly — keeps the self-host kernel free of host-only built-ins.
69
+ const resolveUrl = (spec, base) => new URL(spec, base).href
70
+
61
71
  const nowMs = () => globalThis.performance?.now ? globalThis.performance.now() : Date.now()
62
72
 
63
73
  const compileProfiler = (profile) => {
@@ -154,9 +164,10 @@ jz.memory = enhanceMemory
154
164
  * @param {string} code - jz source
155
165
  * @param {object} [opts]
156
166
  * @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.
167
+ * @param {boolean} [opts.strict] - Enforce the pure canonical subset: skip jzify
168
+ * (so full-JS syntax like var/function/class is rejected, not lowered) and reject
169
+ * dynamic features (obj[k], for-in, unknown receiver method calls) at compile time.
170
+ * Avoids pulling dynamic-dispatch stdlib into output; large size win for static programs.
160
171
  * @param {WebAssembly.Memory|number} [opts.memory] - Shared memory import, or
161
172
  * initial page count. Prefer `memory: N` for owned memory.
162
173
  * @param {boolean} [opts.alloc=true] - Export raw allocator helpers
@@ -172,13 +183,28 @@ jz.memory = enhanceMemory
172
183
  * mutable globals); trades size for speed.
173
184
  * - `{ level?: 0|1|2|3, watr?: bool, hoistAddrBase?: bool, ... }`: per-pass
174
185
  * overrides on top of the chosen level. See PASS_NAMES in src/optimize.js.
186
+ * @param {object} [opts.warnings] - Optional mutable warning sink populated with
187
+ * `entries: [{ code, message, fn?, line?, column? }]`. Heap-growth advisories
188
+ * fire when a module uses the bump allocator and an export or loop retains
189
+ * allocations without a host-side memory.reset().
175
190
  * @param {object} [opts.profile] - Optional mutable profile sink populated with
176
191
  * `entries` and `totals` for parse / jzify / prepare / compile / plan / watr phases.
177
192
  * Set `profile.names = true` to also emit a standard wasm `name` custom section
178
193
  * for profiler/debugger symbolication.
179
- * @param {boolean} [opts.profileNames] - Legacy alias for `profile.names`.
194
+ * @param {Object<string,string>} [opts.modules] - Map of module specifier → source
195
+ * for compile-time `import`/`export` bundling: jz resolves the module graph
196
+ * in-process from this map instead of reading from disk.
197
+ * @param {boolean} [opts.noTailCall] - Disable proper-tail-call emission (self/mutual
198
+ * recursion uses ordinary call frames). For engines/tools without the tail-call proposal.
199
+ * @param {boolean} [opts.nativeTimers] - Emit a blocking `__timer_loop` in `_start` so
200
+ * setTimeout/setInterval fire under a standalone runtime (e.g. the wasmtime CLI) that
201
+ * has no host event loop. Default: timers defer to the JS host.
180
202
  * @param {string} [opts.importMetaUrl] - Module URL used to lower `import.meta.url`
181
203
  * and static `import.meta.resolve("...")` expressions.
204
+ * @param {number|boolean} [opts.randomSeed] - Seed for `Math.random`. Default: seeded
205
+ * once from host entropy on first use (crypto under `host:'js'`, `random_get` under
206
+ * WASI) — non-reproducible. Pass a number for a fixed, reproducible seed; `true` forces
207
+ * entropy explicitly. The randomness syscall is emitted only when `Math.random` is used.
182
208
  * @param {boolean} [opts.inspect] - When true, return `{ wasm, inspect }`
183
209
  * (or `{ wat, inspect }` with `opts.wat`) instead of the bare output.
184
210
  * `inspect` carries per-function inferred shapes (params, locals, JSON shapes,
@@ -186,7 +212,14 @@ jz.memory = enhanceMemory
186
212
  * without re-running the analyzer. Pays a small serialization cost; off by default.
187
213
  * @returns {Uint8Array|string|{wasm: Uint8Array, inspect: object}|{wat: string, inspect: object}}
188
214
  */
215
+ // Test-only compile target. When set (by test/index.js under JZ_TEST_TARGET=jz.wasm)
216
+ // every jz.compile / compile / jz() call routes through it instead of the in-process
217
+ // compiler — used to run the whole suite against dist/jz.wasm (the jz compiler
218
+ // compiled to wasm by jz). null in production: one boolean check on a cold path.
219
+ let compileTarget = null
220
+ export const _setCompileTarget = (fn) => { compileTarget = fn }
189
221
  jz.compile = (code, opts = {}) => {
222
+ if (compileTarget) return compileTarget(code, opts)
190
223
  try {
191
224
  return jzCompileInner(code, opts)
192
225
  } catch (e) {
@@ -194,7 +227,8 @@ jz.compile = (code, opts = {}) => {
194
227
  // codegen leak — surface it as an internal compile error with the source
195
228
  // location we were standing on. err()-thrown errors already pass through.
196
229
  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.`)
230
+ // Pass `e` as the cause so the original stack (the real codegen site) survives.
231
+ 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
232
  }
199
233
  throw e
200
234
  }
@@ -274,7 +308,7 @@ const detectOptimizeConfig = (ast, code) => {
274
308
  // jz's already-optimized IR and inflates output. Disable it automatically.
275
309
  const isLarge = s.sourceChars > 4000 || s.funcCount > 40 || s.maxFuncBodySize > 300
276
310
  const isMachineLike = s.callSites > 300 && s.stringLiteralCount < 10
277
- if (isLarge || isMachineLike) cfg.watr = false
311
+ if (isLarge || isMachineLike) { cfg.watr = false; cfg.splitCharScan = false }
278
312
  // Typed-array heavy: tighten scalarization thresholds when we see large
279
313
  // fixed-size arrays; keep defaults for small/dynamic ones.
280
314
  if (s.typedArrayCount > 0 && s.maxTypedArrayLen > 0) {
@@ -292,13 +326,40 @@ const detectOptimizeConfig = (ast, code) => {
292
326
  return cfg
293
327
  }
294
328
 
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)
329
+ // Test-matrix bridge: when JZ_TEST_* env vars are set, inject them as default
330
+ // opts so the npm test suite can be re-run under varying configurations (opt
331
+ // levels, host, jzify, ) without source changes. User-supplied opts always win
332
+ // — env defaults fill only what the caller left unset. Resolved once at module
333
+ // load; no-op (single boolean check) when the env is empty (production path).
334
+ const TEST_ENV_DEFAULTS = (() => {
335
+ // Guard bare `process` — the compiler bundle must load in browsers/Workers too
336
+ // (this IIFE runs at module import; an unguarded ref throws ReferenceError there).
337
+ const e = (typeof process !== 'undefined' && process.env) || {}
338
+ const out = {}
339
+ if (e.JZ_TEST_OPTIMIZE != null) {
340
+ const v = e.JZ_TEST_OPTIMIZE
341
+ out.optimize = /^-?\d+$/.test(v) ? Number(v) : v === 'false' ? false : v
342
+ }
343
+ if (e.JZ_TEST_HOST) out.host = e.JZ_TEST_HOST
344
+ if (e.JZ_TEST_STRICT) out.strict = e.JZ_TEST_STRICT === '1'
345
+ return out
346
+ })()
347
+ const HAS_TEST_ENV = Object.keys(TEST_ENV_DEFAULTS).length > 0
348
+
349
+ // Shared front-half: reset ctx, wire opts → ctx.transform/memory/module/features,
350
+ // and inject parse/resolveUrl. Called by `jzCompileInner` (the only entry point
351
+ // today). The self-host entry (scripts/self.js) drives reset itself rather than
352
+ // going through this path, since it needs only a minimal, interop-free setup.
353
+ const setupCtx = (code, opts) => {
354
+ if (HAS_TEST_ENV) {
355
+ const merged = { ...opts }
356
+ for (const k of Object.keys(TEST_ENV_DEFAULTS)) if (merged[k] == null) merged[k] = TEST_ENV_DEFAULTS[k]
357
+ opts = merged
358
+ }
359
+ reset(emitter, GLOBALS, { emit, flat, body, bool, idx, spread })
360
+ resetProgramFactsCache()
300
361
  ctx.error.src = code
301
-
362
+ initWarnings(opts.warnings)
302
363
  if (typeof opts.memory === 'number') ctx.memory.pages = opts.memory
303
364
  else if (opts.memory) ctx.memory.shared = true
304
365
  if (opts.modules) ctx.module.importSources = opts.modules
@@ -306,9 +367,14 @@ const jzCompileInner = (code, opts = {}) => {
306
367
  ctx.module.hostImports = opts.imports
307
368
  if (importsMayReturnExternal(opts.imports)) ctx.features.external = true
308
369
  }
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
370
+ // Parser for compile-time import bundling (prepareModule). Injected, not
371
+ // imported by prepare see ctx.transform.parse note in prepare/index.js.
372
+ ctx.transform.parse = parse
373
+ ctx.transform.resolveUrl = resolveUrl
374
+ // jzify runs by default — accept the full JS subset (function/var/switch lowered to
375
+ // arrows/let/if). `strict: true` skips it, so prepare rejects disallowed JS features
376
+ // and the pure canonical subset is enforced. subscript handles ASI natively.
377
+ if (!opts.strict) ctx.transform.jzify = jzify
312
378
  if (opts.noTailCall) ctx.transform.noTailCall = true
313
379
  if (opts.strict) ctx.transform.strict = true
314
380
  if (opts.host) {
@@ -319,31 +385,64 @@ const jzCompileInner = (code, opts = {}) => {
319
385
  if (opts.alloc === false) ctx.transform.alloc = false
320
386
  if (opts.inspect) ctx.transform.inspect = true
321
387
  if (opts.importMetaUrl) ctx.transform.importMetaUrl = String(opts.importMetaUrl)
388
+ if (opts.randomSeed !== undefined) {
389
+ if (opts.randomSeed !== true && !Number.isFinite(opts.randomSeed))
390
+ 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}`)
391
+ ctx.transform.randomSeed = opts.randomSeed
392
+ }
322
393
  if (opts.nativeTimers) ctx.features.blockingTimers = true // wasmtime CLI: include __timer_loop in _start
323
394
  ctx.transform.optimize = resolveOptimize(opts.optimize)
324
-
325
395
  if (opts._interp) {
326
396
  for (const [name, fn] of Object.entries(opts._interp)) {
327
- if (name.startsWith('__ext_')) continue;
397
+ if (name.startsWith('__ext_')) continue
328
398
  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
399
  ctx.features.external = true
330
400
  const params = Array(fn.length).fill(['param', 'f64'])
331
401
  ctx.module.imports.push(['import', '"env"', `"${name}"`, ['func', `$${name}`, ...params, ['result', 'f64']]])
332
402
  }
333
403
  }
404
+ }
405
+
406
+ // U+E000 (T) prefixes every jz-generated local. The JS spec forbids it in
407
+ // identifiers, but subscript's parser is lenient and accepts it — so a user name
408
+ // carrying it could silently alias a compiler temp. Reject it in identifier
409
+ // position on the RAW parse (before jzify, which legitimately mints T-prefixed
410
+ // temps of its own). String-literal nodes are `[null, …]` and skipped, so
411
+ // `"……"` data is fine; only walked when the char is present in source.
412
+ const rejectReservedPrefix = (node) => {
413
+ if (!Array.isArray(node)) return
414
+ if (node.length === 2 && node[0] == null) return // [null, X] — value literal, not an identifier
415
+ for (let i = 1; i < node.length; i++) {
416
+ const v = node[i]
417
+ if (typeof v === 'string') {
418
+ 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`)
419
+ } else rejectReservedPrefix(v)
420
+ }
421
+ }
422
+
423
+ const jzCompileInner = (code, opts = {}) => {
424
+ const profiler = compileProfiler(opts.profile)
425
+ const time = (name, fn) => profiler ? profiler.time(name, fn) : fn()
426
+
427
+ setupCtx(code, opts)
428
+ assertCtxInvariants('post-reset')
334
429
 
335
430
  let parsed = time('parse', () => parse(code))
336
- if (opts.jzify) parsed = time('jzify', () => jzify(parsed))
431
+ if (typeof code === 'string' && code.includes(T)) rejectReservedPrefix(parsed)
432
+ if (!opts.strict) parsed = time('jzify', () => jzify(parsed))
337
433
  const ast = time('prepare', () => prepare(parsed))
434
+ assertCtxInvariants('post-prepare')
338
435
 
339
436
  // 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) {
437
+ // hasn't provided any optimize option. detectOptimizeConfig has two *live*
438
+ // overrides over the level-2 preset: the typed-array scalarization thresholds,
439
+ // and `watr: false` for large/machine-generated code (whose WAT-level CSE/DCE
440
+ // fights jz's already-optimized IR and inflates output). watr is ON at level 2,
441
+ // so that switch is a real override run the scan when the program either
442
+ // touches typed arrays or is large enough for the machine-code heuristic to
443
+ // bite. `code.length` is the one signal free without scanning; gating on it
444
+ // keeps small programs (the common case) on the no-scan fast path.
445
+ if (opts.optimize == null && (ctx.module.modules.typedarray || code.length > 4000)) {
347
446
  const autoCfg = detectOptimizeConfig(ast, code)
348
447
  if (Object.keys(autoCfg).length) {
349
448
  ctx.transform.optimize = resolveOptimize(autoCfg)
@@ -351,6 +450,7 @@ const jzCompileInner = (code, opts = {}) => {
351
450
  }
352
451
 
353
452
  const module = time('compile', () => compile(ast, profiler))
453
+ assertCtxInvariants('post-compile')
354
454
 
355
455
  // host: 'wasi' — error if the wasm would import any env.__ext_* helper. Those exist
356
456
  // only to defer to a JS host's value-aware semantics; in a wasmtime/wasmer/deno
@@ -368,24 +468,45 @@ const jzCompileInner = (code, opts = {}) => {
368
468
  }
369
469
 
370
470
  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
471
+ let watrOpts = typeof cfg.watr === 'object' ? { ...cfg.watr } : true
472
+ if (cfg.vectorizeLaneLocal) {
473
+ if (watrOpts === true) watrOpts = { loopify: false }
474
+ else if (typeof watrOpts === 'object' && watrOpts.loopify === undefined) watrOpts.loopify = false
475
+ }
476
+ if (cfg.devirtIndirect) {
477
+ if (watrOpts === true) watrOpts = { devirt: true }
478
+ else if (typeof watrOpts === 'object' && watrOpts.devirt === undefined) watrOpts.devirt = true
479
+ }
480
+ const optimized = cfg.watr ? time('watOptimize', () => watOptimize(module, watrOpts)) : module
481
+ // Stable-pointee module globals: resolve the __ptr_offset once per function.
482
+ // Never-forwarding kinds — every PTR tag outside __ptr_offset's forwarding
483
+ // set {ARRAY, HASH, SET, MAP} — give the same offset for the same bits, so
484
+ // the snapshot only needs the global's VALUE stable through the function:
485
+ // the reachable-writes call graph proves that precisely. Independent of watr
486
+ // (the auto-config turns watr off for large sources — exactly the
487
+ // module-global DSP-state programs this pass exists for: rfft, diffusion);
488
+ // when watr DID run, it goes before the post leaf passes so the snapped
489
+ // local participates in hoistAddrBase/cseScalarLoad.
490
+ if (cfg.hoistGlobalPtrOffset !== false) {
491
+ const stableGlobals = stablePtrGlobalNames()
492
+ if (stableGlobals.size) {
493
+ const funcs = optimized.filter(node => Array.isArray(node) && node[0] === 'func')
494
+ const reach = collectReachableGlobalWrites(funcs)
495
+ for (const node of funcs) hoistGlobalPtrOffset(node, stableGlobals, reach)
496
+ }
497
+ }
498
+ // Final peephole pass: watOptimize's inliner can re-introduce rebox/unbox at boundaries
378
499
  // (e.g. inlined closure body's `i32.wrap_i64 (i64.reinterpret_f64 __env)` next to caller's
379
500
  // `boxPtrIR(g)` rebox). Our fusedRewrite folds these, watr's peephole doesn't.
380
501
  // Only valuable to re-run when watr ran (watr is what re-introduces the boundaries).
381
502
  if (cfg.watr) {
382
- const postCfg = { ...cfg, __phase: 'post' }
383
503
  // Build global name→type map from ctx.scope.globalTypes for promoteGlobals
384
504
  const globalTypesMap = ctx.scope.globalTypes ? new Map([...ctx.scope.globalTypes].map(([k, v]) => [`$${k}`, v])) : null
385
505
  time('watrReopt', () => {
386
506
  const funcs = optimized.filter(node => Array.isArray(node) && node[0] === 'func')
387
507
  const volatileGlobals = collectVolatileGlobals(funcs)
388
- for (const node of funcs) optimizeFunc(node, postCfg, globalTypesMap, volatileGlobals)
508
+ const reach = collectReachableGlobalWrites(funcs)
509
+ for (const node of funcs) optimizeFunc(node, cfg, globalTypesMap, volatileGlobals, 'post', reach)
389
510
  })
390
511
  }
391
512
  try {
@@ -395,7 +516,7 @@ const jzCompileInner = (code, opts = {}) => {
395
516
  }
396
517
  const wasm = time('watrCompile', () => watrCompile(optimized))
397
518
  let bytes = wasm
398
- if (opts.profileNames || opts.profile?.names) bytes = appendFunctionNames(bytes, optimized)
519
+ if (opts.profile?.names) bytes = appendFunctionNames(bytes, optimized)
399
520
  return opts.inspect ? { wasm: bytes, inspect: ctx.inspect } : bytes
400
521
  } catch (e) {
401
522
  // watr surfaces dangling identifiers as "Unknown local|func|global|table|memory $X".
@@ -427,6 +548,8 @@ export default function jz(code, ...args) {
427
548
 
428
549
  // Serialize JS value to jz source literal. Returns null if not serializable.
429
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
430
553
  if (typeof v === 'number' || typeof v === 'boolean') return String(v)
431
554
  if (v === null) return 'null'
432
555
  if (typeof v === 'string') return JSON.stringify(v)
@@ -484,13 +607,12 @@ export default function jz(code, ...args) {
484
607
  // String call: jz('code', opts?) — compile + instantiate + wrap
485
608
  const callOpts = args[0] || {}
486
609
  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)
610
+ const wasm = out && typeof out === 'object' && 'wasm' in out ? out.wasm : out
611
+ const result = instantiateRuntime(wasm, callOpts)
612
+ const extra = {}
613
+ if (callOpts.inspect && out && typeof out === 'object' && 'inspect' in out) extra.inspect = out.inspect
614
+ if (callOpts.warnings) extra.warnings = callOpts.warnings.entries
615
+ return Object.keys(extra).length ? Object.assign(result, extra) : result
494
616
  }
495
617
 
496
618
  export { jz }