jz 0.2.1 → 0.3.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.
package/src/analyze.js CHANGED
@@ -33,11 +33,27 @@ export const STMT_OPS = new Set([';', 'let', 'const', 'return', 'if', 'for', 'fo
33
33
  '=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??=',
34
34
  'throw', 'try', 'catch', 'finally', '++', '--', '()'])
35
35
 
36
+ /** Assignment operators — shared across analyze, plan, emit. */
37
+ export const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
38
+
36
39
  /** Distinguish a function block body `{ ... }` from an expression-bodied object literal `({a:1})`.
37
40
  * Both share the `'{}'` op tag; blocks have a non-`':'` first child (object literals start with key:val pairs). */
38
41
  export const isBlockBody = (body) =>
39
42
  Array.isArray(body) && body[0] === '{}' && body[1]?.[0] !== ':'
40
43
 
44
+ /** Extract integer value from AST literal node. Returns null if not a 32-bit integer. */
45
+ export function intLiteralValue(expr) {
46
+ let v = null
47
+ if (typeof expr === 'number') v = expr
48
+ else if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number') v = expr[1]
49
+ else if (Array.isArray(expr) && expr[0] === 'u-' && typeof expr[1] === 'number') v = -expr[1]
50
+ else if (typeof expr === 'string') v = repOf(expr)?.intConst ?? ctx.scope.constInts?.get(expr) ?? null
51
+ return v != null && Number.isInteger(v) && v >= -2147483648 && v <= 2147483647 ? v : null
52
+ }
53
+
54
+ /** Non-negative integer literal — used for string/typed-array index bounds. */
55
+ export const nonNegIntLiteral = (node) => { const n = intLiteralValue(node); return n != null && n >= 0 ? n : null }
56
+
41
57
  /** Collect all `return X` expressions (X != null) from a function body, skipping nested arrow funcs.
42
58
  * Pushes into `out`. Non-returning paths are silently skipped — pair with `alwaysReturns` if total
43
59
  * coverage matters, or with `hasBareReturn` to detect `return;` (undef result). */
@@ -1892,8 +1908,6 @@ export function analyzePtrUnboxable(body, locals, boxed) {
1892
1908
  for (const a of args) collect(a)
1893
1909
  }
1894
1910
 
1895
- const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=',
1896
- '<<=', '>>=', '>>>=', '||=', '&&=', '??='])
1897
1911
  const NULL_CMP_OPS = new Set(['==', '!=', '===', '!=='])
1898
1912
 
