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
@@ -12,8 +12,8 @@
12
12
  * @module assemble
13
13
  */
14
14
 
15
- import { parse as parseWat } from 'watr'
16
- import { ctx, inc, resolveIncludes, err, PTR, LAYOUT } from './ctx.js'
15
+ import parseWat from 'watr/parse'
16
+ import { ctx, inc, resolveIncludes, err, PTR, LAYOUT, HEAP, declGlobal } from '../ctx.js'
17
17
 
18
18
  // Stdlib WAT templates are fixed text (or feature-keyed text from a factory) —
19
19
  // `parseWat` of the same string always yields the same tree. Parsing is the
@@ -33,10 +33,12 @@ const parseTemplate = (str) => {
33
33
  if (tmpl === undefined) stdlibParseCache.set(str, tmpl = parseWat(str))
34
34
  return cloneTemplate(tmpl)
35
35
  }
36
- import { T, VAL, analyzeValTypes } from './analyze.js'
37
- import { optimizeFunc, collectVolatileGlobals, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, arenaRewindModule } from './optimize.js'
38
- import { emit } from './emit.js'
39
- import { mkPtrIR, MAX_CLOSURE_ARITY, MEM_OPS, findBodyStart } from './ir.js'
36
+ import { T } from '../ast.js'
37
+ import { analyzeValTypes, analyzeBody } from '../compile/analyze.js'
38
+ import { VAL } from '../reps.js'
39
+ import { optimizeFunc, collectVolatileGlobals, collectReachableGlobalWrites, hoistGlobalPtrOffset, stablePtrGlobalNames, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, arenaRewindModule } from '../optimize/index.js'
40
+ import { emit, emitVoid } from '../compile/emit.js'
41
+ import { mkPtrIR, MAX_CLOSURE_ARITY, MEM_OPS, findBodyStart } from '../ir.js'
40
42
 
41
43
  // NaN-prefix top-13-bits as BigInt — used by the static-prefix-strip pass
42
44
  const NAN_PREFIX = BigInt(LAYOUT.NAN_PREFIX)
@@ -46,16 +48,16 @@ const TAG_SHIFT_BIG = BigInt(LAYOUT.TAG_SHIFT)
46
48
  const AUX_SHIFT_BIG = BigInt(LAYOUT.AUX_SHIFT)
47
49
  const SSO_BIT_BIG = BigInt(LAYOUT.SSO_BIT)
48
50
 
49
- // memory[1020] holds the heap pointer only for shared memory (wasm globals are
51
+ // memory[HEAP.PTR_ADDR] holds the heap pointer only for shared memory (wasm globals are
50
52
  // per-instance — see module/core.js comment). Non-shared memory uses $__heap.
51
53
  const heapUsesMem = () => ctx.memory.shared
52
54
 
53
55
  const heapGetIR = () => heapUsesMem()
54
- ? ['i32.load', ['i32.const', 1020]]
56
+ ? ['i32.load', ['i32.const', HEAP.PTR_ADDR]]
55
57
  : ['global.get', '$__heap']
56
58
 
57
59
  const heapSetIR = value => heapUsesMem()
58
- ? ['i32.store', ['i32.const', 1020], value]
60
+ ? ['i32.store', ['i32.const', HEAP.PTR_ADDR], value]
59
61
  : ['global.set', '$__heap', value]
60
62
 
