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.
- package/README.md +315 -318
- package/bench/README.md +369 -0
- package/bench/bench.svg +102 -0
- package/cli.js +104 -30
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +362 -84
- package/interop.js +159 -186
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +184 -0
- package/module/array.js +377 -176
- package/module/collection.js +639 -145
- package/module/console.js +55 -43
- package/module/core.js +264 -153
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +551 -187
- package/module/number.js +327 -60
- package/module/object.js +474 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +117 -0
- package/module/string.js +621 -226
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +459 -73
- package/package.json +58 -14
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +29 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +824 -0
- package/src/compile/analyze.js +1600 -0
- package/src/compile/emit-assign.js +411 -0
- package/src/compile/emit.js +3512 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +778 -128
- package/src/{infer.js → compile/infer.js} +62 -98
- package/src/compile/loop-divmod.js +155 -0
- package/src/{narrow.js → compile/narrow.js} +409 -106
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +370 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +122 -0
- package/src/compile/plan/inline.js +682 -0
- package/src/compile/plan/literals.js +1199 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +649 -0
- package/src/compile/program-facts.js +423 -0
- package/src/ctx.js +220 -64
- package/src/ir.js +589 -172
- package/src/kind-traits.js +132 -0
- package/src/kind.js +524 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1432 -473
- package/src/optimize/vectorize.js +4660 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +649 -205
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +116 -0
- package/src/resolve.js +12 -3
- package/src/static.js +208 -0
- package/src/type.js +651 -0
- package/src/{assemble.js → wat/assemble.js} +275 -55
- package/src/wat/optimize.js +3938 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -3040
- package/src/jzify.js +0 -1580
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* @module assemble
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import
|
|
16
|
-
import { ctx, inc, resolveIncludes, err, PTR, LAYOUT } from '
|
|
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
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
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[
|
|
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',
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
346
|
-
|
|
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
|
-
|
|
410
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
506
|
-
ctx.scope.
|
|
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
|
|
534
|
-
if (((
|
|
535
|
-
const ty =
|
|
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 && ((
|
|
538
|
-
const off =
|
|
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
|
-
|
|
541
|
-
|
|
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
|