1899
1913
  function check(node) {
@@ -2035,8 +2049,6 @@ export function findFreeVars(node, bound, free, scope) {
2035
2049
  for (const a of args) findFreeVars(a, bound, free, scope)
2036
2050
  }
2037
2051
 
2038
- const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
2039
-
2040
2052
  /** Check if any of the given variable names are assigned anywhere in the AST. */
2041
2053
  export function findMutations(node, names, mutated) {
2042
2054
  if (node == null || typeof node !== 'object' || !Array.isArray(node)) return
@@ -0,0 +1,544 @@
1
+ /**
2
+ * Module assembly — WAT section construction, optimization, and finalization.
3
+ *
4
+ * # Stage contract
5
+ * IN: per-function WAT IR (from emit), ctx state (includes, scope, closure, etc.)
6
+ * OUT: assembled module sections via the `sec` object, mutated in place.
7
+ *
8
+ * Extracted from compile.js to separate "per-function compilation" from
9
+ * "module assembly" concerns. All functions receive `sec` (the named-slots
10
+ * section accumulator) and read/write ctx state as needed.
11
+ *
12
+ * @module assemble
13
+ */
14
+
15
+ import { parse as parseWat } from 'watr'
16
+ import { ctx, inc, resolveIncludes, PTR, LAYOUT } from './ctx.js'
17
+ import { T, VAL, analyzeValTypes } from './analyze.js'
18
+ import { optimizeFunc, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, arenaRewindModule } from './optimize.js'
19
+ import { emit } from './emit.js'
20
+ import { mkPtrIR, MAX_CLOSURE_ARITY, MEM_OPS, findBodyStart } from './ir.js'
21
+
22
+ // NaN-prefix top-13-bits as BigInt — used by the static-prefix-strip pass
23
+ const NAN_PREFIX = BigInt(LAYOUT.NAN_PREFIX)
24
+ const TAG_MASK_BIG = BigInt(LAYOUT.TAG_MASK)
25
+ const OFFSET_MASK_BIG = BigInt(LAYOUT.OFFSET_MASK)
26
+ const TAG_SHIFT_BIG = BigInt(LAYOUT.TAG_SHIFT)
27
+ const AUX_SHIFT_BIG = BigInt(LAYOUT.AUX_SHIFT)
28
+ const SSO_BIT_BIG = BigInt(LAYOUT.SSO_BIT)
29
+
30
+ const heapGetIR = () => ctx.memory.shared
31
+ ? ['i32.load', ['i32.const', 1020]]
32
+ : ['global.get', '$__heap']
33
+
34
+ const heapSetIR = value => ctx.memory.shared
35
+ ? ['i32.store', ['i32.const', 1020], value]
36
+ : ['global.set', '$__heap', value]
37
+
38
+ const ARENA_SAFE_CALLS = new Set([
39
+ '$__alloc', '$__alloc_hdr', '$__mkptr',
40
+ '$__ptr_offset', '$__ptr_type', '$__ptr_aux',
41
+ '$__len', '$__cap', '$__typed_shift', '$__typed_data',
42
+ ])
43
+
44
+ function applyArenaRewind(func, fn, safeCallees) {
45
+ if (ctx.transform.optimize?.arenaRewind === false) return false
46
+ if (func.raw || func.sig.params.length !== 0 || func.sig.results.length !== 1) return false
47
+ if (func.sig.ptrKind != null) return false
48
+ if (func.sig.results[0] === 'f64' && func.valResult !== VAL.NUMBER) return false
49
+ if (func.sig.results[0] !== 'f64' && func.sig.results[0] !== 'i32') return false
50
+
51
+ const bodyStart = findBodyStart(fn)
52
+ let hasAlloc = false
53
+ let unsafe = false
54
+ const scan = node => {
55
+ if (unsafe || !Array.isArray(node)) return
56
+ const op = node[0]
57
+ if (op === 'global.set' || op === 'return_call' || op === 'call_indirect' || op === 'call_ref') {
58
+ unsafe = true
59
+ return
60
+ }
61
+ if (op === 'call') {
62
+ const name = node[1]
63
+ if (name === '$__alloc' || name === '$__alloc_hdr') hasAlloc = true
64
+ if (!(safeCallees ?? ARENA_SAFE_CALLS).has(name)) {
65
+ unsafe = true
66
+ return
67
+ }
68
+ }
69
+ for (let i = 1; i < node.length; i++) scan(node[i])
70
+ }
71
+ for (let i = bodyStart; i < fn.length; i++) scan(fn[i])
72
+ if (unsafe || !hasAlloc) return false
73
+
74
+ let id = 0
75
+ const hasLocal = name => fn.some(n => Array.isArray(n) && n[0] === 'local' && n[1] === name)
76
+ while (hasLocal(`$${T}heap_save${id}`) || hasLocal(`$${T}arena_ret${id}`)) id++
77
+ const save = `$${T}heap_save${id}`
78
+ const ret = `$${T}arena_ret${id}`
79
+ const restore = () => heapSetIR(['local.get', save])
80
+ const resultType = func.sig.results[0]
81
+
82
+ const rewriteReturns = node => {
83
+ if (!Array.isArray(node)) return node
84
+ if (node[0] === 'return' && node.length > 1) {
85
+ return ['block',
86
+ ['result', resultType],
87
+ ['local.set', ret, node[1]],
88
+ restore(),
89
+ ['return', ['local.get', ret]],
90
+ ['unreachable']]
91
+ }
92
+ for (let i = 1; i < node.length; i++) node[i] = rewriteReturns(node[i])
93
+ return node
94
+ }
95
+
96
+ const endsWithReturn = fn.at(-1)?.[0] === 'return' || fn.at(-1)?.[0] === 'return_call'
97
+ for (let i = bodyStart; i < fn.length; i++) fn[i] = rewriteReturns(fn[i])
98
+ const newBodyStart = findBodyStart(fn)
99
+ fn.splice(newBodyStart, 0,
100
+ ['local', save, 'i32'],
101
+ ['local', ret, resultType],
102
+ ['local.set', save, heapGetIR()])
103
+ if (!endsWithReturn) {
104
+ const last = fn.pop()
105
+ fn.push(['local.set', ret, last], restore(), ['local.get', ret])
106
+ }
107
+ return true
108
+ }
109
+
110
+ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
111
+ ctx.func.locals = new Map()
112
+ ctx.func.repByLocal = null
113
+ ctx.func.boxed = new Map()
114
+ ctx.func.stack = []
115
+ ctx.func.current = { params: [], results: [] }
116
+ analyzeValTypes(ast)
117
+ const normalizeIR = ir => !ir?.length ? [] : Array.isArray(ir[0]) ? ir : [ir]
118
+
119
+ const moduleInits = []
120
+ if (ctx.module.moduleInits) {
121
+ for (const mi of ctx.module.moduleInits) {
122
+ analyzeValTypes(mi)
123
+ moduleInits.push(...normalizeIR(emit(mi)))
124
+ }
125
+ }
126
+ const init = emit(ast)
127
+
128
+ // Module-scope object literals can create closure bodies while `emit(ast)`
129
+ // runs. Those late closures may pull in stdlib helpers (notably JSON.parse)
130
+ // that affect __start setup, so flush them before deciding which runtime
131
+ // tables __start must initialize. Restore the start-function context after
132
+ // compiling closure bodies; emitClosureBody owns ctx.func.* while it runs.
133
+ const beforeLateClosures = closureFuncs.length
134
+ const startCtx = {
135
+ locals: ctx.func.locals,
136
+ repByLocal: ctx.func.repByLocal,
137
+ boxed: ctx.func.boxed,
138
+ stack: ctx.func.stack,
139
+ current: ctx.func.current,
140
+ body: ctx.func.body,
141
+ directClosures: ctx.func.directClosures,
142
+ preboxed: ctx.func.preboxed,
143
+ localProps: ctx.func.localProps,
144
+ uniq: ctx.func.uniq,
145
+ refinements: ctx.func.refinements,
146
+ }
147
+ compilePendingClosures()
148
+ Object.assign(ctx.func, startCtx)
149
+
150
+ const boxInit = []
151
+ if (ctx.schema.autoBox) {
152
+ const bt = `${T}box`
153
+ ctx.func.locals.set(bt, 'i32')
154
+ for (const [name, { schemaId, schema }] of ctx.schema.autoBox) {
155
+ inc('__alloc_hdr', '__mkptr')
156
+ boxInit.push(
157
+ ['local.set', `$${bt}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)], ['i32.const', 8]]],
158
+ ['f64.store', ['local.get', `$${bt}`],
159
+ ctx.func.names.has(name) ? ['f64.const', 0] : ['global.get', `$${name}`]],
160
+ ...schema.slice(1).map((_, i) =>
161
+ ['f64.store', ['i32.add', ['local.get', `$${bt}`], ['i32.const', (i + 1) * 8]], ['f64.const', 0]]),
162
+ ['global.set', `$${name}`, mkPtrIR(PTR.OBJECT, schemaId, ['local.get', `$${bt}`])])
163
+ }
164
+ }
165
+
166
+ const schemaInit = []
167
+ const hasJpObj = ctx.core.includes.has('__jp_obj') || ctx.core.includes.has('__jp')
168
+ const needsSchemaTbl = (ctx.schema.list.length && (
169
+ ctx.core.includes.has('__stringify') ||
170
+ ctx.core.includes.has('__dyn_get') ||
171
+ ctx.core.includes.has('__dyn_get_t') ||
172
+ ctx.core.includes.has('__dyn_get_t_h') ||
173
+ ctx.core.includes.has('__dyn_get_expr_t_h') ||
174
+ ctx.core.includes.has('__dyn_get_any') ||
175
+ ctx.core.includes.has('__dyn_get_any_t') ||
176
+ ctx.core.includes.has('__dyn_get_expr') ||
177
+ ctx.core.includes.has('__dyn_get_expr_t') ||
178
+ ctx.core.includes.has('__dyn_get_or'))) ||
179
+ hasJpObj
180
+ if (needsSchemaTbl) {
181
+ const nSchemas = ctx.schema.list.length
182
+ const runtimeReserve = hasJpObj ? 256 : 0
183
+ const stbl = `${T}stbl`
184
+ const sarr = `${T}sarr`
185
+ ctx.func.locals.set(stbl, 'i32')
186
+ ctx.func.locals.set(sarr, 'i32')
187
+ inc('__alloc', '__alloc_hdr', '__mkptr')
188
+ schemaInit.push(
189
+ ['local.set', `$${stbl}`, ['call', '$__alloc', ['i32.const', (nSchemas + runtimeReserve) * 8]]],
190
+ ['global.set', '$__schema_tbl', ['local.get', `$${stbl}`]])
191
+ if (runtimeReserve) {
192
+ schemaInit.push(['global.set', '$__schema_next', ['i32.const', nSchemas]])
193
+ }
194
+ for (let s = 0; s < nSchemas; s++) {
195
+ const keys = ctx.schema.list[s]
196
+ const n = keys.length
197
+ schemaInit.push(
198
+ ['local.set', `$${sarr}`, ['call', '$__alloc_hdr', ['i32.const', n], ['i32.const', n], ['i32.const', 8]]])
199
+ for (let k = 0; k < n; k++)
200
+ schemaInit.push(
201
+ ['f64.store', ['i32.add', ['local.get', `$${sarr}`], ['i32.const', k * 8]],
202
+ emit(['str', String(keys[k])])])
203
+ schemaInit.push(
204
+ ['f64.store', ['i32.add', ['local.get', `$${stbl}`], ['i32.const', s * 8]],
205
+ mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${sarr}`])])
206
+ }
207
+ }
208
+
209
+ const strPoolInit = []
210
+ if (ctx.runtime.strPool) {
211
+ const total = ctx.runtime.strPool.length
212
+ strPoolInit.push(
213
+ ['global.set', '$__strBase', ['call', '$__alloc', ['i32.const', total]]],
214
+ ['memory.init', '$__strPool', ['global.get', '$__strBase'], ['i32.const', 0], ['i32.const', total]],
215
+ ['data.drop', '$__strPool'],
216
+ )
217
+ }
218
+
219
+ const typeofInit = []
220
+ if (ctx.runtime.typeofStrs) {
221
+ for (const s of ctx.runtime.typeofStrs)
222
+ typeofInit.push(['global.set', `$__tof_${s}`, emit(['str', s])])
223
+ }
224
+ const wasiTimers = ctx.features.timers && ctx.transform.host === 'wasi'
225
+ if (moduleInits.length || init?.length || boxInit.length || schemaInit.length || typeofInit.length || strPoolInit.length || wasiTimers) {
226
+ const initIR = normalizeIR(init)
227
+ const startFn = ['func', '$__start']
228
+ for (const [l, t] of ctx.func.locals) startFn.push(['local', `$${l}`, t])
229
+ startFn.push(...strPoolInit, ...typeofInit, ...boxInit, ...schemaInit,
230
+ ...(wasiTimers ? [['call', '$__timer_init']] : []),
231
+ ...moduleInits, ...initIR,
232
+ ...(ctx.features.blockingTimers ? [['call', '$__timer_loop']] : []),
233
+ )
234
+ sec.start.push(startFn, ['start', '$__start'])
235
+ }
236
+
237
+ compilePendingClosures()
238
+ if (closureFuncs.length > beforeLateClosures)
239
+ sec.funcs.unshift(...closureFuncs.slice(beforeLateClosures))
240
+ }
241
+
242
+ /**
243
+ * Phase: closure-body dedup.
244
+ *
245
+ * Two closures with structurally-equal bodies (same shape after alpha-renaming
246
+ * locals/params) are emitted as a single function — duplicates redirect through
247
+ * the elem table to the canonical name. Closure bodies often share shape because
248
+ * the same inner arrow can be instantiated in many places (e.g. parser combinators).
249
+ */
250
+ export function dedupClosureBodies(closureFuncs, sec) {
251
+ if (closureFuncs.length <= 1) return
252
+ const canonicalize = (fn) => {
253
+ const localNames = new Set()
254
+ const collect = (node) => {
255
+ if (!Array.isArray(node)) return
256
+ if ((node[0] === 'local' || node[0] === 'param') && typeof node[1] === 'string' && node[1][0] === '$')
257
+ localNames.add(node[1])
258
+ for (const c of node) collect(c)
259
+ }
260
+ collect(fn)
261
+ let counter = 0
262
+ const renameMap = new Map()
263
+ const walk = node => {
264
+ if (typeof node === 'string') {
265
+ if (!localNames.has(node)) return node
266
+ let r = renameMap.get(node)
267
+ if (!r) { r = `$_c${counter++}`; renameMap.set(node, r) }
268
+ return r
269
+ }
270
+ if (!Array.isArray(node)) return node
271
+ return node.map(walk)
272
+ }
273
+ return JSON.stringify(['func', ...fn.slice(2).map(walk)])
274
+ }
275
+ const hashToName = new Map()
276
+ const redirect = new Map()
277
+ const keepSet = new Set()
278
+ for (const fn of closureFuncs) {
279
+ const key = canonicalize(fn)
280
+ const name = fn[1].slice(1)
281
+ const canonical = hashToName.get(key)
282
+ if (canonical) redirect.set(name, canonical)
283
+ else { hashToName.set(key, name); keepSet.add(name) }
284
+ }
285
+ if (!redirect.size) return
286
+ const kept = sec.funcs.filter(fn => {
287
+ if (!Array.isArray(fn) || fn[0] !== 'func') return true
288
+ const name = typeof fn[1] === 'string' && fn[1][0] === '$' ? fn[1].slice(1) : null
289
+ return !name || !redirect.has(name)
290
+ })
291
+ const redirectRefs = node => {
292
+ if (typeof node === 'string') return node[0] === '$' && redirect.has(node.slice(1)) ? `$${redirect.get(node.slice(1))}` : node
293
+ if (!Array.isArray(node)) return node
294
+ for (let i = 0; i < node.length; i++) node[i] = redirectRefs(node[i])
295
+ return node
296
+ }
297
+ for (const fn of kept) redirectRefs(fn)
298
+ ctx.closure.table = ctx.closure.table.map(n => redirect.get(n) || n)
299
+ sec.funcs.length = 0
300
+ sec.funcs.push(...kept)
301
+ }
302
+
303
+ /**
304
+ * Phase: closure-table finalize + ABI shrink.
305
+ */
306
+ export function finalizeClosureTable(sec) {
307
+ let indirectUsed = ctx.transform.host === 'wasi'
308
+ const scan = (n) => {
309
+ if (!Array.isArray(n) || indirectUsed) return
310
+ if (n[0] === 'call_indirect') { indirectUsed = true; return }
311
+ for (const c of n) if (Array.isArray(c)) scan(c)
312
+ }
313
+ for (const fn of sec.funcs) { scan(fn); if (indirectUsed) break }
314
+ if (!indirectUsed) for (const fn of sec.start) scan(fn)
315
+ if (!indirectUsed) for (const s of Object.keys(ctx.core.stdlib)) {
316
+ if (ctx.core.stdlib[s]?.includes?.('call_indirect')) { indirectUsed = true; break }
317
+ }
318
+ if (indirectUsed) {
319
+ if (!ctx.closure.table) ctx.closure.table = []
320
+ sec.table = [['table', ['export', '"__jz_table"'], ctx.closure.table.length, 'funcref']]
321
+ sec.elem = ctx.closure.table.length ? [['elem', ['i32.const', 0], 'func', ...ctx.closure.table.map(n => `$${n}`)]] : []
322
+ return
323
+ }
324
+ sec.table = []
325
+ sec.elem = []
326
+ sec.types = sec.types.filter(t => !(Array.isArray(t) && t[1] === '$ftN'))
327
+ const W = ctx.closure.width ?? MAX_CLOSURE_ARITY
328
+ const abiOf = new Map()
329
+ for (const cb of (ctx.closure.bodies || [])) {
330
+ const fixedN = cb.params.length - (cb.rest ? 1 : 0)
331
+ abiOf.set(cb.name, {
332
+ needEnv: cb.captures.length > 0,
333
+ needArgc: !!cb.rest,
334
+ usedSlots: cb.rest ? W : fixedN,
335
+ rest: !!cb.rest,
336
+ })
337
+ }
338
+ for (const fn of sec.funcs) {
339
+ if (!Array.isArray(fn) || fn[0] !== 'func') continue
340
+ const fnName = typeof fn[1] === 'string' && fn[1][0] === '$' ? fn[1].slice(1) : null
341
+ const abi = abiOf.get(fnName)
342
+ if (!abi) continue
343
+ for (let i = fn.length - 1; i >= 0; i--) {
344
+ const node = fn[i]
345
+ if (!Array.isArray(node) || node[0] !== 'param') continue
346
+ const pname = node[1]
347
+ if (pname === '$__env' && !abi.needEnv) fn.splice(i, 1)
348
+ else if (pname === '$__argc' && !abi.needArgc) fn.splice(i, 1)
349
+ else if (typeof pname === 'string' && pname.startsWith('$__a') && !abi.rest) {
350
+ const idx = parseInt(pname.slice(4), 10)
351
+ if (Number.isFinite(idx) && idx >= abi.usedSlots) fn.splice(i, 1)
352
+ }
353
+ }
354
+ }
355
+ const rewriteCalls = (node) => {
356
+ if (!Array.isArray(node)) return
357
+ for (const c of node) if (Array.isArray(c)) rewriteCalls(c)
358
+ if ((node[0] === 'call' || node[0] === 'return_call') && typeof node[1] === 'string') {
359
+ const callee = node[1].slice(1)
360
+ const abi = abiOf.get(callee)
361
+ if (!abi) return
362
+ const newArgs = []
363
+ if (abi.needEnv) newArgs.push(node[2])
364
+ if (abi.needArgc) newArgs.push(node[3])
365
+ for (let i = 0; i < abi.usedSlots; i++) newArgs.push(node[4 + i])
366
+ node.splice(2, node.length - 2, ...newArgs)
367
+ }
368
+ }
369
+ for (const fn of sec.funcs) rewriteCalls(fn)
370
+ for (const fn of sec.start) rewriteCalls(fn)
371
+ }
372
+
373
+ /**
374
+ * Phase: pull stdlib + memory.
375
+ */
376
+ export function pullStdlib(sec) {
377
+ resolveIncludes()
378
+
379
+ const needsMemory = [...ctx.core.includes].some(n => ctx.core.stdlib[n] && MEM_OPS.test(ctx.core.stdlib[n]))
380
+ if (!needsMemory) ctx.scope.globals.delete('__heap')
381
+ if (needsMemory && ctx.module.modules.core) {
382
+ for (const fn of ['__alloc', '__alloc_hdr', '__clear']) if (!ctx.core.includes.has(fn)) ctx.core.includes.add(fn)
383
+ const pages = ctx.memory.pages || 1
384
+ if (ctx.memory.shared) sec.imports.push(['import', '"env"', '"memory"', ['memory', pages]])
385
+ else sec.memory.push(['memory', ['export', '"memory"'], pages])
386
+ if (ctx.transform.alloc !== false && ctx.core._allocRawFuncs)
387
+ sec.funcs.push(...ctx.core._allocRawFuncs.map(s => parseWat(s)))
388
+ }
389
+
390
+ const stdlibStr = (name) => {
391
+ const v = ctx.core.stdlib[name]
392
+ return typeof v === 'function' ? v() : v
393
+ }
394
+ ctx.core.extImports ??= new Set()
395
+ for (const name of Object.keys(ctx.core.stdlib)) {
396
+ if (name.startsWith('__ext_') && ctx.core.includes.has(name)) {
397
+ const parsed = parseWat(stdlibStr(name))
398
+ sec.extStdlib.push(parsed[0] === "module" ? parsed[1] : parsed)
399
+ ctx.core.extImports.add(name)
400
+ ctx.core.includes.delete(name)
401
+ }
402
+ }
403
+ for (const n of ctx.core.includes) if (!ctx.core.stdlib[n]) console.error("MISSING stdlib:", n)
404
+ sec.stdlib.push(...[...ctx.core.includes].map(n => parseWat(stdlibStr(n))))
405
+ }
406
+
407
+ export function syncImports(sec) {
408
+ for (const imp of ctx.module.imports) {
409
+ if (!sec.imports.some(i => i[1] === imp[1] && i[2] === imp[2])) sec.imports.push(imp)
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Phase: whole-module + per-function optimization passes.
415
+ */
416
+ export function optimizeModule(sec) {
417
+ const cfg = ctx.transform.optimize
418
+ if (!cfg || cfg.specializeMkptr !== false)
419
+ specializeMkptr([...sec.funcs, ...sec.stdlib, ...sec.start], wat => sec.stdlib.push(parseWat(wat)), parseWat)
420
+ if (!cfg || cfg.specializePtrBase !== false)
421
+ specializePtrBase([...sec.funcs, ...sec.stdlib, ...sec.start], wat => sec.stdlib.push(parseWat(wat)), parseWat)
422
+ if (ctx.runtime.strPool && (!cfg || cfg.sortStrPoolByFreq !== false)) {
423
+ const poolRef = { pool: ctx.runtime.strPool }
424
+ sortStrPoolByFreq([...sec.funcs, ...sec.stdlib, ...sec.start], poolRef, ctx.runtime.strPoolDedup)
425
+ ctx.runtime.strPool = poolRef.pool
426
+ }
427
+ // Backfill globalTypes for runtime globals declared only in ctx.scope.globals
428
+ // (e.g., __schema_tbl, __strBase). Parses the WAT string to infer i32/f64/i64.
429
+ if (ctx.scope.globals) {
430
+ for (const [name, wat] of ctx.scope.globals) {
431
+ if (!wat || ctx.scope.globalTypes.has(name)) continue
432
+ const m = wat.match(/\(global\s+\$?\S+\s+(?:\(mut\s+)?(i32|i64|f64|f32)/)
433
+ if (m) ctx.scope.globalTypes.set(name, m[1])
434
+ }
435
+ }
436
+ // Build global name→type map from ctx.scope.globalTypes (keys without $) for promoteGlobals
437
+ const globalTypesMap = ctx.scope.globalTypes ? new Map([...ctx.scope.globalTypes].map(([k, v]) => [`$${k}`, v])) : null
438
+ for (const s of [...sec.funcs, ...sec.stdlib, ...sec.start]) optimizeFunc(s, cfg, globalTypesMap)
439
+ if (!cfg || cfg.arenaRewind !== false) {
440
+ const safeCallees = arenaRewindModule([...sec.funcs, ...sec.stdlib, ...sec.start])
441
+ const fnByName = new Map()
442
+ for (const fn of sec.funcs) {
443
+ if (Array.isArray(fn) && fn[0] === 'func' && typeof fn[1] === 'string')
444
+ fnByName.set(fn[1], fn)
445
+ }
446
+ for (const func of ctx.func.list) {
447
+ const fn = fnByName.get(`$${func.name}`)
448
+ if (fn) applyArenaRewind(func, fn, safeCallees)
449
+ }
450
+ }
451
+ if (!cfg || cfg.hoistConstantPool !== false)
452
+ hoistConstantPool([...sec.funcs, ...sec.stdlib, ...sec.start], (name, wat) => ctx.scope.globals.set(name, wat))
453
+
454
+ // Second promoteGlobals pass disabled: promoting hoistConstantPool's __fc*
455
+ // globals regressed the watr perf micro-pin (WASM compile time increased).
456
+ // The __fc* globals are typically read 3-4 times; the local setup overhead
457
+ // in large functions outweighs the per-read savings. Left as a no-op hook
458
+ // in case future analysis finds a profitable threshold or function-size gate.
459
+ // if (!cfg || cfg.promoteGlobals !== false) {
460
+ // const globalTypesMap2 = ctx.scope.globalTypes ? new Map([...ctx.scope.globalTypes].map(([k, v]) => [`$${k}`, v])) : null
461
+ // for (const s of [...sec.funcs, ...sec.stdlib, ...sec.start]) promoteGlobals(s, globalTypesMap2)
462
+ // }
463
+
464
+ const dataLen = ctx.runtime.data?.length || 0
465
+ if (dataLen > 1024 && !ctx.memory.shared) {
466
+ const heapBase = (dataLen + 7) & ~7
467
+ ctx.scope.globals.set('__heap', `(global $__heap (mut i32) (i32.const ${heapBase}))`)
468
+ ctx.scope.globalTypes.set('__heap', 'i32')
469
+ if (ctx.scope.globals.has('__heap_start')) {
470
+ ctx.scope.globals.set('__heap_start', `(global $__heap_start (mut i32) (i32.const ${heapBase}))`)
471
+ ctx.scope.globalTypes.set('__heap_start', 'i32')
472
+ }
473
+ for (const s of sec.stdlib)
474
+ if (s[0] === 'func' && s[1] === '$__clear')
475
+ for (let i = 2; i < s.length; i++)
476
+ if (Array.isArray(s[i]) && s[i][0] === 'global.set' && Array.isArray(s[i][2]) && s[i][2][0] === 'i32.const')
477
+ s[i][2][1] = `${heapBase}`
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Phase: strip static-data prefix.
483
+ */
484
+ export function stripStaticDataPrefix(sec) {
485
+ if (!ctx.runtime.staticDataLen || ctx.core.includes.has('__static_str')) return
486
+ const prefix = ctx.runtime.staticDataLen
487
+ const SHIFTABLE = new Set([PTR.STRING, PTR.OBJECT, PTR.ARRAY, PTR.HASH, PTR.SET, PTR.MAP, PTR.BUFFER, PTR.TYPED, PTR.CLOSURE])
488
+ const data = ctx.runtime.data || ''
489
+ const buf = new Uint8Array(data.length)
490
+ for (let i = 0; i < data.length; i++) buf[i] = data.charCodeAt(i)
491
+ const dv = new DataView(buf.buffer)
492
+ if (ctx.runtime.staticPtrSlots) {
493
+ for (const slotOff of ctx.runtime.staticPtrSlots) {
494
+ if (slotOff < prefix) continue
495
+ const bits = dv.getBigUint64(slotOff, true)
496
+ if (((bits >> 48n) & 0xFFF8n) !== NAN_PREFIX) continue
497
+ const ty = Number((bits >> TAG_SHIFT_BIG) & TAG_MASK_BIG)
498
+ if (!SHIFTABLE.has(ty)) continue
499
+ if (ty === PTR.STRING && ((bits >> AUX_SHIFT_BIG) & SSO_BIT_BIG)) continue
500
+ const off = Number(bits & OFFSET_MASK_BIG)
501
+ if (off < prefix) continue
502
+ const hi = bits & ~OFFSET_MASK_BIG
503
+ dv.setBigUint64(slotOff, hi | BigInt(off - prefix), true)
504
+ }
505
+ }
506
+ let s = ''
507
+ for (let i = prefix; i < buf.length; i++) s += String.fromCharCode(buf[i])
508
+ ctx.runtime.data = s
509
+ if (ctx.runtime.staticPtrSlots) ctx.runtime.staticPtrSlots = ctx.runtime.staticPtrSlots
510
+ .filter(o => o >= prefix).map(o => o - prefix)
511
+ const shift = (node) => {
512
+ if (!Array.isArray(node)) return
513
+ for (let i = 0; i < node.length; i++) {
514
+ const child = node[i]
515
+ if (!Array.isArray(child)) continue
516
+ if (child[0] === 'call' && child[1] === '$__mkptr' &&
517
+ Array.isArray(child[2]) && SHIFTABLE.has(child[2][1]) &&
518
+ Array.isArray(child[4]) && child[4][0] === 'i32.const' &&
519
+ typeof child[4][1] === 'number' && child[4][1] >= prefix) {
520
+ const isSsoString = child[2][1] === PTR.STRING &&
521
+ Array.isArray(child[3]) && child[3][0] === 'i32.const' &&
522
+ typeof child[3][1] === 'number' && (child[3][1] & LAYOUT.SSO_BIT)
523
+ if (!isSsoString) child[4][1] -= prefix
524
+ } else if (child[0] === 'f64.const' &&
525
+ typeof child[1] === 'string' && child[1].startsWith('nan:0x')) {
526
+ const bits = BigInt(child[1].slice(4)) | 0x7FF0000000000000n
527
+ if (((bits >> 48n) & 0xFFF8n) === NAN_PREFIX) {
528
+ const ty = Number((bits >> TAG_SHIFT_BIG) & TAG_MASK_BIG)
529
+ if (SHIFTABLE.has(ty) &&
530
+ !(ty === PTR.STRING && ((bits >> AUX_SHIFT_BIG) & SSO_BIT_BIG))) {
531
+ const off = Number(bits & OFFSET_MASK_BIG)
532
+ if (off >= prefix) {
533
+ const hi = bits & ~OFFSET_MASK_BIG
534
+ const newBits = hi | BigInt(off - prefix)
535
+ child[1] = 'nan:0x' + newBits.toString(16).toUpperCase().padStart(16, '0')
536
+ }
537
+ }
538
+ }
539
+ }
540
+ shift(child)
541
+ }
542
+ }
543
+ for (const s of [...sec.funcs, ...sec.stdlib, ...sec.start]) shift(s)
544
+ }