61
63
  const ARENA_SAFE_CALLS = new Set([
@@ -134,19 +136,44 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
134
136
  ctx.func.locals = new Map()
135
137
  ctx.func.localReps = null
136
138
  ctx.func.boxed = new Map()
139
+ ctx.func.cellTypes = new Set()
137
140
  ctx.func.stack = []
138
141
  ctx.func.current = { params: [], results: [] }
142
+ // Reserve prepare-generated temp names (for-of `arrVar`/`idx`/`len`,
143
+ // destructure scratch, …) in the __start frame so emit's temp()/tempI32()
144
+ // skip them — the same pre-seed analyzeFuncForEmit gives every function frame.
145
+ // Only T-sentinel names: they're always __start locals (user module-scope
146
+ // bindings become globals and can't contain T). Without this, prepare's
147
+ // `${T}arr${n}` collides with an emit-time tempI32('arr') at the same uniq,
148
+ // declaring the array pointer's local i32 and corrupting it via convert_i32_s.
149
+ const seedGeneratedLocals = (body) => {
150
+ for (const [n, t] of analyzeBody(body).locals)
151
+ if (n.includes(T) && !ctx.func.locals.has(n)) ctx.func.locals.set(n, t)
152
+ }
139
153
  analyzeValTypes(ast)
140
154
  const normalizeIR = ir => !ir?.length ? [] : Array.isArray(ir[0]) ? ir : [ir]
141
155
 
156
+ // Mark module-scope emission: top-level statements run exactly once, so a constant
157
+ // array/object literal here is a single instance that can safely live in a static
158
+ // data segment (no per-call freshness to violate). Function bodies — compiled
159
+ // separately, and the late closures below — leave this unset and alloc fresh.
160
+ ctx.func.atModuleScope = true
142
161
  const moduleInits = []
143
162
  if (ctx.module.moduleInits) {
144
163
  for (const mi of ctx.module.moduleInits) {
145
164
  analyzeValTypes(mi)
165
+ seedGeneratedLocals(mi)
146
166
  moduleInits.push(...normalizeIR(emit(mi)))
147
167
  }
148
168
  }
149
- const init = emit(ast)
169
+ seedGeneratedLocals(ast)
170
+ // __start has no result: emit the top-level program in void context so a stray
171
+ // value is dropped. `ast` is normally a `;` statement-sequence (each statement
172
+ // already void-dropped), but jzify unwraps a single-statement program to its
173
+ // bare expression — emitting that in value context leaves a value on the stack
174
+ // and the start function fails validation. emitVoid handles both shapes.
175
+ const init = emitVoid(ast)
176
+ ctx.func.atModuleScope = false
150
177
 
151
178
  // Module-scope object literals can create closure bodies while `emit(ast)`
152
179
  // runs. Those late closures may pull in stdlib helpers (notably JSON.parse)
@@ -269,6 +296,47 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
269
296
  sec.funcs.unshift(...closureFuncs.slice(beforeLateClosures))
270
297
  }
271
298
 
299
+ /**
300
+ * Hoist constant global initializers out of `__start` into immutable inline decls.
301
+ *
302
+ * A top-level `const x = <constant>` for a non-numeric value (atom `true`/`null`/
303
+ * `undefined`/`NaN`, an SSO or static-string NaN-box, a folded pointer) emits a
304
+ * `(global.set $x (f64.const …))` into `__start`, because only *numeric* consts are
305
+ * folded ahead of emit. But the value is a compile-time constant, so it belongs in
306
+ * the decl itself — `(global $x f64 (f64.const …))` — exactly like the numeric path.
307
+ * That drops the store, and when it empties `__start` the start function and its
308
+ * directive go too. Gated to single-assignment user `const`s so we never freeze a
309
+ * binding something else writes.
310
+ */
311
+ export function hoistConstGlobalInits(sec) {
312
+ const startFn = sec.start.find(n => Array.isArray(n) && n[0] === 'func' && n[1] === '$__start')
313
+ if (!startFn) return
314
+ const writes = new Map()
315
+ const scan = (node) => {
316
+ if (!Array.isArray(node)) return
317
+ if (node[0] === 'global.set' && typeof node[1] === 'string') writes.set(node[1], (writes.get(node[1]) || 0) + 1)
318
+ for (const c of node) scan(c)
319
+ }
320
+ for (const arr of [sec.funcs, sec.stdlib, sec.start]) for (const fn of arr) scan(fn)
321
+ for (let i = startFn.length - 1; i >= findBodyStart(startFn); i--) {
322
+ const stmt = startFn[i]
323
+ if (!Array.isArray(stmt) || stmt[0] !== 'global.set' || writes.get(stmt[1]) !== 1) continue
324
+ const name = typeof stmt[1] === 'string' && stmt[1][0] === '$' ? stmt[1].slice(1) : null
325
+ const g = name && ctx.scope.globals.get(name)
326
+ const c = stmt[2]
327
+ if (!g || !g.mut || !ctx.scope.consts?.has(name) || !ctx.scope.userGlobals?.has(name)) continue
328
+ if (!Array.isArray(c) || c[0] !== `${g.type}.const`) continue
329
+ ctx.scope.globals.set(name, { ...g, mut: false, init: c[1] })
330
+ startFn.splice(i, 1)
331
+ }
332
+ // Hoisting can empty `__start`. The O2 watr pass prunes a bodyless start, but at
333
+ // O0/O1 nothing else does — drop it (func + directive) here so a const-only module
334
+ // carries no start at all.
335
+ if (findBodyStart(startFn) >= startFn.length)
336
+ for (let j = sec.start.length - 1; j >= 0; j--)
337
+ if (Array.isArray(sec.start[j]) && sec.start[j][1] === '$__start') sec.start.splice(j, 1)
338
+ }
339
+
272
340
  /**
273
341
  * Phase: closure-body dedup.
274
342
  *
@@ -342,8 +410,12 @@ export function finalizeClosureTable(sec) {
342
410
  }
343
411
  for (const fn of sec.funcs) { scan(fn); if (indirectUsed) break }
344
412
  if (!indirectUsed) for (const fn of sec.start) scan(fn)
345
- if (!indirectUsed) for (const s of Object.keys(ctx.core.stdlib)) {
346
- if (ctx.core.stdlib[s]?.includes?.('call_indirect')) { indirectUsed = true; break }
413
+ // stdlib values are mixed: WAT-template strings + lazy generator functions.
414
+ // Only the string templates can carry a literal `call_indirect`; a typeof
415
+ // guard skips the generators (where `.includes` is meaningless — and on a jz
416
+ // closure receiver would read the closure pointer as a string, out of bounds).
417
+ if (!indirectUsed) for (const tpl of Object.values(ctx.core.stdlib)) {
418
+ if (typeof tpl === 'string' && tpl.includes('call_indirect')) { indirectUsed = true; break }
347
419
  }
348
420
  if (indirectUsed) {
349
421
  if (!ctx.closure.table) ctx.closure.table = []
@@ -400,20 +472,154 @@ export function finalizeClosureTable(sec) {
400
472
  for (const fn of sec.start) rewriteCalls(fn)
401
473
  }
402
474
 
475
+ /**
476
+ * Stdlib funcs actually reachable from the emitted program. Seeds from real
477
+ * `call`/`return_call`/`ref.func` sites in the user funcs, `__start`, and the elem
478
+ * table, then closes transitively over the stdlib call graph (each reached helper's
479
+ * template references). Conservative by construction — a template `$__foo` in a
480
+ * feature-dead branch is kept, never dropped — so it's safe to gate inclusion and the
481
+ * memory/allocator decision on it. An eagerly-`inc`'d helper that nothing calls is
482
+ * absent, which is the whole point.
483
+ */
484
+ function reachableStdlib(sec) {
485
+ const stdlib = ctx.core.stdlib
486
+ const reach = new Set(), stack = []
487
+ // Track every reached name (module-namespace `math.sin` included), but only follow
488
+ // those with a stdlib template. Names match `$foo`, `$__foo`, `$math.sin_core` — the
489
+ // dotted module funcs are the ones the `$__`-only regex used to miss, pruning live code.
490
+ const add = (name) => { if (!reach.has(name)) { reach.add(name); if (stdlib[name] != null) stack.push(name) } }
491
+ const scanIR = (node) => {
492
+ if (!Array.isArray(node)) return
493
+ if ((node[0] === 'call' || node[0] === 'return_call' || node[0] === 'ref.func') &&
494
+ typeof node[1] === 'string' && node[1][0] === '$') add(node[1].slice(1))
495
+ for (const c of node) scanIR(c)
496
+ }
497
+ for (const fn of sec.funcs) scanIR(fn)
498
+ for (const fn of sec.start) scanIR(fn)
499
+ for (const e of sec.elem) // closure table: bare `$fn` func refs
500
+ if (Array.isArray(e)) for (const c of e) if (typeof c === 'string' && c[0] === '$') add(c.slice(1))
501
+ // A stdlib func that self-exports (`(export "__invoke_closure")`) is a host-facing
502
+ // entry point — the JS host calls it directly, so it's a root even when nothing in
503
+ // the wasm calls it. Mirrors treeshake's inline-export rooting.
504
+ for (const n of ctx.core.includes) {
505
+ const v = stdlib[n]
506
+ let t = ''
507
+ try { t = typeof v === 'function' ? v() : v } catch { t = '' }
508
+ if (typeof t === 'string' && t.includes('(export "')) add(n)
509
+ }
510
+ while (stack.length) {
511
+ const v = stdlib[stack.pop()]
512
+ let text = ''
513
+ try { text = typeof v === 'function' ? v() : v } catch { text = '' }
514
+ if (typeof text === 'string') for (const m of text.matchAll(/\$([A-Za-z_][A-Za-z0-9_.]*)/g)) add(m[1])
515
+ }
516
+ return reach
517
+ }
518
+
519
+ // The f64x2 stdlib mirrors the lane vectorizer (optimize/vectorize.js) injects in the LATE 'post'
520
+ // pass — after the stdlib was pulled + treeshaken. Keep in sync with that pass's call-rewrite map
521
+ // (PPC_CALL2). These are the ONLY helpers appendLateStdlib may add; restricting to them avoids
522
+ // touching helpers that live in other module sections (ext-stdlib, imports) where a blind
523
+ // referenced-but-absent scan would wrongly re-append and duplicate them.
524
+ const LATE_VEC_HELPERS = new Set(['math.sin2', 'math.cos2', 'math.pow2', 'math.atan2_2', 'math.hypot_2', 'math.log_v', 'math.exp_v', 'math.exp2_v'])
525
+
526
+ // A late pass can reference one of the f64x2 mirrors that wasn't present when the stdlib was first
527
+ // assembled. Append any referenced-but-missing mirror body (fixpoint over their own calls, though
528
+ // the trig mirrors call nothing). moduleArr is mutated in place; non-mirror references are left for
529
+ // watr to resolve (a genuine missing helper is the kernel's own pull, already satisfied).
530
+ export function appendLateStdlib(moduleArr) {
531
+ const stdlib = ctx.core.stdlib
532
+ const have = new Set()
533
+ for (const n of moduleArr) if (Array.isArray(n) && n[0] === 'func' && typeof n[1] === 'string') have.add(n[1])
534
+ let added = true
535
+ while (added) {
536
+ added = false
537
+ const refs = new Set()
538
+ const scan = (n) => { if (!Array.isArray(n)) return; if ((n[0] === 'call' || n[0] === 'return_call' || n[0] === 'ref.func') && typeof n[1] === 'string' && n[1][0] === '$') refs.add(n[1]); for (const c of n) scan(c) }
539
+ for (const n of moduleArr) scan(n)
540
+ for (const ref of refs) {
541
+ const name = ref.slice(1)
542
+ if (have.has(ref) || !LATE_VEC_HELPERS.has(name) || stdlib[name] == null) continue
543
+ const node = parseTemplate(typeof stdlib[name] === 'function' ? stdlib[name]() : stdlib[name])
544
+ moduleArr.push(node[0] === 'module' ? node[1] : node)
545
+ have.add(ref)
546
+ added = true
547
+ }
548
+ }
549
+ }
550
+
403
551
  /**
404
552
  * Phase: pull stdlib + memory.
405
553
  */
406
554
  export function pullStdlib(sec) {
407
555
  resolveIncludes()
408
556
 
409
- const needsMemory = [...ctx.core.includes].some(n => ctx.core.stdlib[n] && MEM_OPS.test(ctx.core.stdlib[n]))
410
- if (!needsMemory) ctx.scope.globals.delete('__heap')
557
+ // Reachability, not inclusion, decides what the output needs. `ctx.core.includes`
558
+ // accumulates everything a module *might* use (eager module-load `inc`s + transitive
559
+ // deps), but a const array / static string literal calls none of it. So we seed from
560
+ // the actual call sites in the emitted funcs + __start (+ elem table) and close
561
+ // transitively over the stdlib call graph. An eagerly-included helper that nothing
562
+ // calls never enters this set — so allocator, memory, and exports reflect real use.
563
+ const reachable = reachableStdlib(sec)
564
+ const realize = (n) => { const v = ctx.core.stdlib[n]; try { return typeof v === 'function' ? v() : v } catch { return '' } }
565
+
566
+ // Two distinct needs, kept separate:
567
+ // · needsAlloc — the program allocates at runtime: an allocator func is reachable,
568
+ // or shared-mem string literals seed a pool __start allocs. Drives the bump
569
+ // allocator (`__alloc`/`__alloc_hdr`/`__clear`), the `__heap` pointer, and the
570
+ // `_alloc`/`_clear` marshalling exports.
571
+ // · needsMemory — linear memory must merely *exist*: we allocate, OR a literal lives
572
+ // in a static data segment (a const pointer, no allocator behind it), OR a reached
573
+ // helper / inline body does a load/store, OR `__ptr_type` is reached (the module
574
+ // discriminates heap tags — an `instanceof`/`typeof x==='object'` whose argument the
575
+ // host marshals across the boundary). A data segment with no memory is invalid wasm,
576
+ // so memory can't be gated on allocation alone.
577
+ const ALLOC_FUNCS = ['__alloc', '__alloc_hdr', '__alloc_hdr_n']
578
+ const needsAlloc = !!ctx.runtime.strPool || ALLOC_FUNCS.some(a => reachable.has(a))
579
+ // Memory ops can be emitted *inline* into user/start funcs (a heap-path char read
580
+ // loads without calling a stdlib helper), so scan the emitted bodies too.
581
+ const hasMemOp = (node) => Array.isArray(node) &&
582
+ ((typeof node[0] === 'string' && MEM_OPS.test(node[0])) || node.some(hasMemOp))
583
+ // `ctx.runtime.data` is never empty here — the number module seeds a static stringify
584
+ // prefix (`NaNInfinity…`) at offset 0; stripStaticDataPrefix removes it when unused, so
585
+ // the real question is whether any data lives *beyond* that strippable prefix.
586
+ // An explicit `{ memory: pages }` / shared-memory option is a caller request to own
587
+ // linear memory (e.g. to marshal host values in), independent of what the wasm itself
588
+ // reaches — honour it even for an otherwise-memoryless program.
589
+ const explicitMemory = ctx.memory.pages > 0 || !!ctx.memory.shared
590
+ const needsMemory = needsAlloc || explicitMemory ||
591
+ (ctx.runtime.data?.length || 0) > (ctx.runtime.staticDataLen || 0) ||
592
+ reachable.has('__ptr_type') ||
593
+ [...reachable].some(n => MEM_OPS.test(realize(n))) ||
594
+ sec.funcs.some(hasMemOp) || sec.start.some(hasMemOp)
595
+ // Emit only what's reachable: drop every eagerly-`inc`'d *internal* helper the program
596
+ // never calls. This is what lets a const-array / static-string / atom module shed the
597
+ // allocator, pointer dispatchers, and length helpers that an array/object module load
598
+ // pulled in wholesale — and it keeps the dead allocator from dangling on the `$__heap`
599
+ // we delete below. Scoped to `__`-prefixed names: module-namespace funcs (`math.sin`)
600
+ // are pulled in on demand, never eagerly, so they're already minimal and never pruned
601
+ // here (guarding against any reachability blind spot in a dotted-name template).
602
+ for (const n of [...ctx.core.includes]) if (n.startsWith('__') && !reachable.has(n)) ctx.core.includes.delete(n)
603
+ if (!needsAlloc) ctx.scope.globals.delete('__heap')
411
604
  if (needsMemory && ctx.module.modules.core) {
412
- for (const fn of ['__alloc', '__alloc_hdr', '__clear']) if (!ctx.core.includes.has(fn)) ctx.core.includes.add(fn)
413
- const pages = ctx.memory.pages || 1
414
- if (ctx.memory.shared) sec.imports.push(['import', '"env"', '"memory"', ['memory', pages]])
415
- else sec.memory.push(['memory', ['export', '"memory"'], pages])
416
- if (ctx.transform.alloc !== false && ctx.core._allocRawFuncs)
605
+ if (needsAlloc) {
606
+ for (const fn of ['__alloc', '__alloc_hdr', '__clear']) ctx.core.includes.add(fn)
607
+ // Late-add of allocators may pull in transitive deps (__alloc __memgrow,
608
+ // etc.) that the initial resolveIncludes did not yet see; re-resolve.
609
+ // No-op when the alloc trio was already present.
610
+ resolveIncludes()
611
+ }
612
+ // Initial pages must cover the static data segment (it loads at instantiation), not
613
+ // just the default 1 — otherwise a module whose constants exceed 64 KiB emits a data
614
+ // segment that overflows its own memory. The heap grows past this on demand via
615
+ // __memgrow. (Shared memory loads literals via memory.init into allocated space, so
616
+ // its initial size isn't pinned by the data length.)
617
+ const dataPages = ctx.memory.shared ? 0 : Math.ceil((ctx.runtime.data?.length || 0) / 65536)
618
+ const pages = Math.max(ctx.memory.pages || 1, dataPages)
619
+ const max = ctx.memory.max || 0 // 0 = no maximum (unbounded growth)
620
+ if (ctx.memory.shared) sec.imports.push(['import', '"env"', '"memory"', max ? ['memory', pages, max] : ['memory', pages]])
621
+ else sec.memory.push(max ? ['memory', ['export', '"memory"'], pages, max] : ['memory', ['export', '"memory"'], pages])
622
+ if (needsAlloc && ctx.transform.alloc !== false && ctx.core._allocRawFuncs)
417
623
  sec.funcs.push(...ctx.core._allocRawFuncs.map(parseTemplate))
418
624
  }
419
625
 
@@ -443,31 +649,34 @@ export function syncImports(sec) {
443
649
  /**
444
650
  * Phase: whole-module + per-function optimization passes.
445
651
  */
446
- export function optimizeModule(sec) {
652
+ export function optimizeModule(sec, profiler) {
653
+ const t = profiler?.time ? (name, fn) => profiler.time(`optMod:${name}`, fn) : (_, fn) => fn()
447
654
  const cfg = ctx.transform.optimize
448
- if (!cfg || cfg.specializeMkptr !== false)
449
- specializeMkptr([...sec.funcs, ...sec.stdlib, ...sec.start], wat => sec.stdlib.push(parseWat(wat)), parseWat)
450
- if (!cfg || cfg.specializePtrBase !== false)
451
- specializePtrBase([...sec.funcs, ...sec.stdlib, ...sec.start], wat => sec.stdlib.push(parseWat(wat)), parseWat)
452
- if (ctx.runtime.strPool && (!cfg || cfg.sortStrPoolByFreq !== false)) {
655
+ if (!cfg || cfg.specializeMkptr !== false) t('specializeMkptr', () =>
656
+ specializeMkptr([...sec.funcs, ...sec.stdlib, ...sec.start], wat => sec.stdlib.push(parseWat(wat)), parseWat))
657
+ if (!cfg || cfg.specializePtrBase !== false) t('specializePtrBase', () =>
658
+ specializePtrBase([...sec.funcs, ...sec.stdlib, ...sec.start], wat => sec.stdlib.push(parseWat(wat)), parseWat))
659
+ if (ctx.runtime.strPool && (!cfg || cfg.sortStrPoolByFreq !== false)) t('sortStrPool', () => {
453
660
  const poolRef = { pool: ctx.runtime.strPool }
454
661
  sortStrPoolByFreq([...sec.funcs, ...sec.stdlib, ...sec.start], poolRef, ctx.runtime.strPoolDedup)
455
662
  ctx.runtime.strPool = poolRef.pool
456
- }
457
- // Backfill globalTypes for runtime globals declared only in ctx.scope.globals
458
- // (e.g., __schema_tbl, __strBase). Parses the WAT string to infer i32/f64/i64.
459
- if (ctx.scope.globals) {
460
- for (const [name, wat] of ctx.scope.globals) {
461
- if (!wat || ctx.scope.globalTypes.has(name)) continue
462
- const m = wat.match(/\(global\s+\$?\S+\s+(?:\(mut\s+)?(i32|i64|f64|f32)/)
463
- if (m) ctx.scope.globalTypes.set(name, m[1])
464
- }
465
- }
663
+ })
664
+ // (globalTypes backfill gone: declGlobal sets the type at declaration.)
466
665
  // Build global name→type map from ctx.scope.globalTypes (keys without $) for promoteGlobals
467
666
  const globalTypesMap = ctx.scope.globalTypes ? new Map([...ctx.scope.globalTypes].map(([k, v]) => [`$${k}`, v])) : null
468
667
  const allFuncs = [...sec.funcs, ...sec.stdlib, ...sec.start]
469
- const volatileGlobals = collectVolatileGlobals(allFuncs)
470
- for (const s of allFuncs) optimizeFunc(s, cfg, globalTypesMap, volatileGlobals)
668
+ const volatileGlobals = t('volatileGlobals', () => collectVolatileGlobals(allFuncs))
669
+ const reachableWrites = t('reachableWrites', () => collectReachableGlobalWrites(allFuncs))
670
+ // Offset-hoist BEFORE promoteGlobals (inside optimizeFunc): value-promoting a
671
+ // stable-pointee global to a $_pg local would destroy the global.get pattern
672
+ // this pass matches, reverting rfft/diffusion to per-iteration resolves. After
673
+ // the hoist, the surviving global.get count is 1 (the entry snap) — naturally
674
+ // below promoteGlobals' threshold, so the two passes compose either way.
675
+ if (!cfg || cfg.hoistGlobalPtrOffset !== false) t('hoistGlobalPtr', () => {
676
+ const stable = stablePtrGlobalNames()
677
+ if (stable.size) for (const s of allFuncs) hoistGlobalPtrOffset(s, stable, reachableWrites)
678
+ })
679
+ t('optimizeFuncs', () => { for (const s of allFuncs) optimizeFunc(s, cfg, globalTypesMap, volatileGlobals, 'pre', reachableWrites) })
471
680
  if (!cfg || cfg.arenaRewind !== false) {
472
681
  const safeCallees = arenaRewindModule([...sec.funcs, ...sec.stdlib, ...sec.start])
473
682
  const fnByName = new Map()
@@ -481,11 +690,7 @@ export function optimizeModule(sec) {
481
690
  }
482
691
  }
483
692
  if (!cfg || cfg.hoistConstantPool !== false)
484
- hoistConstantPool([...sec.funcs, ...sec.stdlib, ...sec.start], (name, wat) => {
485
- ctx.scope.globals.set(name, wat)
486
- const m = wat.match(/\(global\s+\$?\S+\s+(?:\(mut\s+)?(i32|i64|f64|f32)/)
487
- if (m) ctx.scope.globalTypes.set(name, m[1])
488
- })
693
+ hoistConstantPool([...sec.funcs, ...sec.stdlib, ...sec.start], (name, lit) => declGlobal(name, 'f64', lit))
489
694
 
490
695
  // Second promoteGlobals pass disabled: promoting hoistConstantPool's __fc*
491
696
  // globals regressed the watr perf micro-pin (WASM compile time increased).
@@ -502,12 +707,8 @@ export function optimizeModule(sec) {
502
707
  const heapBase = (dataLen + 7) & ~7
503
708
  // Non-shared memory always carries a $__heap global — start it past the
504
709
  // static data so the bump allocator never overwrites a literal.
505
- ctx.scope.globals.set('__heap', `(global $__heap (export "__heap") (mut i32) (i32.const ${heapBase}))`)
506
- ctx.scope.globalTypes.set('__heap', 'i32')
507
- if (ctx.scope.globals.has('__heap_start')) {
508
- ctx.scope.globals.set('__heap_start', `(global $__heap_start (mut i32) (i32.const ${heapBase}))`)
509
- ctx.scope.globalTypes.set('__heap_start', 'i32')
510
- }
710
+ declGlobal('__heap', 'i32', heapBase, { export: '__heap' })
711
+ if (ctx.scope.globals.has('__heap_start')) declGlobal('__heap_start', 'i32', heapBase)
511
712
  for (const s of sec.stdlib)
512
713
  if (s[0] === 'func' && s[1] === '$__clear')
513
714
  for (let i = 2; i < s.length; i++)
@@ -528,18 +729,33 @@ export function stripStaticDataPrefix(sec) {
528
729
  for (let i = 0; i < data.length; i++) buf[i] = data.charCodeAt(i)
529
730
  const dv = new DataView(buf.buffer)
530
731
  if (ctx.runtime.staticPtrSlots) {
732
+ // u32-half reads/writes — DataView's BigInt accessors are unfaithful in the
733
+ // self-host kernel; the offset lives entirely in the LE low word and the
734
+ // tag/aux fields entirely in the high word, so plain number math suffices.
531
735
  for (const slotOff of ctx.runtime.staticPtrSlots) {
532
736
  if (slotOff < prefix) continue
533
- const bits = dv.getBigUint64(slotOff, true)
534
- if (((bits >> 48n) & 0xFFF8n) !== NAN_PREFIX) continue
535
- const ty = Number((bits >> TAG_SHIFT_BIG) & TAG_MASK_BIG)
737
+ const hi = dv.getUint32(slotOff + 4, true)
738
+ if (((hi >>> 16) & 0xFFF8) !== LAYOUT.NAN_PREFIX) continue
739
+ const ty = (hi >>> 15) & 15
536
740
  if (!SHIFTABLE.has(ty)) continue
537
- if (ty === PTR.STRING && ((bits >> AUX_SHIFT_BIG) & SSO_BIT_BIG)) continue
538
- const off = Number(bits & OFFSET_MASK_BIG)
741
+ if (ty === PTR.STRING && ((hi >>> (LAYOUT.AUX_SHIFT - 32)) & LAYOUT.SSO_BIT)) continue
742
+ const off = dv.getUint32(slotOff, true)
539
743
  if (off < prefix) continue
540
- const hi = bits & ~OFFSET_MASK_BIG
541
- dv.setBigUint64(slotOff, hi | BigInt(off - prefix), true)
744
+ dv.setUint32(slotOff, off - prefix, true)
745
+ }
746
+ }
747
+ // The intern index (buildInternTable) stores raw static-string ptrs as u32
748
+ // slots — shift each occupied slot like every other static reference, and
749
+ // re-declare the (already-declared) base global at its post-strip position.
750
+ if (ctx.runtime.internTable) {
751
+ const { base, size } = ctx.runtime.internTable
752
+ for (let i = 0; i < size; i++) {
753
+ const slot = base + i * 8 + 4
754
+ const off = dv.getUint32(slot, true)
755
+ if (off >= prefix) dv.setUint32(slot, off - prefix, true)
542
756
  }
757
+ ctx.runtime.internTable.base = base - prefix
758
+ declGlobal('__internBase', 'i32', base - prefix, { mut: false })
543
759
  }
544
760
  let s = ''
545
761
  for (let i = prefix; i < buf.length; i++) s += String.fromCharCode(buf[i])
@@ -559,6 +775,10 @@ export function stripStaticDataPrefix(sec) {
559
775
  Array.isArray(child[3]) && child[3][0] === 'i32.const' &&
560
776
  typeof child[3][1] === 'number' && (child[3][1] & LAYOUT.SSO_BIT)
561
777
  if (!isSsoString) child[4][1] -= prefix
778
+ } else if (typeof child[0] === 'string' && child[0].endsWith('.store') &&
779
+ Array.isArray(child[1]) && child[1][0] === 'i32.const' &&
780
+ typeof child[1][1] === 'number' && child[1][1] >= prefix) {
781
+ child[1][1] -= prefix
562
782
  } else if (child[0] === 'f64.const' &&
563
783
  typeof child[1] === 'string' && child[1].startsWith('nan:0x')) {
564
784
  const bits = BigInt(child[1].slice(4)) | 0x7FF0000000000000n