jz 0.6.0 → 0.8.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 +99 -72
- package/bench/README.md +253 -100
- package/bench/bench.svg +58 -81
- package/cli.js +85 -12
- package/dist/interop.js +1 -0
- package/dist/jz.js +6196 -0
- package/index.d.ts +126 -0
- package/index.js +222 -35
- package/interop.js +240 -141
- package/layout.js +34 -18
- package/module/array.js +99 -111
- package/module/collection.js +124 -30
- package/module/console.js +1 -1
- package/module/core.js +163 -19
- package/module/json.js +3 -3
- package/module/math.js +162 -3
- package/module/number.js +268 -13
- package/module/object.js +37 -5
- package/module/regex.js +8 -7
- package/module/simd.js +37 -5
- package/module/string.js +203 -168
- package/module/typedarray.js +565 -112
- package/package.json +26 -7
- package/src/abi/string.js +29 -29
- package/src/ast.js +19 -2
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +101 -4
- package/src/compile/cse-load.js +200 -0
- package/src/compile/emit-assign.js +82 -20
- package/src/compile/emit.js +592 -51
- package/src/compile/index.js +504 -89
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +109 -0
- package/src/compile/loop-model.js +91 -0
- package/src/compile/loop-square.js +102 -0
- package/src/compile/narrow.js +275 -41
- package/src/compile/peel-stencil.js +215 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/common.js +29 -0
- package/src/compile/plan/index.js +8 -1
- package/src/compile/plan/inline.js +180 -22
- package/src/compile/plan/literals.js +313 -24
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +96 -19
- package/src/helper-counters.js +137 -0
- package/src/ir.js +157 -20
- package/src/kind-traits.js +34 -3
- package/src/kind.js +79 -4
- package/src/op-policy.js +5 -2
- package/src/ops.js +119 -0
- package/src/optimize/index.js +1274 -151
- package/src/optimize/recurse.js +182 -0
- package/src/optimize/vectorize.js +4187 -253
- package/src/prepare/index.js +75 -14
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +6 -2
- package/src/static.js +9 -0
- package/src/type.js +63 -51
- package/src/wat/assemble.js +286 -21
- package/src/widen.js +21 -0
- package/src/wat/optimize.js +0 -3760
package/src/wat/assemble.js
CHANGED
|
@@ -37,8 +37,9 @@ import { T } from '../ast.js'
|
|
|
37
37
|
import { analyzeValTypes, analyzeBody } from '../compile/analyze.js'
|
|
38
38
|
import { VAL } from '../reps.js'
|
|
39
39
|
import { optimizeFunc, collectVolatileGlobals, collectReachableGlobalWrites, hoistGlobalPtrOffset, stablePtrGlobalNames, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, arenaRewindModule } from '../optimize/index.js'
|
|
40
|
-
import { emit } from '../compile/emit.js'
|
|
40
|
+
import { emit, emitVoid } from '../compile/emit.js'
|
|
41
41
|
import { mkPtrIR, MAX_CLOSURE_ARITY, MEM_OPS, findBodyStart } from '../ir.js'
|
|
42
|
+
import { installHelperCounters, instrumentHelperCounter } from '../helper-counters.js'
|
|
42
43
|
|
|
43
44
|
// NaN-prefix top-13-bits as BigInt — used by the static-prefix-strip pass
|
|
44
45
|
const NAN_PREFIX = BigInt(LAYOUT.NAN_PREFIX)
|
|
@@ -153,6 +154,11 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
153
154
|
analyzeValTypes(ast)
|
|
154
155
|
const normalizeIR = ir => !ir?.length ? [] : Array.isArray(ir[0]) ? ir : [ir]
|
|
155
156
|
|
|
157
|
+
// Mark module-scope emission: top-level statements run exactly once, so a constant
|
|
158
|
+
// array/object literal here is a single instance that can safely live in a static
|
|
159
|
+
// data segment (no per-call freshness to violate). Function bodies — compiled
|
|
160
|
+
// separately, and the late closures below — leave this unset and alloc fresh.
|
|
161
|
+
ctx.func.atModuleScope = true
|
|
156
162
|
const moduleInits = []
|
|
157
163
|
if (ctx.module.moduleInits) {
|
|
158
164
|
for (const mi of ctx.module.moduleInits) {
|
|
@@ -162,7 +168,13 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
162
168
|
}
|
|
163
169
|
}
|
|
164
170
|
seedGeneratedLocals(ast)
|
|
165
|
-
|
|
171
|
+
// __start has no result: emit the top-level program in void context so a stray
|
|
172
|
+
// value is dropped. `ast` is normally a `;` statement-sequence (each statement
|
|
173
|
+
// already void-dropped), but jzify unwraps a single-statement program to its
|
|
174
|
+
// bare expression — emitting that in value context leaves a value on the stack
|
|
175
|
+
// and the start function fails validation. emitVoid handles both shapes.
|
|
176
|
+
const init = emitVoid(ast)
|
|
177
|
+
ctx.func.atModuleScope = false
|
|
166
178
|
|
|
167
179
|
// Module-scope object literals can create closure bodies while `emit(ast)`
|
|
168
180
|
// runs. Those late closures may pull in stdlib helpers (notably JSON.parse)
|
|
@@ -210,6 +222,7 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
210
222
|
// table holding only empty schemas is pure dead weight there. __json_obj has
|
|
211
223
|
// no such guard — it must read the table whenever stringify is in play.
|
|
212
224
|
const tblConsumed = hasStringify ||
|
|
225
|
+
ctx.core.includes.has('__obj_clone') ||
|
|
213
226
|
ctx.core.includes.has('__dyn_get') ||
|
|
214
227
|
ctx.core.includes.has('__dyn_get_t') ||
|
|
215
228
|
ctx.core.includes.has('__dyn_get_t_h') ||
|
|
@@ -219,7 +232,13 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
219
232
|
ctx.core.includes.has('__dyn_get_any_t_h') ||
|
|
220
233
|
ctx.core.includes.has('__dyn_get_expr') ||
|
|
221
234
|
ctx.core.includes.has('__dyn_get_expr_t') ||
|
|
222
|
-
ctx.core.includes.has('__dyn_get_or')
|
|
235
|
+
ctx.core.includes.has('__dyn_get_or') ||
|
|
236
|
+
// A string runtime-key WRITE `o[k]=v` whose `k` matches a schema field must
|
|
237
|
+
// mirror the value into the fixed schema slot (buildObjectSchemaSetArm), or a
|
|
238
|
+
// later static `o.x` read returns the stale slot. That mirror is gated on
|
|
239
|
+
// `$__schema_tbl != 0`, so a write-only module (no `__dyn_get*`) must still
|
|
240
|
+
// build the table. (needsSchemaTbl below skips it when every schema is empty.)
|
|
241
|
+
ctx.core.includes.has('__dyn_set')
|
|
223
242
|
const needsSchemaTbl = (ctx.schema.list.length && tblConsumed &&
|
|
224
243
|
(hasStringify || ctx.schema.list.some(s => s.length > 0))) ||
|
|
225
244
|
hasJpObj
|
|
@@ -285,6 +304,47 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
285
304
|
sec.funcs.unshift(...closureFuncs.slice(beforeLateClosures))
|
|
286
305
|
}
|
|
287
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Hoist constant global initializers out of `__start` into immutable inline decls.
|
|
309
|
+
*
|
|
310
|
+
* A top-level `const x = <constant>` for a non-numeric value (atom `true`/`null`/
|
|
311
|
+
* `undefined`/`NaN`, an SSO or static-string NaN-box, a folded pointer) emits a
|
|
312
|
+
* `(global.set $x (f64.const …))` into `__start`, because only *numeric* consts are
|
|
313
|
+
* folded ahead of emit. But the value is a compile-time constant, so it belongs in
|
|
314
|
+
* the decl itself — `(global $x f64 (f64.const …))` — exactly like the numeric path.
|
|
315
|
+
* That drops the store, and when it empties `__start` the start function and its
|
|
316
|
+
* directive go too. Gated to single-assignment user `const`s so we never freeze a
|
|
317
|
+
* binding something else writes.
|
|
318
|
+
*/
|
|
319
|
+
export function hoistConstGlobalInits(sec) {
|
|
320
|
+
const startFn = sec.start.find(n => Array.isArray(n) && n[0] === 'func' && n[1] === '$__start')
|
|
321
|
+
if (!startFn) return
|
|
322
|
+
const writes = new Map()
|
|
323
|
+
const scan = (node) => {
|
|
324
|
+
if (!Array.isArray(node)) return
|
|
325
|
+
if (node[0] === 'global.set' && typeof node[1] === 'string') writes.set(node[1], (writes.get(node[1]) || 0) + 1)
|
|
326
|
+
for (const c of node) scan(c)
|
|
327
|
+
}
|
|
328
|
+
for (const arr of [sec.funcs, sec.stdlib, sec.start]) for (const fn of arr) scan(fn)
|
|
329
|
+
for (let i = startFn.length - 1; i >= findBodyStart(startFn); i--) {
|
|
330
|
+
const stmt = startFn[i]
|
|
331
|
+
if (!Array.isArray(stmt) || stmt[0] !== 'global.set' || writes.get(stmt[1]) !== 1) continue
|
|
332
|
+
const name = typeof stmt[1] === 'string' && stmt[1][0] === '$' ? stmt[1].slice(1) : null
|
|
333
|
+
const g = name && ctx.scope.globals.get(name)
|
|
334
|
+
const c = stmt[2]
|
|
335
|
+
if (!g || !g.mut || !ctx.scope.consts?.has(name) || !ctx.scope.userGlobals?.has(name)) continue
|
|
336
|
+
if (!Array.isArray(c) || c[0] !== `${g.type}.const`) continue
|
|
337
|
+
ctx.scope.globals.set(name, { ...g, mut: false, init: c[1] })
|
|
338
|
+
startFn.splice(i, 1)
|
|
339
|
+
}
|
|
340
|
+
// Hoisting can empty `__start`. The O2 watr pass prunes a bodyless start, but at
|
|
341
|
+
// O0/O1 nothing else does — drop it (func + directive) here so a const-only module
|
|
342
|
+
// carries no start at all.
|
|
343
|
+
if (findBodyStart(startFn) >= startFn.length)
|
|
344
|
+
for (let j = sec.start.length - 1; j >= 0; j--)
|
|
345
|
+
if (Array.isArray(sec.start[j]) && sec.start[j][1] === '$__start') sec.start.splice(j, 1)
|
|
346
|
+
}
|
|
347
|
+
|
|
288
348
|
/**
|
|
289
349
|
* Phase: closure-body dedup.
|
|
290
350
|
*
|
|
@@ -420,24 +480,188 @@ export function finalizeClosureTable(sec) {
|
|
|
420
480
|
for (const fn of sec.start) rewriteCalls(fn)
|
|
421
481
|
}
|
|
422
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Stdlib funcs actually reachable from the emitted program. Seeds from real
|
|
485
|
+
* `call`/`return_call`/`ref.func` sites in the user funcs, `__start`, and the elem
|
|
486
|
+
* table, then closes transitively over the stdlib call graph (each reached helper's
|
|
487
|
+
* template references). Conservative by construction — a template `$__foo` in a
|
|
488
|
+
* feature-dead branch is kept, never dropped — so it's safe to gate inclusion and the
|
|
489
|
+
* memory/allocator decision on it. An eagerly-`inc`'d helper that nothing calls is
|
|
490
|
+
* absent, which is the whole point.
|
|
491
|
+
*/
|
|
492
|
+
function reachableStdlib(sec) {
|
|
493
|
+
const stdlib = ctx.core.stdlib
|
|
494
|
+
const reach = new Set(), stack = []
|
|
495
|
+
// Track every reached name (module-namespace `math.sin` included), but only follow
|
|
496
|
+
// those with a stdlib template. Names match `$foo`, `$__foo`, `$math.sin_core` — the
|
|
497
|
+
// dotted module funcs are the ones the `$__`-only regex used to miss, pruning live code.
|
|
498
|
+
const add = (name) => { if (!reach.has(name)) { reach.add(name); if (stdlib[name] != null) stack.push(name) } }
|
|
499
|
+
const scanIR = (node) => {
|
|
500
|
+
if (!Array.isArray(node)) return
|
|
501
|
+
if ((node[0] === 'call' || node[0] === 'return_call' || node[0] === 'ref.func') &&
|
|
502
|
+
typeof node[1] === 'string' && node[1][0] === '$') add(node[1].slice(1))
|
|
503
|
+
for (const c of node) scanIR(c)
|
|
504
|
+
}
|
|
505
|
+
for (const fn of sec.funcs) scanIR(fn)
|
|
506
|
+
for (const fn of sec.start) scanIR(fn)
|
|
507
|
+
for (const e of sec.elem) // closure table: bare `$fn` func refs
|
|
508
|
+
if (Array.isArray(e)) for (const c of e) if (typeof c === 'string' && c[0] === '$') add(c.slice(1))
|
|
509
|
+
// A stdlib func that self-exports (`(export "__invoke_closure")`) is a host-facing
|
|
510
|
+
// entry point — the JS host calls it directly, so it's a root even when nothing in
|
|
511
|
+
// the wasm calls it. Mirrors treeshake's inline-export rooting.
|
|
512
|
+
for (const n of ctx.core.includes) {
|
|
513
|
+
const v = stdlib[n]
|
|
514
|
+
let t = ''
|
|
515
|
+
try { t = typeof v === 'function' ? v() : v } catch { t = '' }
|
|
516
|
+
if (typeof t === 'string' && t.includes('(export "')) add(n)
|
|
517
|
+
}
|
|
518
|
+
while (stack.length) {
|
|
519
|
+
const v = stdlib[stack.pop()]
|
|
520
|
+
let text = ''
|
|
521
|
+
try { text = typeof v === 'function' ? v() : v } catch { text = '' }
|
|
522
|
+
if (typeof text === 'string') for (const m of text.matchAll(/\$([A-Za-z_][A-Za-z0-9_.]*)/g)) add(m[1])
|
|
523
|
+
}
|
|
524
|
+
return reach
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// The f64x2 stdlib mirrors the lane vectorizer (optimize/vectorize.js) injects in the LATE 'post'
|
|
528
|
+
// pass — after the stdlib was pulled + treeshaken. Keep in sync with that pass's call-rewrite map
|
|
529
|
+
// (PPC_CALL2). These are the ONLY helpers appendLateStdlib may add; restricting to them avoids
|
|
530
|
+
// touching helpers that live in other module sections (ext-stdlib, imports) where a blind
|
|
531
|
+
// 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'])
|
|
533
|
+
|
|
534
|
+
// A late pass can reference one of the f64x2 mirrors that wasn't present when the stdlib was first
|
|
535
|
+
// assembled. Append any referenced-but-missing mirror body (fixpoint over their own calls, though
|
|
536
|
+
// the trig mirrors call nothing). moduleArr is mutated in place; non-mirror references are left for
|
|
537
|
+
// watr to resolve (a genuine missing helper is the kernel's own pull, already satisfied).
|
|
538
|
+
export function appendLateStdlib(moduleArr) {
|
|
539
|
+
const stdlib = ctx.core.stdlib
|
|
540
|
+
const have = new Set()
|
|
541
|
+
for (const n of moduleArr) if (Array.isArray(n) && n[0] === 'func' && typeof n[1] === 'string') have.add(n[1])
|
|
542
|
+
let added = true
|
|
543
|
+
while (added) {
|
|
544
|
+
added = false
|
|
545
|
+
const refs = new Set()
|
|
546
|
+
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) }
|
|
547
|
+
for (const n of moduleArr) scan(n)
|
|
548
|
+
for (const ref of refs) {
|
|
549
|
+
const name = ref.slice(1)
|
|
550
|
+
if (have.has(ref) || !LATE_VEC_HELPERS.has(name) || stdlib[name] == null) continue
|
|
551
|
+
const node = parseTemplate(typeof stdlib[name] === 'function' ? stdlib[name]() : stdlib[name])
|
|
552
|
+
moduleArr.push(node[0] === 'module' ? node[1] : node)
|
|
553
|
+
have.add(ref)
|
|
554
|
+
added = true
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
423
559
|
/**
|
|
424
560
|
* Phase: pull stdlib + memory.
|
|
425
561
|
*/
|
|
426
562
|
export function pullStdlib(sec) {
|
|
563
|
+
installHelperCounters()
|
|
427
564
|
resolveIncludes()
|
|
428
565
|
|
|
429
|
-
|
|
430
|
-
|
|
566
|
+
// Reachability, not inclusion, decides what the output needs. `ctx.core.includes`
|
|
567
|
+
// accumulates everything a module *might* use (eager module-load `inc`s + transitive
|
|
568
|
+
// deps), but a const array / static string literal calls none of it. So we seed from
|
|
569
|
+
// the actual call sites in the emitted funcs + __start (+ elem table) and close
|
|
570
|
+
// transitively over the stdlib call graph. An eagerly-included helper that nothing
|
|
571
|
+
// calls never enters this set — so allocator, memory, and exports reflect real use.
|
|
572
|
+
const reachable = reachableStdlib(sec)
|
|
573
|
+
const realize = (n) => { const v = ctx.core.stdlib[n]; try { return typeof v === 'function' ? v() : v } catch { return '' } }
|
|
574
|
+
|
|
575
|
+
// Two distinct needs, kept separate:
|
|
576
|
+
// · needsAlloc — the program allocates at runtime: an allocator func is reachable,
|
|
577
|
+
// or shared-mem string literals seed a pool __start allocs. Drives the bump
|
|
578
|
+
// allocator (`__alloc`/`__alloc_hdr`/`__clear`), the `__heap` pointer, and the
|
|
579
|
+
// `_alloc`/`_clear` marshalling exports.
|
|
580
|
+
// · needsMemory — linear memory must merely *exist*: we allocate, OR a literal lives
|
|
581
|
+
// in a static data segment (a const pointer, no allocator behind it), OR a reached
|
|
582
|
+
// helper / inline body does a load/store, OR `__ptr_type` is reached (the module
|
|
583
|
+
// discriminates heap tags — an `instanceof`/`typeof x==='object'` whose argument the
|
|
584
|
+
// host marshals across the boundary). A data segment with no memory is invalid wasm,
|
|
585
|
+
// so memory can't be gated on allocation alone.
|
|
586
|
+
const ALLOC_FUNCS = ['__alloc', '__alloc_hdr', '__alloc_hdr_n']
|
|
587
|
+
const needsAlloc = !!ctx.runtime.strPool || ALLOC_FUNCS.some(a => reachable.has(a))
|
|
588
|
+
// Memory ops can be emitted *inline* into user/start funcs (a heap-path char read
|
|
589
|
+
// loads without calling a stdlib helper), so scan the emitted bodies too.
|
|
590
|
+
const hasMemOp = (node) => Array.isArray(node) &&
|
|
591
|
+
((typeof node[0] === 'string' && MEM_OPS.test(node[0])) || node.some(hasMemOp))
|
|
592
|
+
// `ctx.runtime.data` is never empty here — the number module seeds a static stringify
|
|
593
|
+
// prefix (`NaNInfinity…`) at offset 0; stripStaticDataPrefix removes it when unused, so
|
|
594
|
+
// the real question is whether any data lives *beyond* that strippable prefix.
|
|
595
|
+
// An explicit `{ memory: pages }` / shared-memory option is a caller request to own
|
|
596
|
+
// linear memory (e.g. to marshal host values in), independent of what the wasm itself
|
|
597
|
+
// reaches — honour it even for an otherwise-memoryless program.
|
|
598
|
+
const explicitMemory = ctx.memory.pages > 0 || !!ctx.memory.shared
|
|
599
|
+
const needsMemory = needsAlloc || explicitMemory ||
|
|
600
|
+
(ctx.runtime.data?.length || 0) > (ctx.runtime.staticDataLen || 0) ||
|
|
601
|
+
reachable.has('__ptr_type') ||
|
|
602
|
+
[...reachable].some(n => MEM_OPS.test(realize(n))) ||
|
|
603
|
+
sec.funcs.some(hasMemOp) || sec.start.some(hasMemOp)
|
|
604
|
+
// Emit only what's reachable: drop every eagerly-`inc`'d *internal* helper the program
|
|
605
|
+
// never calls. This is what lets a const-array / static-string / atom module shed the
|
|
606
|
+
// allocator, pointer dispatchers, and length helpers that an array/object module load
|
|
607
|
+
// pulled in wholesale — and it keeps the dead allocator from dangling on the `$__heap`
|
|
608
|
+
// we delete below. Scoped to `__`-prefixed names: module-namespace funcs (`math.sin`)
|
|
609
|
+
// are pulled in on demand, never eagerly, so they're already minimal and never pruned
|
|
610
|
+
// here (guarding against any reachability blind spot in a dotted-name template).
|
|
611
|
+
for (const n of [...ctx.core.includes]) if (n.startsWith('__') && !reachable.has(n)) ctx.core.includes.delete(n)
|
|
612
|
+
// Lazy Eisel-Lemire table injection: only when __dec_to_f64 (correctly-rounded
|
|
613
|
+
// decimal→f64, module/number.js) survived pruning — append its trimmed 131-entry
|
|
614
|
+
// (~2KB) power-of-10 table and declare $__el_tbl = that offset. Must run HERE so
|
|
615
|
+
// dataPages (below) accounts for the addition; keeps it out of programs that never
|
|
616
|
+
// parse decimals at runtime.
|
|
617
|
+
if (ctx.core.includes.has('__dec_to_f64') && ctx.runtime.elTable) {
|
|
618
|
+
const elBefore = ctx.runtime.data.length
|
|
619
|
+
while (ctx.runtime.data.length % 8 !== 0) ctx.runtime.data += '\0'
|
|
620
|
+
const elTblOff = ctx.runtime.data.length
|
|
621
|
+
ctx.runtime.data += ctx.runtime.elTable
|
|
622
|
+
declGlobal('__el_tbl', 'i32', elTblOff, { mut: false })
|
|
623
|
+
ctx.runtime.elTable = null // prevent double-injection on re-entry (null-sentinel; jz forbids delete)
|
|
624
|
+
// Reachability here OVER-counts __dec_to_f64: a dead inlined helper's `arr[i] | 0`
|
|
625
|
+
// on an untyped param pulls __to_num → __dec_to_f64, landing the table even when no
|
|
626
|
+
// LIVE code parses decimals. Record the appended span (padding + table, always the
|
|
627
|
+
// data tail — it's the last append) so stripDeadElTable can drop it post-lowering,
|
|
628
|
+
// once reachability is exact. See stripDeadElTable.
|
|
629
|
+
ctx.runtime.elTableLen = ctx.runtime.data.length - elBefore
|
|
630
|
+
}
|
|
631
|
+
if (!needsAlloc) { ctx.scope.globals.delete('__heap'); ctx.scope.globals.delete('__heap_reset') }
|
|
431
632
|
if (needsMemory && ctx.module.modules.core) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
633
|
+
if (needsAlloc) {
|
|
634
|
+
for (const fn of ['__alloc', '__alloc_hdr', '__clear']) ctx.core.includes.add(fn)
|
|
635
|
+
// Late-add of allocators may pull in transitive deps (__alloc → __memgrow,
|
|
636
|
+
// etc.) that the initial resolveIncludes did not yet see; re-resolve.
|
|
637
|
+
// No-op when the alloc trio was already present.
|
|
638
|
+
resolveIncludes()
|
|
639
|
+
// Record the post-init heap top into `__heap_reset` so `__clear` rewinds to
|
|
640
|
+
// just above this module's init-time heap state (e.g. the self-host compiler's
|
|
641
|
+
// GLOBALS/atom tables), not into it. Done here — where `__heap` is known to
|
|
642
|
+
// survive — as the last `__start` action before any non-returning timer loop.
|
|
643
|
+
// No `__start` ⇒ no init allocations ⇒ `__heap_reset`'s data-end seed is right.
|
|
644
|
+
if (!ctx.memory.shared && ctx.scope.globals.has('__heap_reset')) {
|
|
645
|
+
const startFn = sec.start.find(n => Array.isArray(n) && n[0] === 'func' && n[1] === '$__start')
|
|
646
|
+
if (startFn) {
|
|
647
|
+
const capture = ['global.set', '$__heap_reset', ['global.get', '$__heap']]
|
|
648
|
+
const tail = startFn[startFn.length - 1]
|
|
649
|
+
if (Array.isArray(tail) && tail[0] === 'call' && tail[1] === '$__timer_loop') startFn.splice(startFn.length - 1, 0, capture)
|
|
650
|
+
else startFn.push(capture)
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// Initial pages must cover the static data segment (it loads at instantiation), not
|
|
655
|
+
// just the default 1 — otherwise a module whose constants exceed 64 KiB emits a data
|
|
656
|
+
// segment that overflows its own memory. The heap grows past this on demand via
|
|
657
|
+
// __memgrow. (Shared memory loads literals via memory.init into allocated space, so
|
|
658
|
+
// its initial size isn't pinned by the data length.)
|
|
659
|
+
const dataPages = ctx.memory.shared ? 0 : Math.ceil((ctx.runtime.data?.length || 0) / 65536)
|
|
660
|
+
const pages = Math.max(ctx.memory.pages || 1, dataPages)
|
|
661
|
+
const max = ctx.memory.max || 0 // 0 = no maximum (unbounded growth)
|
|
662
|
+
if (ctx.memory.shared) sec.imports.push(['import', '"env"', '"memory"', max ? ['memory', pages, max] : ['memory', pages]])
|
|
663
|
+
else sec.memory.push(max ? ['memory', ['export', '"memory"'], pages, max] : ['memory', ['export', '"memory"'], pages])
|
|
664
|
+
if (needsAlloc && ctx.transform.alloc !== false && ctx.core._allocRawFuncs)
|
|
441
665
|
sec.funcs.push(...ctx.core._allocRawFuncs.map(parseTemplate))
|
|
442
666
|
}
|
|
443
667
|
|
|
@@ -455,7 +679,7 @@ export function pullStdlib(sec) {
|
|
|
455
679
|
}
|
|
456
680
|
}
|
|
457
681
|
for (const n of ctx.core.includes) if (!ctx.core.stdlib[n]) err(`internal: stdlib '${n}' was requested but never registered (this is a jz bug — feature pulled in something it can't deliver)`)
|
|
458
|
-
sec.stdlib.push(...[...ctx.core.includes].map(n => parseTemplate(stdlibStr(n))))
|
|
682
|
+
sec.stdlib.push(...[...ctx.core.includes].map(n => instrumentHelperCounter(n, parseTemplate(stdlibStr(n)))))
|
|
459
683
|
}
|
|
460
684
|
|
|
461
685
|
export function syncImports(sec) {
|
|
@@ -524,17 +748,58 @@ export function optimizeModule(sec, profiler) {
|
|
|
524
748
|
if (dataLen > 1024 && !ctx.memory.shared) {
|
|
525
749
|
const heapBase = (dataLen + 7) & ~7
|
|
526
750
|
// Non-shared memory always carries a $__heap global — start it past the
|
|
527
|
-
// static data so the bump allocator never overwrites a literal.
|
|
751
|
+
// static data so the bump allocator never overwrites a literal. `__heap_reset`
|
|
752
|
+
// seeds to the same data end (its runtime value is overwritten by `__start`'s
|
|
753
|
+
// tail capture for modules that init-allocate; this seed serves modules with no
|
|
754
|
+
// `__start`, where the data end IS the correct rewind point). `__clear` reads
|
|
755
|
+
// `$__heap_reset` directly, so no per-function constant patch is needed.
|
|
528
756
|
declGlobal('__heap', 'i32', heapBase, { export: '__heap' })
|
|
757
|
+
if (ctx.scope.globals.has('__heap_reset')) declGlobal('__heap_reset', 'i32', heapBase)
|
|
529
758
|
if (ctx.scope.globals.has('__heap_start')) declGlobal('__heap_start', 'i32', heapBase)
|
|
530
|
-
for (const s of sec.stdlib)
|
|
531
|
-
if (s[0] === 'func' && s[1] === '$__clear')
|
|
532
|
-
for (let i = 2; i < s.length; i++)
|
|
533
|
-
if (Array.isArray(s[i]) && s[i][0] === 'global.set' && Array.isArray(s[i][2]) && s[i][2][0] === 'i32.const')
|
|
534
|
-
s[i][2][1] = `${heapBase}`
|
|
535
759
|
}
|
|
536
760
|
}
|
|
537
761
|
|
|
762
|
+
/**
|
|
763
|
+
* Phase: strip the Eisel-Lemire table when it is dead.
|
|
764
|
+
*
|
|
765
|
+
* pullStdlib injects the ~2 KB power-of-10 table whenever `__dec_to_f64` is *reachable*,
|
|
766
|
+
* but that over-counts: a dead inlined helper's `arr[i] | 0` on an untyped param pulls
|
|
767
|
+
* `__to_num` → `__dec_to_f64`, so the table lands even in a module no live code parses
|
|
768
|
+
* decimals in. watr later treeshakes the dead function + its `$__el_tbl` global, but it
|
|
769
|
+
* does NOT treeshake the data segment — so the orphaned table bloated every module ~2 KB.
|
|
770
|
+
*
|
|
771
|
+
* This runs LAST (after every lowering has emitted its call/ref.func — doing it earlier is
|
|
772
|
+
* unsound: refs like `util.clone` are emitted *after* pullStdlib), so a mark-sweep from the
|
|
773
|
+
* real roots (inline-exported funcs, __start, the closure table, globals/tags/table) gives
|
|
774
|
+
* EXACT liveness. If `__dec_to_f64` is dead, truncate the table from the data tail (it is
|
|
775
|
+
* the last append — see pullStdlib). DATA only: the dead function + global are left for
|
|
776
|
+
* watr, which already removes them. Keeps correctly-rounded decimal parsing wherever it is
|
|
777
|
+
* genuinely live (parseFloat, the self-host compiler's `Number()` on source literals).
|
|
778
|
+
*/
|
|
779
|
+
export function stripDeadElTable(sec) {
|
|
780
|
+
if (!ctx.runtime.elTableLen) return
|
|
781
|
+
const byName = new Map()
|
|
782
|
+
for (const arr of [sec.funcs, sec.stdlib, sec.start])
|
|
783
|
+
for (const f of arr || []) if (Array.isArray(f) && f[0] === 'func' && typeof f[1] === 'string') byName.set(f[1], f)
|
|
784
|
+
const live = new Set(), work = []
|
|
785
|
+
const mark = (ref) => { if (typeof ref === 'string' && byName.has(ref) && !live.has(ref)) { live.add(ref); work.push(ref) } }
|
|
786
|
+
const scan = (n) => {
|
|
787
|
+
if (!Array.isArray(n)) return
|
|
788
|
+
if ((n[0] === 'call' || n[0] === 'return_call' || n[0] === 'ref.func') && typeof n[1] === 'string') mark(n[1])
|
|
789
|
+
for (const c of n) scan(c)
|
|
790
|
+
}
|
|
791
|
+
for (const f of sec.funcs) if (f.some(el => Array.isArray(el) && el[0] === 'export')) mark(f[1])
|
|
792
|
+
for (const f of sec.start) scan(f)
|
|
793
|
+
for (const part of [sec.elem, sec.globals, sec.tags, sec.table]) for (const n of part || []) {
|
|
794
|
+
if (!Array.isArray(n)) continue
|
|
795
|
+
for (const c of n) { if (typeof c === 'string' && c[0] === '$') mark(c); else scan(c) }
|
|
796
|
+
}
|
|
797
|
+
while (work.length) scan(byName.get(work.pop()))
|
|
798
|
+
if (live.has('$__dec_to_f64')) return // genuinely parses decimals at runtime — keep it
|
|
799
|
+
ctx.runtime.data = ctx.runtime.data.slice(0, ctx.runtime.data.length - ctx.runtime.elTableLen)
|
|
800
|
+
ctx.runtime.elTableLen = 0
|
|
801
|
+
}
|
|
802
|
+
|
|
538
803
|
/**
|
|
539
804
|
* Phase: strip static-data prefix.
|
|
540
805
|
*/
|
package/src/widen.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Numeric widening thresholds — the SINGLE SOURCE for "when does an i32 arithmetic
|
|
3
|
+
* op stay i32 vs widen to f64", shared by the two phases that must agree:
|
|
4
|
+
* • emit.js DECIDES — emits `i32.mul`/`i32.add` or widens to `f64.mul`/`f64.add`
|
|
5
|
+
* • type.js MIRRORS — exprType predicts the same i32/f64 so locals are typed right
|
|
6
|
+
*
|
|
7
|
+
* SOUNDNESS INVARIANT (one-way, unforgiving): exprType's i32 verdict must be a SUBSET
|
|
8
|
+
* of emit's — exprType may answer i32 only where emit DEFINITELY produces i32. If type
|
|
9
|
+
* says i32 but emit yields f64, the result is `trunc_sat`-narrowed back to i32 → silent
|
|
10
|
+
* miscompile. The two predicates can't share a function (emit reads IR values via
|
|
11
|
+
* isLit/maskBound, type reads AST via staticValue), but they MUST share this threshold,
|
|
12
|
+
* or a future edit to one silently drifts the other out of the safe subset.
|
|
13
|
+
*
|
|
14
|
+
* @module widen
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// JS `*` is an f64 multiply; `i32.mul` agrees only while the exact product stays
|
|
18
|
+
// f64-exact (|product| ≤ 2^53). Against the full i32 range (2^31) of one operand, the
|
|
19
|
+
// other must be bounded |v| ≤ 2^22 for the product to hold within 2^53 — so a literal
|
|
20
|
+
// or provably-masked operand of that magnitude keeps the multiply on `i32.mul`.
|
|
21
|
+
export const FITS_I32_MAX = 0x400000 // 2^22
|