jz 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +27 -27
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6178
- package/index.js +165 -74
- package/interop.js +189 -17
- package/jzify/arguments.js +32 -1
- package/jzify/async.js +409 -0
- package/jzify/classes.js +136 -18
- package/jzify/generators.js +639 -0
- package/jzify/hoist-vars.js +73 -1
- package/jzify/index.js +123 -4
- package/jzify/names.js +1 -0
- package/jzify/transform.js +239 -1
- package/layout.js +48 -3
- package/module/array.js +318 -43
- package/module/atomics.js +144 -0
- package/module/collection.js +1429 -155
- package/module/core.js +431 -40
- package/module/date.js +142 -120
- package/module/fs.js +144 -0
- package/module/function.js +6 -3
- package/module/index.js +4 -1
- package/module/json.js +270 -49
- package/module/math.js +1032 -145
- package/module/number.js +532 -163
- package/module/object.js +353 -95
- package/module/regex.js +157 -7
- package/module/schema.js +100 -2
- package/module/string.js +428 -93
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +82 -11
- package/src/ast.js +11 -5
- package/src/autoload.js +28 -1
- package/src/bridge.js +7 -2
- package/src/compile/analyze-scans.js +22 -7
- package/src/compile/analyze.js +136 -30
- package/src/compile/dyn-closure-tables.js +277 -0
- package/src/compile/emit-assign.js +281 -15
- package/src/compile/emit.js +1055 -70
- package/src/compile/index.js +277 -29
- package/src/compile/infer.js +42 -4
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/narrow.js +555 -18
- package/src/compile/peel-stencil.js +5 -0
- package/src/compile/plan/index.js +45 -3
- package/src/compile/plan/inline.js +8 -1
- package/src/compile/plan/literals.js +39 -0
- package/src/compile/plan/scope.js +430 -23
- package/src/compile/program-facts.js +732 -38
- package/src/ctx.js +64 -9
- package/src/helper-counters.js +7 -1
- package/src/ir.js +113 -5
- package/src/kind-traits.js +66 -3
- package/src/kind.js +84 -12
- package/src/op-policy.js +7 -4
- package/src/optimize/index.js +1179 -704
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +982 -67
- package/src/prepare/index.js +809 -67
- package/src/prepare/math-kernel.js +331 -0
- package/src/prepare/pre-eval.js +714 -0
- package/src/snapshot.js +194 -0
- package/src/type.js +1170 -56
- package/src/wat/assemble.js +403 -65
- package/src/wat/codegen.js +74 -14
- package/transform.js +113 -4
- package/wasi.js +3 -0
package/src/wat/assemble.js
CHANGED
|
@@ -21,7 +21,7 @@ import { ctx, inc, resolveIncludes, err, PTR, LAYOUT, HEAP, declGlobal } from '.
|
|
|
21
21
|
// it re-tokenizes ~KB of text every compile. Parse once per distinct resolved
|
|
22
22
|
// string, then hand out a deep clone (downstream passes mutate nodes in place).
|
|
23
23
|
// Module-level on purpose: the cache persists across compile() calls.
|
|
24
|
-
|
|
24
|
+
let stdlibParseCache = new Map() // resolved WAT string → pristine parsed tree
|
|
25
25
|
const cloneTemplate = (node) => {
|
|
26
26
|
if (!Array.isArray(node)) return node
|
|
27
27
|
const copy = node.map(cloneTemplate)
|
|
@@ -33,12 +33,18 @@ const parseTemplate = (str) => {
|
|
|
33
33
|
if (tmpl === undefined) stdlibParseCache.set(str, tmpl = parseWat(str))
|
|
34
34
|
return cloneTemplate(tmpl)
|
|
35
35
|
}
|
|
36
|
+
// Self-host-only: see clearDollar (src/ir.js) — same dangling-arena-pointer hazard,
|
|
37
|
+
// and the same fix: swap in a fresh Map, don't just `.clear()` the old one (its
|
|
38
|
+
// backing table is itself an arena allocation `_clear` invalidates). Must run every
|
|
39
|
+
// compile in a warm-instance loop (see scripts/self.js setupSelf).
|
|
40
|
+
export const clearStdlibParseCache = () => { stdlibParseCache = new Map() }
|
|
36
41
|
import { T } from '../ast.js'
|
|
37
42
|
import { analyzeValTypes, analyzeBody } from '../compile/analyze.js'
|
|
38
43
|
import { VAL } from '../reps.js'
|
|
39
|
-
import { optimizeFunc, collectVolatileGlobals, collectReachableGlobalWrites, hoistGlobalPtrOffset, stablePtrGlobalNames, hoistConstantPool, specializeMkptr,
|
|
44
|
+
import { optimizeFunc, collectVolatileGlobals, collectReachableGlobalWrites, hoistGlobalPtrOffset, hoistLoopGlobalPtrOffset, stablePtrGlobalNames, hoistConstantPool, specializeMkptr, arenaRewindModule, buildPureFuncMap, inlinePureFnsInFn } from '../optimize/index.js'
|
|
40
45
|
import { emit, emitVoid } from '../compile/emit.js'
|
|
41
|
-
import { mkPtrIR, MAX_CLOSURE_ARITY, MEM_OPS, findBodyStart } from '../ir.js'
|
|
46
|
+
import { mkPtrIR, MAX_CLOSURE_ARITY, MEM_OPS, findBodyStart, extractF64Bits, asF64, appendStaticSlots } from '../ir.js'
|
|
47
|
+
import { staticArrayPtr } from '../../module/array.js'
|
|
42
48
|
import { installHelperCounters, instrumentHelperCounter } from '../helper-counters.js'
|
|
43
49
|
|
|
44
50
|
// NaN-prefix top-13-bits as BigInt — used by the static-prefix-strip pass
|
|
@@ -105,15 +111,20 @@ function applyArenaRewind(func, fn, safeCallees) {
|
|
|
105
111
|
const restore = () => heapSetIR(['local.get', save])
|
|
106
112
|
const resultType = func.sig.results[0]
|
|
107
113
|
|
|
114
|
+
// Rewrite the return's VALUE, not the return: `return` is stack-polymorphic
|
|
115
|
+
// (never falls through), so it validates in statement AND value position alike.
|
|
116
|
+
// The old form — a value-typed block AROUND the return — reified `(result T)`
|
|
117
|
+
// even where the return was a statement, leaving a phantom value on the stack
|
|
118
|
+
// of a void enclosing frame (a `return` inside try_table failed validation:
|
|
119
|
+
// "expected 0 elements on the stack for fallthru, found 1").
|
|
108
120
|
const rewriteReturns = node => {
|
|
109
121
|
if (!Array.isArray(node)) return node
|
|
110
122
|
if (node[0] === 'return' && node.length > 1) {
|
|
111
|
-
return ['block',
|
|
123
|
+
return ['return', ['block',
|
|
112
124
|
['result', resultType],
|
|
113
125
|
['local.set', ret, node[1]],
|
|
114
126
|
restore(),
|
|
115
|
-
['
|
|
116
|
-
['unreachable']]
|
|
127
|
+
['local.get', ret]]]
|
|
117
128
|
}
|
|
118
129
|
for (let i = 1; i < node.length; i++) node[i] = rewriteReturns(node[i])
|
|
119
130
|
return node
|
|
@@ -222,6 +233,8 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
222
233
|
// table holding only empty schemas is pure dead weight there. __json_obj has
|
|
223
234
|
// no such guard — it must read the table whenever stringify is in play.
|
|
224
235
|
const tblConsumed = hasStringify ||
|
|
236
|
+
// Inline readers (object.js enumeration scaffolds) — no named helper to count.
|
|
237
|
+
ctx.runtime.schemaTblConsumed ||
|
|
225
238
|
ctx.core.includes.has('__obj_clone') ||
|
|
226
239
|
ctx.core.includes.has('__dyn_get') ||
|
|
227
240
|
ctx.core.includes.has('__dyn_get_t') ||
|
|
@@ -245,29 +258,63 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
245
258
|
if (needsSchemaTbl) {
|
|
246
259
|
const nSchemas = ctx.schema.list.length
|
|
247
260
|
const runtimeReserve = hasJpObj ? 256 : 0
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (
|
|
257
|
-
|
|
261
|
+
// Pre-eval tier 2: the schema NAME TABLE is compile-time data — every key is a
|
|
262
|
+
// static string literal, every keys-array a constant, the table an array of
|
|
263
|
+
// constant boxed pointers. Lay it out in the data segment (staticArrayPtr per
|
|
264
|
+
// schema + one slot run for the table incl. a zeroed runtime reserve) and bake
|
|
265
|
+
// __schema_tbl/__schema_next as GLOBAL INITS: zero init code, and the reserve
|
|
266
|
+
// (JSON.parse-registered runtime schemas) lives in writable data the globals
|
|
267
|
+
// sweep correctly rewinds at _clear (runtime schemas are round state).
|
|
268
|
+
let staticBits = ctx.memory.shared ? null : []
|
|
269
|
+
if (staticBits) for (const keys of ctx.schema.list) {
|
|
270
|
+
const bits = keys.map(k => extractF64Bits(asF64(emit(['str', String(k)]))))
|
|
271
|
+
if (bits.some(b => b == null)) { staticBits = null; break }
|
|
272
|
+
staticBits.push(extractF64Bits(staticArrayPtr(bits)))
|
|
258
273
|
}
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
274
|
+
if (staticBits) {
|
|
275
|
+
const tblOff = appendStaticSlots([...staticBits, ...Array(runtimeReserve).fill('0x0000000000000000')])
|
|
276
|
+
// The consumers declGlobal '__schema_tbl' lazily at TEMPLATE EXPANSION
|
|
277
|
+
// (pullStdlib) — AFTER this runs. Declare it here so the static offset
|
|
278
|
+
// lands as the initializer instead of silently missing a not-yet-declared
|
|
279
|
+
// global (which then defaulted 0 and the `$__schema_tbl == 0` guards
|
|
280
|
+
// disabled the whole schema arm — for-in enumerated zero keys).
|
|
281
|
+
if (!ctx.scope.globals.has('__schema_tbl')) declGlobal('__schema_tbl', 'i32')
|
|
282
|
+
const tblG = ctx.scope.globals.get('__schema_tbl')
|
|
283
|
+
if (tblG) tblG.init = tblOff
|
|
284
|
+
// Raw-i32 global init pointing into static data: stripStaticDataPrefix
|
|
285
|
+
// patches BOXED slots via staticPtrSlots, but a global's declared init
|
|
286
|
+
// needs its own shift (same as __internBase's bespoke re-declare).
|
|
287
|
+
;(ctx.runtime.staticI32GlobalInits ??= []).push('__schema_tbl')
|
|
288
|
+
if (runtimeReserve) {
|
|
289
|
+
if (!ctx.scope.globals.has('__schema_next')) declGlobal('__schema_next', 'i32')
|
|
290
|
+
const nextG = ctx.scope.globals.get('__schema_next')
|
|
291
|
+
if (nextG) nextG.init = nSchemas
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
const stbl = `${T}stbl`
|
|
295
|
+
const sarr = `${T}sarr`
|
|
296
|
+
ctx.func.locals.set(stbl, 'i32')
|
|
297
|
+
ctx.func.locals.set(sarr, 'i32')
|
|
298
|
+
inc('__alloc', '__alloc_hdr', '__mkptr')
|
|
262
299
|
schemaInit.push(
|
|
263
|
-
['local.set', `$${
|
|
264
|
-
|
|
300
|
+
['local.set', `$${stbl}`, ['call', '$__alloc', ['i32.const', (nSchemas + runtimeReserve) * 8]]],
|
|
301
|
+
['global.set', '$__schema_tbl', ['local.get', `$${stbl}`]])
|
|
302
|
+
if (runtimeReserve) {
|
|
303
|
+
schemaInit.push(['global.set', '$__schema_next', ['i32.const', nSchemas]])
|
|
304
|
+
}
|
|
305
|
+
for (let s = 0; s < nSchemas; s++) {
|
|
306
|
+
const keys = ctx.schema.list[s]
|
|
307
|
+
const n = keys.length
|
|
265
308
|
schemaInit.push(
|
|
266
|
-
['
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
309
|
+
['local.set', `$${sarr}`, ['call', '$__alloc_hdr', ['i32.const', n], ['i32.const', n]]])
|
|
310
|
+
for (let k = 0; k < n; k++)
|
|
311
|
+
schemaInit.push(
|
|
312
|
+
['f64.store', ['i32.add', ['local.get', `$${sarr}`], ['i32.const', k * 8]],
|
|
313
|
+
emit(['str', String(keys[k])])])
|
|
314
|
+
schemaInit.push(
|
|
315
|
+
['f64.store', ['i32.add', ['local.get', `$${stbl}`], ['i32.const', s * 8]],
|
|
316
|
+
mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${sarr}`])])
|
|
317
|
+
}
|
|
271
318
|
}
|
|
272
319
|
}
|
|
273
320
|
|
|
@@ -339,8 +386,15 @@ export function hoistConstGlobalInits(sec) {
|
|
|
339
386
|
}
|
|
340
387
|
// Hoisting can empty `__start`. The O2 watr pass prunes a bodyless start, but at
|
|
341
388
|
// O0/O1 nothing else does — drop it (func + directive) here so a const-only module
|
|
342
|
-
// carries no start at all.
|
|
343
|
-
|
|
389
|
+
// carries no start at all. A body reduced to ONLY the `__heap_reset` capture
|
|
390
|
+
// (injected before this hoist ran) counts as empty too: with every init store
|
|
391
|
+
// folded away, nothing allocated, so the capture would store exactly the
|
|
392
|
+
// data-end seed `__heap_reset` already declares — tier-2 static-tree modules
|
|
393
|
+
// ship with no start section at all.
|
|
394
|
+
const bs = findBodyStart(startFn)
|
|
395
|
+
const captureOnly = startFn.length - bs === 1 &&
|
|
396
|
+
Array.isArray(startFn[bs]) && startFn[bs][0] === 'global.set' && startFn[bs][1] === '$__heap_reset'
|
|
397
|
+
if (bs >= startFn.length || captureOnly)
|
|
344
398
|
for (let j = sec.start.length - 1; j >= 0; j--)
|
|
345
399
|
if (Array.isArray(sec.start[j]) && sec.start[j][1] === '$__start') sec.start.splice(j, 1)
|
|
346
400
|
}
|
|
@@ -529,13 +583,20 @@ function reachableStdlib(sec) {
|
|
|
529
583
|
// (PPC_CALL2). These are the ONLY helpers appendLateStdlib may add; restricting to them avoids
|
|
530
584
|
// touching helpers that live in other module sections (ext-stdlib, imports) where a blind
|
|
531
585
|
// referenced-but-absent scan would wrongly re-append and duplicate them.
|
|
532
|
-
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'
|
|
586
|
+
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', 'math.cbrt_v', 'math.fifthroot_v',
|
|
587
|
+
// math.pow_fold (scalar) is normally eager-included by emitPow's own const-exponent fold (which
|
|
588
|
+
// always `inc()`s it before the vectorizer ever runs, under optimize.crPow — see module/math.js).
|
|
589
|
+
// It's ALSO listed here for the one path where that eager inc doesn't fire: a genuine runtime
|
|
590
|
+
// $math.pow(x,y) whose y is proven constant only during vectorization (vectorize.js's
|
|
591
|
+
// `$math.pow(x,c)` lift) — that rewrite calls pow_fold_v directly, so pow_fold_v's own
|
|
592
|
+
// dependency needs the same fixpoint append. Both only ever exist in the stdlib under crPow.
|
|
593
|
+
'math.pow_fold_v', 'math.pow_fold'])
|
|
533
594
|
|
|
534
595
|
// A late pass can reference one of the f64x2 mirrors that wasn't present when the stdlib was first
|
|
535
596
|
// assembled. Append any referenced-but-missing mirror body (fixpoint over their own calls, though
|
|
536
597
|
// the trig mirrors call nothing). moduleArr is mutated in place; non-mirror references are left for
|
|
537
598
|
// watr to resolve (a genuine missing helper is the kernel's own pull, already satisfied).
|
|
538
|
-
export function appendLateStdlib(moduleArr) {
|
|
599
|
+
export function appendLateStdlib(moduleArr, pushTarget = moduleArr) {
|
|
539
600
|
const stdlib = ctx.core.stdlib
|
|
540
601
|
const have = new Set()
|
|
541
602
|
for (const n of moduleArr) if (Array.isArray(n) && n[0] === 'func' && typeof n[1] === 'string') have.add(n[1])
|
|
@@ -549,7 +610,12 @@ export function appendLateStdlib(moduleArr) {
|
|
|
549
610
|
const name = ref.slice(1)
|
|
550
611
|
if (have.has(ref) || !LATE_VEC_HELPERS.has(name) || stdlib[name] == null) continue
|
|
551
612
|
const node = parseTemplate(typeof stdlib[name] === 'function' ? stdlib[name]() : stdlib[name])
|
|
552
|
-
|
|
613
|
+
const body = node[0] === 'module' ? node[1] : node
|
|
614
|
+
pushTarget.push(body)
|
|
615
|
+
// Keep the scan array in sync so the fixpoint can resolve a mirror that itself
|
|
616
|
+
// calls another mirror (cbrt_v → log_v/exp_v). When pushTarget IS moduleArr the
|
|
617
|
+
// single push already did this.
|
|
618
|
+
if (pushTarget !== moduleArr) moduleArr.push(body)
|
|
553
619
|
have.add(ref)
|
|
554
620
|
added = true
|
|
555
621
|
}
|
|
@@ -584,7 +650,9 @@ export function pullStdlib(sec) {
|
|
|
584
650
|
// host marshals across the boundary). A data segment with no memory is invalid wasm,
|
|
585
651
|
// so memory can't be gated on allocation alone.
|
|
586
652
|
const ALLOC_FUNCS = ['__alloc', '__alloc_hdr', '__alloc_hdr_n']
|
|
587
|
-
const needsAlloc = !!ctx.runtime.strPool || ALLOC_FUNCS.some(a => reachable.has(a))
|
|
653
|
+
const needsAlloc = !!ctx.runtime.strPool || ALLOC_FUNCS.some(a => reachable.has(a)) ||
|
|
654
|
+
// shared memory memory.init's the static region into __alloc'd space at start
|
|
655
|
+
!!(ctx.memory.shared && ctx.runtime.data)
|
|
588
656
|
// Memory ops can be emitted *inline* into user/start funcs (a heap-path char read
|
|
589
657
|
// loads without calling a stdlib helper), so scan the emitted bodies too.
|
|
590
658
|
const hasMemOp = (node) => Array.isArray(node) &&
|
|
@@ -609,25 +677,47 @@ export function pullStdlib(sec) {
|
|
|
609
677
|
// are pulled in on demand, never eagerly, so they're already minimal and never pruned
|
|
610
678
|
// here (guarding against any reachability blind spot in a dotted-name template).
|
|
611
679
|
for (const n of [...ctx.core.includes]) if (n.startsWith('__') && !reachable.has(n)) ctx.core.includes.delete(n)
|
|
612
|
-
// Lazy
|
|
613
|
-
// decimal
|
|
614
|
-
//
|
|
615
|
-
// dataPages (below) accounts for the addition; keeps
|
|
616
|
-
//
|
|
617
|
-
|
|
618
|
-
|
|
680
|
+
// Lazy data-table injection — Eisel-Lemire decimal→f64 (~2KB) and Ryū
|
|
681
|
+
// float→decimal (~9.7KB), module/number.js. Each table is appended only when
|
|
682
|
+
// its owning function survived pruning, and its base global declared at the
|
|
683
|
+
// offset. Must run HERE so dataPages (below) accounts for the addition; keeps
|
|
684
|
+
// the tables out of programs that never convert decimals at runtime.
|
|
685
|
+
//
|
|
686
|
+
// Reachability here OVER-counts: a dead inlined helper's `arr[i] | 0` on an
|
|
687
|
+
// untyped param pulls __to_num → __dec_to_f64, landing a table even when no
|
|
688
|
+
// LIVE code uses it. Record each span (they are the data tail — the last
|
|
689
|
+
// appends) so stripDeadLazyTables can excise dead ones post-lowering, once
|
|
690
|
+
// reachability is exact. Base globals register in staticI32GlobalInits so a
|
|
691
|
+
// later static-prefix strip shifts them like every other static offset.
|
|
692
|
+
ctx.runtime.lazySpans = []
|
|
693
|
+
const injectTable = (fn, global, bytes) => {
|
|
694
|
+
if (!ctx.core.includes.has(fn) || !bytes) return false
|
|
695
|
+
// ctx.runtime.data is normally already a string by here because module/number.js's
|
|
696
|
+
// setup (which seeds the static NaN/Infinity/… stringify prefix) runs unconditionally —
|
|
697
|
+
// but ONLY for a program that pulls in something from number.js. A program reaching
|
|
698
|
+
// this via a module with no such dependency (e.g. module/math.js's CR-pow tables, needed
|
|
699
|
+
// by any `**`/Math.pow call, independent of number formatting) can hit pullStdlib with
|
|
700
|
+
// ctx.runtime.data still at its unset default — initialize defensively.
|
|
701
|
+
ctx.runtime.data ??= ''
|
|
702
|
+
const start = ctx.runtime.data.length
|
|
619
703
|
while (ctx.runtime.data.length % 8 !== 0) ctx.runtime.data += '\0'
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
ctx.runtime.
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
ctx.runtime.elTableLen = ctx.runtime.data.length - elBefore
|
|
704
|
+
// Shared memory: the table lands via memory.init at a runtime base, so the
|
|
705
|
+
// global is MUTABLE and re-pointed at start (compile/index.js); its declared
|
|
706
|
+
// init meanwhile holds the offset WITHIN the static region.
|
|
707
|
+
declGlobal(global, 'i32', ctx.runtime.data.length, ctx.memory.shared ? undefined : { mut: false })
|
|
708
|
+
if (ctx.memory.shared && !ctx.scope.globals.has('__staticBase')) declGlobal('__staticBase', 'i32')
|
|
709
|
+
ctx.runtime.data += bytes
|
|
710
|
+
;(ctx.runtime.staticI32GlobalInits ??= []).push(global)
|
|
711
|
+
ctx.runtime.lazySpans.push({ fn: '$' + fn, global, start, bytes })
|
|
712
|
+
return true
|
|
630
713
|
}
|
|
714
|
+
// prevent double-injection on re-entry (null-sentinel; jz forbids delete)
|
|
715
|
+
if (injectTable('__dec_to_f64', '__el_tbl', ctx.runtime.elTable)) ctx.runtime.elTable = null
|
|
716
|
+
if (injectTable('__ftoa_shortest', '__ryu_tbl', ctx.runtime.ryuTable)) ctx.runtime.ryuTable = null
|
|
717
|
+
// CR-pow log2/exp2 breakpoint tables (module/math.js's $math.pow_transcend) — both gated on
|
|
718
|
+
// the SAME owning function since the shared kernel always needs both tables together.
|
|
719
|
+
if (injectTable('math.pow_transcend', 'math.pow_log2_tbl', ctx.runtime.powLog2Table)) ctx.runtime.powLog2Table = null
|
|
720
|
+
if (injectTable('math.pow_transcend', 'math.pow_exp2_tbl', ctx.runtime.powExp2Table)) ctx.runtime.powExp2Table = null
|
|
631
721
|
if (!needsAlloc) { ctx.scope.globals.delete('__heap'); ctx.scope.globals.delete('__heap_reset') }
|
|
632
722
|
if (needsMemory && ctx.module.modules.core) {
|
|
633
723
|
if (needsAlloc) {
|
|
@@ -641,15 +731,174 @@ export function pullStdlib(sec) {
|
|
|
641
731
|
// GLOBALS/atom tables), not into it. Done here — where `__heap` is known to
|
|
642
732
|
// survive — as the last `__start` action before any non-returning timer loop.
|
|
643
733
|
// No `__start` ⇒ no init allocations ⇒ `__heap_reset`'s data-end seed is right.
|
|
734
|
+
// Module-global snapshot sweep: `__clear` rewinds the arena, so ANY mutable module
|
|
735
|
+
// global still holding a pointer into it dangles — the whole warm-reuse landmine
|
|
736
|
+
// class (a lazy `let CACHE = null` cache, json's `__jbuf` stringify buffer, watr's
|
|
737
|
+
// in-kernel NCLS dict, a memoized string…). Fixing sites one at a time is
|
|
738
|
+
// whack-a-mole (eager-NCLS peeled "Unknown memory end" only to expose the next
|
|
739
|
+
// dangler behind it); the class fix is a CONTRACT: `_clear` restores every
|
|
740
|
+
// runtime-written module global to its post-`__start` value — warm behaves as
|
|
741
|
+
// fresh, minus the init cost. Blanket restore beats an is-ephemeral-pointer test:
|
|
742
|
+
// it also heals SCALAR poisoning (a cached length/hash derived from round-1 arena
|
|
743
|
+
// content is stale garbage even though it's no pointer). Mechanics: reserve one
|
|
744
|
+
// durable slab slot per candidate (allocated at `__start` tail — BEFORE the
|
|
745
|
+
// `__heap_reset` capture, so the slab sits under the watermark), store each
|
|
746
|
+
// global's post-init value there, and re-load it in `__clear`. Only globals the
|
|
747
|
+
// write-scan sees mutated OUTSIDE `__start` participate (read-only globals cannot
|
|
748
|
+
// dangle, and rooting them here would defeat watr's dead-global pruning); with no
|
|
749
|
+
// `__start` a global's post-init value IS its declared init — restore the constant
|
|
750
|
+
// directly, no slot. Excluded: the runtime-protocol globals (each has its own
|
|
751
|
+
// reset right here in `__clear` — resetting `__heap_reset` itself would be
|
|
752
|
+
// self-defeating), `__tof_*` coercion scratch (written-before-read within one
|
|
753
|
+
// expression, can never carry state across a round) and `__hc_*` helper counters
|
|
754
|
+
// (diagnostics must observe rounds, not be reset by them).
|
|
755
|
+
const globalRestores = []
|
|
644
756
|
if (!ctx.memory.shared && ctx.scope.globals.has('__heap_reset')) {
|
|
645
757
|
const startFn = sec.start.find(n => Array.isArray(n) && n[0] === 'func' && n[1] === '$__start')
|
|
758
|
+
const SNAP_PROTOCOL = new Set(['__heap', '__heap_reset', '__heap_start', '__dyn_props', '__dyn_props_filter',
|
|
759
|
+
'__dyn_get_cache_off', '__dyn_get_cache_props', '__durable_fwd_buf', '__durable_fwd_n', '__gsnap_base',
|
|
760
|
+
'__enumc_off', '__enumc_len', '__enumc_arr'])
|
|
761
|
+
const runtimeWritten = new Set()
|
|
762
|
+
const scanSet = (node) => {
|
|
763
|
+
if (!Array.isArray(node)) return
|
|
764
|
+
if (node[0] === 'global.set' && typeof node[1] === 'string' && node[1][0] === '$') runtimeWritten.add(node[1].slice(1))
|
|
765
|
+
for (const c of node) scanSet(c)
|
|
766
|
+
}
|
|
767
|
+
for (const fn of sec.funcs) scanSet(fn)
|
|
768
|
+
// stdlib bodies are still WAT text here (parseTemplate runs later) — scan textually.
|
|
769
|
+
// Helpers write registry globals too: collection's __seq, json's __jbuf/__jstack….
|
|
770
|
+
// Thunked templates expand ONCE by contract (expansion-time ctx reads) — memoize
|
|
771
|
+
// the expansion back into the registry so the later parseTemplate pass reuses this
|
|
772
|
+
// exact string instead of expanding a second time.
|
|
773
|
+
for (const name of ctx.core.includes) {
|
|
774
|
+
let src = ctx.core.stdlib[name]
|
|
775
|
+
if (typeof src === 'function') ctx.core.stdlib[name] = src = src()
|
|
776
|
+
if (typeof src !== 'string') continue
|
|
777
|
+
for (const m of src.matchAll(/\(global\.set \$([A-Za-z0-9_.$]+)/g)) runtimeWritten.add(m[1])
|
|
778
|
+
}
|
|
779
|
+
// Self-host divergence diagnostics (see resolveIncludes' twin block).
|
|
780
|
+
if (ctx.core.diagSink) ctx.core.diagSink.sweep = {
|
|
781
|
+
includes: [...ctx.core.includes].sort().join(' '),
|
|
782
|
+
runtimeWritten: [...runtimeWritten].sort().join(' '),
|
|
783
|
+
}
|
|
784
|
+
const SNAP_TYPES = { i32: 8, i64: 8, f32: 8, f64: 8, v128: 16 }
|
|
785
|
+
const snapSlots = [] // [name, type, slabOffset]
|
|
786
|
+
let slabBytes = 0
|
|
787
|
+
for (const name of runtimeWritten) {
|
|
788
|
+
const g = ctx.scope.globals.get(name)
|
|
789
|
+
if (!g || !g.mut || !SNAP_TYPES[g.type]) continue
|
|
790
|
+
if (SNAP_PROTOCOL.has(name) || name.startsWith('__tof_') || name.startsWith('__hc_')) continue
|
|
791
|
+
if (startFn) { snapSlots.push([name, g.type, slabBytes]); slabBytes += SNAP_TYPES[g.type] }
|
|
792
|
+
// no __start ⇒ post-init value = declared init: restore the constant, no slot
|
|
793
|
+
else globalRestores.push(`(global.set $${name} (${g.type}.const ${g.init ?? 0}))`)
|
|
794
|
+
}
|
|
795
|
+
if (ctx.core.diagSink?.sweep) {
|
|
796
|
+
ctx.core.diagSink.sweep.snapSlots = snapSlots.map(([n]) => n).sort().join(' ')
|
|
797
|
+
ctx.core.diagSink.sweep.restores = globalRestores.slice().sort().join(' ')
|
|
798
|
+
ctx.core.diagSink.sweep.hasStart = !!startFn
|
|
799
|
+
}
|
|
646
800
|
if (startFn) {
|
|
801
|
+
// Tier 2 payoff: when module init folded away entirely (static trees,
|
|
802
|
+
// static schema table) and no global needs a snapshot slot, the ONLY
|
|
803
|
+
// thing left to do is the __heap_reset capture — whose value is exactly
|
|
804
|
+
// the seeded data-end init. Drop __start altogether.
|
|
805
|
+
if (!snapSlots.length && findBodyStart(startFn) >= startFn.length) {
|
|
806
|
+
const dirIdx = sec.start.findIndex(n => Array.isArray(n) && n[0] === 'start')
|
|
807
|
+
sec.start.length = 0
|
|
808
|
+
if (dirIdx !== -1) { /* directive lived in sec.start — cleared above */ }
|
|
809
|
+
} else {
|
|
647
810
|
const capture = ['global.set', '$__heap_reset', ['global.get', '$__heap']]
|
|
811
|
+
const inject = [capture]
|
|
812
|
+
if (snapSlots.length) {
|
|
813
|
+
declGlobal('__gsnap_base', 'i32')
|
|
814
|
+
inject.unshift(['global.set', '$__gsnap_base', ['call', '$__alloc', ['i32.const', String(slabBytes)]]],
|
|
815
|
+
...snapSlots.map(([name, type, off]) =>
|
|
816
|
+
[`${type}.store`, `offset=${off}`, ['global.get', '$__gsnap_base'], ['global.get', `$${name}`]]))
|
|
817
|
+
for (const [name, type, off] of snapSlots)
|
|
818
|
+
globalRestores.push(`(global.set $${name} (${type}.load offset=${off} (global.get $__gsnap_base)))`)
|
|
819
|
+
}
|
|
648
820
|
const tail = startFn[startFn.length - 1]
|
|
649
|
-
if (Array.isArray(tail) && tail[0] === 'call' && tail[1] === '$__timer_loop') startFn.splice(startFn.length - 1, 0,
|
|
650
|
-
else startFn.push(
|
|
821
|
+
if (Array.isArray(tail) && tail[0] === 'call' && tail[1] === '$__timer_loop') startFn.splice(startFn.length - 1, 0, ...inject)
|
|
822
|
+
else startFn.push(...inject)
|
|
823
|
+
}
|
|
651
824
|
}
|
|
652
825
|
}
|
|
826
|
+
// __dyn_props reset: __clear rewinds the bump arena, but __dyn_props /
|
|
827
|
+
// __dyn_get_cache_off / __dyn_get_cache_props (module/collection.js) cache
|
|
828
|
+
// pointers/offsets INTO that arena across calls — a warm compile-clear-
|
|
829
|
+
// compile loop (self-host kernel: one instance, `_clear()` between compiles)
|
|
830
|
+
// needs them reset too, or a later compile can read a dangling pointer or,
|
|
831
|
+
// worse, alias a stale cached OFFSET onto a freshly-reused arena address
|
|
832
|
+
// (an ABA hazard, not just a dangling one). Only patched in when __dyn_set
|
|
833
|
+
// (the sole writer of __dyn_props) actually SURVIVED reachability pruning
|
|
834
|
+
// (line ~616, just above) — those globals are declared unconditionally
|
|
835
|
+
// whenever the collection module loads, so gating on mere declaration
|
|
836
|
+
// (`ctx.scope.globals.has`) would inject a dead `global.set $__dyn_props`
|
|
837
|
+
// into every such program, wasting bytes and leaking the __dyn_get_cache_*
|
|
838
|
+
// names into WAT text that never otherwise mentions dynamic props (tripping
|
|
839
|
+
// coarse `!/__dyn_get/.test(wat)`-style assertions — see test/closures.js).
|
|
840
|
+
// Both blocks below extend the SAME `__clear` body — accumulate into one
|
|
841
|
+
// shared list and rebuild once, so whichever runs second doesn't clobber the
|
|
842
|
+
// other's addition (a program can need both: dyn-props AND durable-growth
|
|
843
|
+
// relocation both reach here independent of each other).
|
|
844
|
+
const resets = []
|
|
845
|
+
if (ctx.core.includes.has('__dyn_set')) {
|
|
846
|
+
if (ctx.scope.globals.has('__dyn_props')) resets.push(`(global.set $__dyn_props (f64.const 0))`)
|
|
847
|
+
// The membership filter mirrors the table: emptying __dyn_props makes every
|
|
848
|
+
// set bit a stale false-positive — safe, but a warm compile-clear loop would
|
|
849
|
+
// saturate the filter and erode its skip rate. Reset them together.
|
|
850
|
+
if (ctx.scope.globals.has('__dyn_props_filter')) resets.push(`(global.set $__dyn_props_filter (i64.const 0))`)
|
|
851
|
+
if (ctx.scope.globals.has('__dyn_get_cache_off')) resets.push(`(global.set $__dyn_get_cache_off (i32.const -1))`)
|
|
852
|
+
if (ctx.scope.globals.has('__dyn_get_cache_props')) resets.push(`(global.set $__dyn_get_cache_props (f64.const 0))`)
|
|
853
|
+
}
|
|
854
|
+
// for-in enum cache (core.js __hash_keys_ro / object.js ro-enumeration):
|
|
855
|
+
// the cache keys a boxed array by table offset — both live in the arena
|
|
856
|
+
// __clear rewinds, so a later round could re-issue the cached offset to a
|
|
857
|
+
// NEW table and false-hit onto reclaimed memory (same ABA hazard as
|
|
858
|
+
// __dyn_get_cache_off above). Gated on enumcConsumed, not reachability:
|
|
859
|
+
// the OBJECT-arm fill sites are inline IR (no named helper to count).
|
|
860
|
+
if (ctx.runtime.enumcConsumed)
|
|
861
|
+
resets.push(`(global.set $__enumc_off (i32.const 0))`)
|
|
862
|
+
// Durable relocation heal (collection.js's durableFwdLogIR / core.js's
|
|
863
|
+
// __durable_fwd_log/__durable_fwd_heal): only reachable when some growable
|
|
864
|
+
// ARRAY/HASH/SET/MAP relocation site actually logged a durable→ephemeral
|
|
865
|
+
// forward this build — see durableFwdLogIR's header comment for the full
|
|
866
|
+
// rationale. Must run before the next round can allocate over the logged
|
|
867
|
+
// ephemeral targets, so it belongs in `__clear` alongside the arena rewind
|
|
868
|
+
// (order vs the rewind itself doesn't matter — `_clear` never zeroes memory,
|
|
869
|
+
// only moves the bump pointer — but keeping it grouped with the other resets
|
|
870
|
+
// reads as "finish with this round's bookkeeping, then reclaim its arena").
|
|
871
|
+
if (ctx.core.includes.has('__durable_fwd_log')) {
|
|
872
|
+
// __durable_fwd_heal is called ONLY from this injected `__clear` text — it has
|
|
873
|
+
// no OTHER call site for reachableStdlib (line ~582, already run) to have found
|
|
874
|
+
// it through, so (unlike __durable_fwd_log itself, whose deps() edges at every
|
|
875
|
+
// grow/shift call site make it self-host-robust — see test/selfhost-includes.js)
|
|
876
|
+
// it needs an explicit include here, mirroring the `__alloc`/`__alloc_hdr`/
|
|
877
|
+
// `__clear` late-add just above. `inc()`, not a raw `ctx.core.includes.add()`:
|
|
878
|
+
// the former is what test/selfhost-includes.js's source-scan recognizes as an
|
|
879
|
+
// explicit (self-host-safe) edge. No further resolveIncludes() needed:
|
|
880
|
+
// __durable_fwd_heal's body calls nothing else (raw i32 loads/stores + global
|
|
881
|
+
// get/set only).
|
|
882
|
+
inc('__durable_fwd_heal')
|
|
883
|
+
resets.push(`(call $__durable_fwd_heal)`)
|
|
884
|
+
}
|
|
885
|
+
// Durable SLOT heal (core.js __durable_slot_log/__durable_slot_heal — the
|
|
886
|
+
// entry/value sibling of the relocation heal above): every logged durable
|
|
887
|
+
// collection slot written this round is healed (inserted entries zombied +
|
|
888
|
+
// len decremented, overwritten values read undefined) before the arena
|
|
889
|
+
// rewinds. Ordered AFTER the fwd heal: a grown-then-healed table's len must
|
|
890
|
+
// already be its restored pre-grow value when the zombie decrements land.
|
|
891
|
+
// Same explicit-include pattern (its ONLY call site is this injected text).
|
|
892
|
+
if (ctx.core.includes.has('__durable_slot_log')) {
|
|
893
|
+
inc('__durable_slot_heal')
|
|
894
|
+
resets.push(`(call $__durable_slot_heal)`)
|
|
895
|
+
}
|
|
896
|
+
// Global-snapshot restores (see the sweep above) join the same rebuilt body.
|
|
897
|
+
// Order is free — restores touch only globals + the durable slab, which the
|
|
898
|
+
// rewind never moves — but bookkeeping-then-rewind-then-restore reads naturally.
|
|
899
|
+
if (resets.length || globalRestores.length) ctx.core.stdlib['__clear'] = `(func $__clear
|
|
900
|
+
(global.set $__heap (global.get $__heap_reset))
|
|
901
|
+
${[...resets, ...globalRestores].join('\n ')})`
|
|
653
902
|
}
|
|
654
903
|
// Initial pages must cover the static data segment (it loads at instantiation), not
|
|
655
904
|
// just the default 1 — otherwise a module whose constants exceed 64 KiB emits a data
|
|
@@ -659,7 +908,13 @@ export function pullStdlib(sec) {
|
|
|
659
908
|
const dataPages = ctx.memory.shared ? 0 : Math.ceil((ctx.runtime.data?.length || 0) / 65536)
|
|
660
909
|
const pages = Math.max(ctx.memory.pages || 1, dataPages)
|
|
661
910
|
const max = ctx.memory.max || 0 // 0 = no maximum (unbounded growth)
|
|
662
|
-
|
|
911
|
+
// Truly-shared memory (opts.sharedMemory) declares the `shared` memtype —
|
|
912
|
+
// the spec requires an explicit max there (default: the wasm32 page ceiling).
|
|
913
|
+
// Plain imported memory (opts.importMemory / a Memory-valued opts.memory)
|
|
914
|
+
// stays non-shared, or a host passing an ordinary Memory could never link.
|
|
915
|
+
if (ctx.memory.shared) sec.imports.push(['import', '"env"', '"memory"',
|
|
916
|
+
ctx.memory.atomic ? ['memory', pages, max || 65536, 'shared']
|
|
917
|
+
: max ? ['memory', pages, max] : ['memory', pages]])
|
|
663
918
|
else sec.memory.push(max ? ['memory', ['export', '"memory"'], pages, max] : ['memory', ['export', '"memory"'], pages])
|
|
664
919
|
if (needsAlloc && ctx.transform.alloc !== false && ctx.core._allocRawFuncs)
|
|
665
920
|
sec.funcs.push(...ctx.core._allocRawFuncs.map(parseTemplate))
|
|
@@ -696,13 +951,9 @@ export function optimizeModule(sec, profiler) {
|
|
|
696
951
|
const cfg = ctx.transform.optimize
|
|
697
952
|
if (!cfg || cfg.specializeMkptr !== false) t('specializeMkptr', () =>
|
|
698
953
|
specializeMkptr([...sec.funcs, ...sec.stdlib, ...sec.start], wat => sec.stdlib.push(parseWat(wat)), parseWat))
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const poolRef = { pool: ctx.runtime.strPool }
|
|
703
|
-
sortStrPoolByFreq([...sec.funcs, ...sec.stdlib, ...sec.start], poolRef, ctx.runtime.strPoolDedup)
|
|
704
|
-
ctx.runtime.strPool = poolRef.pool
|
|
705
|
-
})
|
|
954
|
+
// (specializePtrBase and sortStrPoolByFreq deleted: byte-identical output with
|
|
955
|
+
// both disabled across the bench + examples corpora AND the self-host kernel at
|
|
956
|
+
// every watr tier — watr's own inlining/offset folding subsumed them. ~350ms/corpus.)
|
|
706
957
|
// (globalTypes backfill gone: declGlobal sets the type at declaration.)
|
|
707
958
|
// Build global name→type map from ctx.scope.globalTypes (keys without $) for promoteGlobals
|
|
708
959
|
const globalTypesMap = ctx.scope.globalTypes ? new Map([...ctx.scope.globalTypes].map(([k, v]) => [`$${k}`, v])) : null
|
|
@@ -718,7 +969,64 @@ export function optimizeModule(sec, profiler) {
|
|
|
718
969
|
const stable = stablePtrGlobalNames()
|
|
719
970
|
if (stable.size) for (const s of allFuncs) hoistGlobalPtrOffset(s, stable, reachableWrites)
|
|
720
971
|
})
|
|
721
|
-
|
|
972
|
+
// Per-loop complement: a function the whole-function pass above declined
|
|
973
|
+
// (an unrelated call_indirect / write ANYWHERE in the function poisons
|
|
974
|
+
// every global for it) may still have individual loops that are clean on
|
|
975
|
+
// their own narrower scope — e.g. a char-scan loop inside a devirtualized
|
|
976
|
+
// Pratt-loop trampoline that also inlines unrelated operator dispatch.
|
|
977
|
+
if (!cfg || cfg.hoistLoopGlobalPtrOffset !== false) t('hoistLoopGlobalPtr', () => {
|
|
978
|
+
const stable = stablePtrGlobalNames()
|
|
979
|
+
if (stable.size) for (const s of allFuncs) hoistLoopGlobalPtrOffset(s, stable, reachableWrites)
|
|
980
|
+
})
|
|
981
|
+
// Build the pure-function map for tryPerPixelColor's Phase-2 lane inline BEFORE the
|
|
982
|
+
// per-function vectorizer runs — the vectorizer is jz lowering (pre-watr), so it needs
|
|
983
|
+
// its inline candidates now, not after watr. Bodies are still clean scalar here.
|
|
984
|
+
if (cfg && cfg.vectorizeLaneLocal === true) {
|
|
985
|
+
const pureFuncMap = buildPureFuncMap(allFuncs)
|
|
986
|
+
if (pureFuncMap.size) {
|
|
987
|
+
cfg._pureFuncMap = pureFuncMap
|
|
988
|
+
// jz semantic inlining (LOWERING) — inline pure user functions into their call sites BEFORE the
|
|
989
|
+
// vectorizer, so it sees the callee arithmetic (the pow/decode a colour helper hides). jz owns
|
|
990
|
+
// this because the decision is purity+type-driven; watr keeps only mechanical residual inlining.
|
|
991
|
+
// Gated to SINGLE-CALLER pure functions: inlining the sole call site is a guaranteed win (removes
|
|
992
|
+
// the call AND the now-dead function, zero size cost). Multi-caller small helpers stay watr's
|
|
993
|
+
// size-gated mechanical job at the speed tier — jz doesn't duplicate that.
|
|
994
|
+
// SMALL single-caller only: inlining a small pure helper (a `spow`/`decode` colour term) into its
|
|
995
|
+
// sole caller exposes its arithmetic to the vectorizer at zero size cost. Inlining a LARGE function
|
|
996
|
+
// (a whole conversion loop) is neutral-to-harmful (worse layout/regalloc, measured on colorpq), and
|
|
997
|
+
// watr's own inlineOnce already handles the mechanical single-caller case — so jz stays out of it.
|
|
998
|
+
// OPT-IN (default off): correct + fuzz-clean, but inlining across the corpus changes a lot of
|
|
999
|
+
// pinned output-shape assertions for no measured bench win (the current regressions are outer-strip/
|
|
1000
|
+
// widening recognition + watr wasm-opt-class, not inlining). Kept as the architectural home for
|
|
1001
|
+
// semantic inlining, enabled per-compile via `optimize.inlinePureFns: true`, until a real case pays.
|
|
1002
|
+
if (cfg.inlinePureFns === true) t('inlinePureFns', () => {
|
|
1003
|
+
const callCount = new Map()
|
|
1004
|
+
const countCalls = (n) => { if (!Array.isArray(n)) return; if ((n[0] === 'call' || n[0] === 'return_call') && typeof n[1] === 'string') callCount.set(n[1], (callCount.get(n[1]) || 0) + 1); for (let i = 1; i < n.length; i++) countCalls(n[i]) }
|
|
1005
|
+
for (const s of allFuncs) countCalls(s)
|
|
1006
|
+
const nodeCount = (n) => !Array.isArray(n) ? 0 : 1 + n.reduce((a, c, i) => a + (i > 0 ? nodeCount(c) : 0), 0)
|
|
1007
|
+
const INLINE_MAX = 48
|
|
1008
|
+
const canInline = new Set([...pureFuncMap.keys()].filter(name =>
|
|
1009
|
+
callCount.get(name) === 1 && nodeCount(pureFuncMap.get(name)) <= INLINE_MAX))
|
|
1010
|
+
if (canInline.size) { const idRef = { next: 0 }; for (const s of allFuncs) inlinePureFnsInFn(s, pureFuncMap, idRef, canInline) }
|
|
1011
|
+
})
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
// Candidate bodies for devirt arm inlining and block-narrowing
|
|
1015
|
+
// (devirtConstFnArrayCalls): the UNFILTERED name→fn map of const-fn-array
|
|
1016
|
+
// element bodies. Built here — the pass runs per-function inside optimizeFunc
|
|
1017
|
+
// and can't see sibling functions. No purity filter: the inliner enforces
|
|
1018
|
+
// straight-line shape itself, and an arm executes exactly when the original
|
|
1019
|
+
// call did, so side-effecting bodies substitute safely.
|
|
1020
|
+
if (ctx.scope.constFnArrays?.size) {
|
|
1021
|
+
const candNames = new Set()
|
|
1022
|
+
for (const list of ctx.scope.constFnArrays.values()) for (const c of list) candNames.add(`$${c.name}`)
|
|
1023
|
+
ctx.scope.dvArmFns = new Map(allFuncs.filter(f => Array.isArray(f) && candNames.has(f[1])).map(f => [f[1], f]))
|
|
1024
|
+
}
|
|
1025
|
+
t('optimizeFuncs', () => { for (const s of allFuncs) optimizeFunc(s, cfg, globalTypesMap, volatileGlobals, reachableWrites) })
|
|
1026
|
+
// The lane vectorizer can inject f64x2 stdlib mirrors ($math.log_v, $math.cos2, …)
|
|
1027
|
+
// absent from the already-pulled+treeshaken module. Append any now-referenced mirror
|
|
1028
|
+
// body to sec.stdlib — the pre-watr analogue of index.js's post-watr appendLateStdlib.
|
|
1029
|
+
if (cfg && cfg.vectorizeLaneLocal === true) t('appendLateStdlib', () => appendLateStdlib(allFuncs, sec.stdlib))
|
|
722
1030
|
if (!cfg || cfg.arenaRewind !== false) {
|
|
723
1031
|
const safeCallees = arenaRewindModule([...sec.funcs, ...sec.stdlib, ...sec.start])
|
|
724
1032
|
const fnByName = new Map()
|
|
@@ -776,8 +1084,9 @@ export function optimizeModule(sec, profiler) {
|
|
|
776
1084
|
* watr, which already removes them. Keeps correctly-rounded decimal parsing wherever it is
|
|
777
1085
|
* genuinely live (parseFloat, the self-host compiler's `Number()` on source literals).
|
|
778
1086
|
*/
|
|
779
|
-
export function
|
|
780
|
-
|
|
1087
|
+
export function stripDeadLazyTables(sec) {
|
|
1088
|
+
const spans = ctx.runtime.lazySpans
|
|
1089
|
+
if (!spans || !spans.length) return
|
|
781
1090
|
const byName = new Map()
|
|
782
1091
|
for (const arr of [sec.funcs, sec.stdlib, sec.start])
|
|
783
1092
|
for (const f of arr || []) if (Array.isArray(f) && f[0] === 'func' && typeof f[1] === 'string') byName.set(f[1], f)
|
|
@@ -795,9 +1104,28 @@ export function stripDeadElTable(sec) {
|
|
|
795
1104
|
for (const c of n) { if (typeof c === 'string' && c[0] === '$') mark(c); else scan(c) }
|
|
796
1105
|
}
|
|
797
1106
|
while (work.length) scan(byName.get(work.pop()))
|
|
798
|
-
if (live.has(
|
|
799
|
-
|
|
800
|
-
|
|
1107
|
+
if (spans.every(s => live.has(s.fn))) { ctx.runtime.lazySpans = ctx.memory.shared ? spans : [] ; return }
|
|
1108
|
+
// Rebuild the tail (the spans are the last data appends, in order): truncate
|
|
1109
|
+
// to the first span's pre-pad start, re-append live tables, re-point their
|
|
1110
|
+
// base globals — both the scope entry and the already-pushed sec.globals IR.
|
|
1111
|
+
const setInit = (name, off) => {
|
|
1112
|
+
const g = ctx.scope.globals.get(name)
|
|
1113
|
+
if (g) g.init = off
|
|
1114
|
+
for (const node of sec.globals) if (Array.isArray(node) && node[1] === '$' + name) {
|
|
1115
|
+
const c = node[node.length - 1]
|
|
1116
|
+
if (Array.isArray(c) && c[0] === 'i32.const') c[1] = off
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
ctx.runtime.data = ctx.runtime.data.slice(0, spans[0].start)
|
|
1120
|
+
for (const s of spans) {
|
|
1121
|
+
if (!live.has(s.fn)) { setInit(s.global, 0); continue }
|
|
1122
|
+
while (ctx.runtime.data.length % 8 !== 0) ctx.runtime.data += '\0'
|
|
1123
|
+
setInit(s.global, ctx.runtime.data.length)
|
|
1124
|
+
ctx.runtime.data += s.bytes
|
|
1125
|
+
}
|
|
1126
|
+
// Shared memory needs the survivor list after this pass — the start-time
|
|
1127
|
+
// rebase (compile/index.js) re-points each surviving table global.
|
|
1128
|
+
ctx.runtime.lazySpans = ctx.memory.shared ? spans.filter(s => live.has(s.fn)) : []
|
|
801
1129
|
}
|
|
802
1130
|
|
|
803
1131
|
/**
|
|
@@ -840,6 +1168,16 @@ export function stripStaticDataPrefix(sec) {
|
|
|
840
1168
|
ctx.runtime.internTable.base = base - prefix
|
|
841
1169
|
declGlobal('__internBase', 'i32', base - prefix, { mut: false })
|
|
842
1170
|
}
|
|
1171
|
+
// Raw-i32 globals whose INIT is a static-data offset (static schema table, …):
|
|
1172
|
+
// shift the declared init by the stripped prefix, like every boxed slot above.
|
|
1173
|
+
if (ctx.runtime.staticI32GlobalInits) for (const name of ctx.runtime.staticI32GlobalInits) {
|
|
1174
|
+
const g = ctx.scope.globals.get(name)
|
|
1175
|
+
if (g && typeof g.init === 'number' && g.init >= prefix) g.init -= prefix
|
|
1176
|
+
}
|
|
1177
|
+
// Lazy-table spans (EL/Ryū) sit at the data tail — keep their recorded starts
|
|
1178
|
+
// in post-strip coordinates so stripDeadLazyTables truncates at the right base.
|
|
1179
|
+
if (ctx.runtime.lazySpans) for (const s of ctx.runtime.lazySpans)
|
|
1180
|
+
if (s.start >= prefix) s.start -= prefix
|
|
843
1181
|
let s = ''
|
|
844
1182
|
for (let i = prefix; i < buf.length; i++) s += String.fromCharCode(buf[i])
|
|
845
1183
|
ctx.runtime.data = s
|