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/compile/index.js
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
|
|
28
28
|
import parseWat from 'watr/parse'
|
|
29
29
|
import { ctx, err, inc, resolveIncludes, PTR, LAYOUT, declGlobal } from '../ctx.js'
|
|
30
|
-
import { T, isBlockBody, isReassigned, refsName, REFS_IN_EXPR } from '../ast.js'
|
|
30
|
+
import { T, isBlockBody, isReassigned, refsName, REFS_IN_EXPR, returnExprs } from '../ast.js'
|
|
31
|
+
import { valTypeOf } from '../kind.js'
|
|
31
32
|
import { intLiteralValue } from '../static.js'
|
|
32
33
|
import {
|
|
33
34
|
analyzeBody, unboxablePtrs, cseSafeLoadBases, boxedCaptures,
|
|
@@ -37,6 +38,14 @@ import { typedElemAux } from '../../layout.js'
|
|
|
37
38
|
import { VAL, updateRep, REP_FIELDS } from '../reps.js'
|
|
38
39
|
import { inferLocals } from './infer.js'
|
|
39
40
|
import { optimizeFunc, treeshake } from '../optimize/index.js'
|
|
41
|
+
import { strengthReduceLoopDivMod } from './loop-divmod.js'
|
|
42
|
+
import { narrowBoundedSquare } from './loop-square.js'
|
|
43
|
+
import { peelClampedStencil } from './peel-stencil.js'
|
|
44
|
+
import { cseLoads } from './cse-load.js'
|
|
45
|
+
|
|
46
|
+
// Monotonic across all functions so a CSE temp never collides (even after later inlining).
|
|
47
|
+
let __cseCtr = 0
|
|
48
|
+
const freshCseName = () => `${T}cse${__cseCtr++}`
|
|
40
49
|
import { emit, emitter, emitVoid, emitBlockBody } from './emit.js'
|
|
41
50
|
import { emitCharDecompPrologue, JSS_IMPORT_SIGS } from '../abi/string.js'
|
|
42
51
|
import {
|
|
@@ -54,10 +63,12 @@ import {
|
|
|
54
63
|
I32_MIN, I32_MAX, dollar,
|
|
55
64
|
} from '../ir.js'
|
|
56
65
|
import plan from './plan/index.js'
|
|
66
|
+
import { foldStaticConstAggregates } from './plan/literals.js'
|
|
57
67
|
import {
|
|
58
68
|
buildStartFn, dedupClosureBodies, finalizeClosureTable,
|
|
59
|
-
pullStdlib, syncImports, optimizeModule, stripStaticDataPrefix,
|
|
69
|
+
pullStdlib, syncImports, optimizeModule, stripStaticDataPrefix, hoistConstGlobalInits, stripDeadElTable,
|
|
60
70
|
} from '../wat/assemble.js'
|
|
71
|
+
import { instrumentHelperCallsites } from '../helper-counters.js'
|
|
61
72
|
|
|
62
73
|
// =============================================================================
|
|
63
74
|
// Single-source export semantics
|
|
@@ -130,15 +141,21 @@ const timePhase = (profiler, name, fn) => profiler?.time ? profiler.time(name, f
|
|
|
130
141
|
* fractional Number gets the same truncation it would get from `arr[n]`).
|
|
131
142
|
*/
|
|
132
143
|
const isBoundaryWrapped = (func) => {
|
|
133
|
-
if (!isExported(func) || func.raw
|
|
144
|
+
if (!isExported(func) || func.raw) return false
|
|
145
|
+
// Multi-value return: every lane is an f64 NaN-box carrier (the `return [a,b,…]` emit forces
|
|
146
|
+
// asF64 per lane; result narrowing only touches single-result funcs), so any lane may hold a
|
|
147
|
+
// box whose NaN payload JSC/V8 erases at the boundary — wrap to i64-carry every lane.
|
|
148
|
+
if (func.sig.results.length !== 1) return true
|
|
134
149
|
if (func.sig.results[0] !== 'f64' || func.sig.ptrKind != null) return true
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
if (func.
|
|
141
|
-
|
|
150
|
+
// Any result that isn't a proven plain number can be a NaN-box — a heap pointer,
|
|
151
|
+
// a null/undef/bool atom, a bigint carrier, or a dynamic value — so it crosses as
|
|
152
|
+
// i64 and JSC (Safari) can't canonicalize the payload away. A proven-number result
|
|
153
|
+
// stays f64: free, and a number is never a NaN-box. `_resultNumeric` is set in
|
|
154
|
+
// analyzeFuncForEmit (covers value-bound arrows narrowValResults skips).
|
|
155
|
+
if (!func._resultNumeric) return true
|
|
156
|
+
// Number result, but a param may still carry a box — a pointer-ABI param, or a
|
|
157
|
+
// dynamic f64 param flagged `boundaryI64` during analyze — so wrap for i64 params.
|
|
158
|
+
return func.sig.params.some(p => p.type !== 'f64' || p.ptrKind != null || p.boundaryI64)
|
|
142
159
|
}
|
|
143
160
|
|
|
144
161
|
// Static-string intern index (the `internStrings` pass). Open-addressing table
|
|
@@ -209,20 +226,39 @@ const ensureThrowRuntime = (sec) => {
|
|
|
209
226
|
sec.tags.push(['export', '"__jz_last_err_bits"', ['global', '$__jz_last_err_bits']])
|
|
210
227
|
}
|
|
211
228
|
|
|
212
|
-
// Drop the $__jz_err tag + __jz_last_err_bits globals when
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
// throw
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
229
|
+
// Drop the $__jz_err tag + __jz_last_err_bits globals when no throw can be CAUGHT.
|
|
230
|
+
// ensureThrowRuntime runs before optimizeModule so dead-throw analysis sees the
|
|
231
|
+
// tag/global as live; once opt has finished, an unused tag still forces consumers
|
|
232
|
+
// (wasmtime, wasm2c, wabt) to enable the exceptions proposal just to PARSE the module.
|
|
233
|
+
//
|
|
234
|
+
// When `!userThrows`, every `throw` is compiler-internal (bounds / coercion / type
|
|
235
|
+
// errors) and — with no user try/catch — uncatchable: nothing inspects the thrown
|
|
236
|
+
// value, so it is semantically a trap. The exceptions proposal is needed only to
|
|
237
|
+
// DECLARE the tag a `throw` references; lowering each surviving uncatchable throw to
|
|
238
|
+
// `unreachable` keeps the module in the wasm MVP, so every runtime can parse it
|
|
239
|
+
// (V8 alone enables exceptions by default, which masked this). A pure-recursion or
|
|
240
|
+
// typed-array kernel (nqueens, anything pulling __to_num) thus stops emitting a Tag
|
|
241
|
+
// section it can never use. User-written throw/try/catch/finally is an ABI contract
|
|
242
|
+
// (JS-side may inspect __jz_last_err_bits), so `userThrows` keeps the runtime intact.
|
|
221
243
|
const pruneUnusedThrowRuntime = (sec) => {
|
|
222
244
|
if (!ctx.runtime.throws || ctx.runtime.userThrows) return
|
|
223
|
-
|
|
245
|
+
// A catch handler (try_table) appears only under userThrows; defensively bail if one
|
|
246
|
+
// is present so a caught throw is never silently turned into a trap.
|
|
247
|
+
const hasCatch = (n) => Array.isArray(n) &&
|
|
248
|
+
(n[0] === 'try_table' || n[0] === 'catch' || n[0] === 'catch_all' || n.some(hasCatch))
|
|
249
|
+
for (const arr of [sec.funcs, sec.stdlib, sec.start])
|
|
250
|
+
for (const f of arr) if (hasCatch(f)) return
|
|
251
|
+
// Rewrite every surviving `(throw $__jz_err …)` to `(unreachable)` (same polymorphic
|
|
252
|
+
// stack type — a drop-in in any position). The thrown operand is side-effect-free
|
|
253
|
+
// (a local read / const), so dropping it loses nothing.
|
|
254
|
+
const lowerThrows = (n) => {
|
|
255
|
+
if (!Array.isArray(n)) return n
|
|
256
|
+
if (n[0] === 'throw') return ['unreachable']
|
|
257
|
+
for (let i = 1; i < n.length; i++) n[i] = lowerThrows(n[i])
|
|
258
|
+
return n
|
|
259
|
+
}
|
|
224
260
|
for (const arr of [sec.funcs, sec.stdlib, sec.start])
|
|
225
|
-
for (
|
|
261
|
+
for (let i = 0; i < arr.length; i++) arr[i] = lowerThrows(arr[i])
|
|
226
262
|
sec.tags = sec.tags.filter(t => !(Array.isArray(t) &&
|
|
227
263
|
((t[0] === 'tag' && t[1] === '$__jz_err') ||
|
|
228
264
|
(t[0] === 'export' && t[1] === '"__jz_last_err_bits"'))))
|
|
@@ -346,7 +382,9 @@ function scanAndTagNonEscapingClosures(body) {
|
|
|
346
382
|
// their synthetic labels can't collide with the parent frame's.
|
|
347
383
|
function enterFunc(sig, body, { uniq = 0, directClosures = null } = {}) {
|
|
348
384
|
ctx.func.stack = []
|
|
385
|
+
ctx.func.zeroInitSeen = new Set() // names whose `let x=0` zero-init was elided once; a 2nd is a real re-init (unrolled bodies)
|
|
349
386
|
ctx.func.maybeNullish = new Set() // bindings assigned a nullish literal → coerce in arithmetic (null-flow)
|
|
387
|
+
ctx.func.refinements = new Map() // flow-sensitive type facts (typeof/instanceof guards) — per-function; clear so none leak across bodies
|
|
350
388
|
ctx.func.pendingLabel = null // label awaiting its loop, for `continue <label>`
|
|
351
389
|
ctx.func.uniq = uniq
|
|
352
390
|
ctx.func.current = sig
|
|
@@ -379,6 +417,19 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
379
417
|
const { paramReps } = programFacts
|
|
380
418
|
if (func.raw) return null
|
|
381
419
|
|
|
420
|
+
// Strength-reduce per-iteration `i % w` / `(i/w)|0` to incremental i32 counters
|
|
421
|
+
// (idempotent: a reduced loop has no modulo left to match). Before analyze so the
|
|
422
|
+
// counters are typed/narrowed like any i32 local. Off at L0 / `loopIVDivMod:false`.
|
|
423
|
+
const _o = ctx.transform.optimize
|
|
424
|
+
if (_o && _o.loopIVDivMod !== false && isBlockBody(func.body)) func.body = strengthReduceLoopDivMod(func.body)
|
|
425
|
+
// Bounded-square narrowing: `i*i` under an `i*i < CONST` (CONST ≤ 2³⁰) guard → Math.imul,
|
|
426
|
+
// so the sieve's product/counter chain carries i32 instead of f64. Before analyze so the
|
|
427
|
+
// Math.imul typed/narrows like any i32. Off at L0 / `loopSquare:false`.
|
|
428
|
+
if (_o && _o.loopSquare !== false && isBlockBody(func.body)) func.body = narrowBoundedSquare(func.body)
|
|
429
|
+
// Edge-clamp peeling: split a clamped stencil loop into clamp-free interior + edges
|
|
430
|
+
// (the interior then lifts to SIMD). Before analyze so the new loops are analyzed.
|
|
431
|
+
if (_o && _o.clampPeel !== false && isBlockBody(func.body)) func.body = peelClampedStencil(func.body)
|
|
432
|
+
|
|
382
433
|
const { name, body, sig } = func
|
|
383
434
|
enterFunc(sig, body)
|
|
384
435
|
|
|
@@ -412,15 +463,28 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
412
463
|
// ToNumber string-parse dep tree (`__to_str`→`__itoa`/`__toExp`/`__mkstr`/…)
|
|
413
464
|
// treeshake away — a ~4× module shrink that, decisively, lets V8 tier the hot
|
|
414
465
|
// fill loop up properly (the bloated module JITs the *identical* loop ~2× slower).
|
|
415
|
-
|
|
466
|
+
// Block AND expression bodies: value-bound arrows (`export let f = (a,b) => a*b`) are
|
|
467
|
+
// skipped by narrowValResults, so without trusting their params here they'd fall to the
|
|
468
|
+
// i64 boundary carrier. The closure path runs the same proof at line ~1300.
|
|
469
|
+
if (func.exported) {
|
|
416
470
|
for (const p of sig.params) {
|
|
417
471
|
if (p.type === 'f64' && p.ptrKind == null && !p.jsstring
|
|
418
472
|
&& !func.defaults?.[p.name] && !ctx.func.boxed?.has(p.name)
|
|
419
473
|
&& !ctx.func.localReps?.get(p.name)?.val
|
|
420
|
-
|
|
474
|
+
// Numeric either by PROOF (ToNumber-forcing uses) or by the export
|
|
475
|
+
// boundary contract (never used as a string → wrapVal guarantees a
|
|
476
|
+
// number). The latter catches `acc + cre` float kernels whose `+` would
|
|
477
|
+
// otherwise pull a per-iteration string-concat fork (julia, floatbeats).
|
|
478
|
+
&& (paramAllUsesNumeric(body, p.name) || paramNeverString(body, p.name)))
|
|
421
479
|
updateRep(p.name, { val: VAL.NUMBER })
|
|
422
480
|
}
|
|
423
481
|
}
|
|
482
|
+
// Sound load-CSE: cache a repeated pure typed-array load `arr[idx]` when every intervening
|
|
483
|
+
// store writes a provably-different element (idx2 ≠ idx). Recovers the fft butterfly's redundant
|
|
484
|
+
// `re[a]` load. Before analyze so the introduced temp is typed/narrowed like any local.
|
|
485
|
+
if (_o && _o.loadCSE !== false && block && ctx.types.typedElem?.size)
|
|
486
|
+
cseLoads(body, n => ctx.types.typedElem.has(n), freshCseName)
|
|
487
|
+
|
|
424
488
|
if (block) {
|
|
425
489
|
seedLocalIntConsts(body)
|
|
426
490
|
}
|
|
@@ -434,6 +498,8 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
434
498
|
if (bodyFacts?.valTypes) {
|
|
435
499
|
for (const [name, vt] of bodyFacts.valTypes) updateRep(name, { val: vt })
|
|
436
500
|
}
|
|
501
|
+
// Never-relocated array bindings — the `[]` reader skips the forwarding follow.
|
|
502
|
+
if (bodyFacts?.neverGrown) for (const name of bodyFacts.neverGrown) updateRep(name, { neverGrown: true })
|
|
437
503
|
// Proven uint32 accumulator locals — readVar tags reads `.unsigned` so the
|
|
438
504
|
// f64 round-trip widens with convert_i32_u (not _s).
|
|
439
505
|
if (bodyFacts?.unsignedLocals) for (const n of bodyFacts.unsignedLocals) updateRep(n, { unsigned: true })
|
|
@@ -512,6 +578,32 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
512
578
|
if (ctx.func.localReps?.get(name)?.intCertain === true) cellTypes.add(name)
|
|
513
579
|
}
|
|
514
580
|
|
|
581
|
+
// Snapshot each param's JS-boundary carrier while reps are live — synthesizeBoundaryWrappers
|
|
582
|
+
// runs after they're torn down. A dynamic f64 param crosses as i64 (the carrier JSC can't
|
|
583
|
+
// canonicalize) iff it can hold a NaN-box, i.e. it isn't proven numeric. Numeric (NUMBER /
|
|
584
|
+
// BOOL → 0/1) params keep f64; pointer-ABI (ptrKind, type i32) and jsstring params are
|
|
585
|
+
// classified directly in the wrapper, so leave their flag false here.
|
|
586
|
+
if (isExported(func)) for (const p of sig.params) {
|
|
587
|
+
if (p.jsstring || p.ptrKind != null || p.type !== 'f64') { p.boundaryI64 = false; continue }
|
|
588
|
+
const rv = ctx.func.localReps?.get(p.name)?.val
|
|
589
|
+
p.boundaryI64 = rv !== VAL.NUMBER && rv !== VAL.BOOL
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Result-numeric proof for the boundary carrier. Block bodies get func.valResult from
|
|
593
|
+
// narrowValResults; value-bound arrows (`export let f = (a,b) => a*b`) don't, so prove via
|
|
594
|
+
// the return expression(s) with params now trusted numeric. A proven-number f64 result
|
|
595
|
+
// never carries a NaN-box → crosses as plain f64; anything else rides i64 (Safari-safe).
|
|
596
|
+
if (isExported(func))
|
|
597
|
+
func._resultNumeric = func.valResult === VAL.NUMBER ||
|
|
598
|
+
(func.valResult == null && sig.results[0] === 'f64' &&
|
|
599
|
+
(() => {
|
|
600
|
+
const rex = returnExprs(body)
|
|
601
|
+
// Void body (falls off → undefined, which callers ignore) keeps the f64 carrier:
|
|
602
|
+
// undefined isn't a reference, so no i64 is needed and wrapping every void export
|
|
603
|
+
// is pure overhead. A non-empty set must be all-NUMBER to stay f64.
|
|
604
|
+
return rex.length === 0 || rex.every(e => valTypeOf(e) === VAL.NUMBER)
|
|
605
|
+
})())
|
|
606
|
+
|
|
515
607
|
return {
|
|
516
608
|
block,
|
|
517
609
|
locals: new Map(ctx.func.locals),
|
|
@@ -520,27 +612,61 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
520
612
|
flatObjects: new Map(ctx.func.flatObjects),
|
|
521
613
|
sliceViews: new Set(ctx.func.sliceViews),
|
|
522
614
|
cseLoadBases,
|
|
615
|
+
distinctParams: func.distinctParams || null,
|
|
523
616
|
typedElem: ctx.types.typedElem ? new Map(ctx.types.typedElem) : null,
|
|
524
617
|
localReps: cloneRepMap(ctx.func.localReps),
|
|
525
618
|
}
|
|
526
619
|
}
|
|
527
620
|
|
|
528
621
|
function seedLocalIntConsts(body) {
|
|
622
|
+
// Fold each never-reassigned local `const`/`let NAME = EXPR` to a known i32, so a
|
|
623
|
+
// divisor / bound / size built from earlier consts (`rr = R|0; win = 2*rr+1`) becomes
|
|
624
|
+
// a compile-time literal — which lets the int-divide lowering hand the wasm backend a
|
|
625
|
+
// constant divisor to magic-multiply (no runtime sdiv), array bounds resolve, etc.
|
|
626
|
+
// Mirrors the module-scope fold (evalConst above); a string ref resolves through the
|
|
627
|
+
// intConst already recorded on its rep, and the fixpoint lets a later const see an
|
|
628
|
+
// earlier one regardless of declaration order. Skips nested functions (own scope).
|
|
629
|
+
const evalC = (n) => {
|
|
630
|
+
if (typeof n === 'number') return Number.isInteger(n) ? n : null
|
|
631
|
+
if (Array.isArray(n) && n[0] == null && typeof n[1] === 'number') return Number.isInteger(n[1]) ? n[1] : null
|
|
632
|
+
if (typeof n === 'string') return intLiteralValue(n) // a seeded intConst / literal local
|
|
633
|
+
if (!Array.isArray(n)) return null
|
|
634
|
+
const [op, a, b] = n
|
|
635
|
+
const va = evalC(a); if (va == null) return null
|
|
636
|
+
if (op === 'u-' || (op === '-' && b === undefined)) return -va
|
|
637
|
+
const vb = evalC(b); if (vb == null) return null
|
|
638
|
+
switch (op) {
|
|
639
|
+
case '+': return va + vb; case '-': return va - vb; case '*': return va * vb
|
|
640
|
+
case '&': return va & vb; case '|': return va | vb; case '^': return va ^ vb
|
|
641
|
+
case '<<': return va << vb; case '>>': return va >> vb; case '>>>': return va >>> vb
|
|
642
|
+
default: return null
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const decls = []
|
|
529
646
|
const walk = (node) => {
|
|
530
647
|
if (!Array.isArray(node)) return
|
|
531
648
|
const [op, ...args] = node
|
|
532
649
|
if (op === '=>') return
|
|
533
650
|
if (op === 'let' || op === 'const') {
|
|
534
|
-
for (const decl of args)
|
|
535
|
-
if (
|
|
536
|
-
const value = intLiteralValue(decl[2])
|
|
537
|
-
if (value != null && !isReassigned(body, decl[1])) updateRep(decl[1], { intConst: value })
|
|
538
|
-
}
|
|
651
|
+
for (const decl of args)
|
|
652
|
+
if (Array.isArray(decl) && decl[0] === '=' && typeof decl[1] === 'string' && !isReassigned(body, decl[1])) decls.push(decl)
|
|
539
653
|
return
|
|
540
654
|
}
|
|
541
655
|
for (const arg of args) walk(arg)
|
|
542
656
|
}
|
|
543
657
|
walk(body)
|
|
658
|
+
const seeded = new Set()
|
|
659
|
+
let changed = true
|
|
660
|
+
while (changed) {
|
|
661
|
+
changed = false
|
|
662
|
+
for (const decl of decls) {
|
|
663
|
+
if (seeded.has(decl[1])) continue
|
|
664
|
+
const value = evalC(decl[2])
|
|
665
|
+
if (value != null && Number.isInteger(value) && value >= I32_MIN && value <= I32_MAX) {
|
|
666
|
+
updateRep(decl[1], { intConst: value }); seeded.add(decl[1]); changed = true
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
544
670
|
}
|
|
545
671
|
|
|
546
672
|
// ── Loop-invariant exported-param coercion hoist ────────────────────────────
|
|
@@ -562,21 +688,47 @@ function seedLocalIntConsts(body) {
|
|
|
562
688
|
const PARAM_REASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=',
|
|
563
689
|
'^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??=', '++', '--'])
|
|
564
690
|
// Binary ops that unconditionally ToNumber BOTH operands, so a bare param operand
|
|
565
|
-
// is a pure numeric use. `+` is excluded (may concatenate);
|
|
566
|
-
//
|
|
691
|
+
// is a pure numeric use. `+` is excluded (may concatenate); `===`/`==` are excluded
|
|
692
|
+
// (they branch on type, never coerce a string operand to number).
|
|
567
693
|
const NUM_BIN_OPS = new Set(['*', '/', '%', '**', '&', '|', '^', '<<', '>>', '>>>'])
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
694
|
+
// Relational ops: jz has no lexicographic compare for an untyped operand — `<`
|
|
695
|
+
// lowers to `f64.lt`, taking the string path only when a *known-string* operand
|
|
696
|
+
// is present (emit.js cmpOp). So a bare param compared against a non-string is a
|
|
697
|
+
// pure numeric use, same as NUM_BIN_OPS. A string-literal counterpart (`x < "m"`)
|
|
698
|
+
// signals string intent and is rejected (handled in the walk below).
|
|
699
|
+
const REL_OPS = new Set(['<', '<=', '>', '>='])
|
|
700
|
+
// A string literal/template operand poisons relational numeric inference.
|
|
701
|
+
const isStrLiteral = (n) => Array.isArray(n) && (n[0] === 'str' || n[0] === 'template')
|
|
702
|
+
|
|
703
|
+
/** True iff every use of param `name` in `body` is numeric-COMPATIBLE *and* at
|
|
704
|
+
* least one use is numeric-PROVING — so coercing it to a number once at entry is
|
|
705
|
+
* observationally exact. Two verdict levels guard against a polymorphic slot
|
|
706
|
+
* passing on absence of evidence:
|
|
707
|
+
* - PROVING (`proven=true`): arithmetic / relational / bitwise / unary operand —
|
|
708
|
+
* JS ToNumbers these, and a string/array value would have shown a disqualifying
|
|
709
|
+
* use elsewhere.
|
|
710
|
+
* - COMPATIBLE-ONLY: the length slot of `new TypedArray(x)` / `new ArrayBuffer(x)`.
|
|
711
|
+
* A number sizes the buffer, but an array is COPIED and a buffer VIEWED — so a
|
|
712
|
+
* bare param here proves nothing. A param used *solely* as `new Float64Array(arr)`
|
|
713
|
+
* stays unproven and keeps the polymorphic ctor dispatch (else array-copy is lost).
|
|
714
|
+
* Any other appearance (member/call-arg/return/concat/`===`/reassignment) rejects.
|
|
715
|
+
* Two transparencies:
|
|
573
716
|
* - copy aliases: `let x = name` makes `x` carry the same value, so `x`'s uses
|
|
574
717
|
* must be numeric too (fixpoint-collected). Catches `let T = t` then `…T…`.
|
|
575
718
|
* - captured closures: a non-shadowing inner arrow captures the binding by
|
|
576
719
|
* reference, so its body's uses count — we recurse instead of rejecting.
|
|
577
720
|
* Catches floatbeat helpers `let s=(f)=>…t…` that read the param numerically. */
|
|
578
|
-
|
|
721
|
+
// requireProof=true (default): the param has a ToNumber-FORCING use (PROVES numeric).
|
|
722
|
+
// requireProof=false: the param merely has NO string-requiring use (numeric-COMPATIBLE).
|
|
723
|
+
// Forwarding recursions use the latter — a callee receiving the param need only be
|
|
724
|
+
// string-free (e.g. fbm's `ph`, used additively inside Math.sin), since the OUTER
|
|
725
|
+
// param earns its own proof from its own uses; requiring the callee be self-proven
|
|
726
|
+
// wrongly rejected forwards into additive-only params.
|
|
727
|
+
function paramAllUsesNumeric(body, name, _seen = new Set(), requireProof = true) {
|
|
579
728
|
if (body == null) return false
|
|
729
|
+
// Local closure defs (`let f = (p,…) => …`) so a call `f(name)` can be judged by
|
|
730
|
+
// f's own param numericity (see the call-arg handler in the walk).
|
|
731
|
+
const closures = new Map() // name → { params:[string], body }
|
|
580
732
|
// Fixpoint-collect copy aliases: `let/const x = <name-or-alias>`.
|
|
581
733
|
const names = new Set([name])
|
|
582
734
|
for (let grew = true; grew;) {
|
|
@@ -584,15 +736,25 @@ function paramAllUsesNumeric(body, name) {
|
|
|
584
736
|
const collect = (node) => {
|
|
585
737
|
if (!Array.isArray(node)) return
|
|
586
738
|
if ((node[0] === 'let' || node[0] === 'const') && node.length === 2
|
|
587
|
-
&& Array.isArray(node[1]) && node[1][0] === '=' && typeof node[1][1] === 'string'
|
|
588
|
-
|
|
589
|
-
names.add(node[1][1]); grew = true
|
|
739
|
+
&& Array.isArray(node[1]) && node[1][0] === '=' && typeof node[1][1] === 'string') {
|
|
740
|
+
const init = node[1][2]
|
|
741
|
+
if (typeof init === 'string' && names.has(init) && !names.has(node[1][1])) { names.add(node[1][1]); grew = true }
|
|
742
|
+
else if (Array.isArray(init) && init[0] === '=>' && !closures.has(node[1][1])) {
|
|
743
|
+
const ps = Array.isArray(init[1]) ? init[1].slice(1) : [init[1]] // ['()', p0, p1] → [p0,p1]
|
|
744
|
+
if (ps.every(p => typeof p === 'string')) closures.set(node[1][1], { params: ps, body: init[2] })
|
|
745
|
+
}
|
|
590
746
|
}
|
|
591
747
|
for (let i = 1; i < node.length; i++) collect(node[i])
|
|
592
748
|
}
|
|
593
749
|
collect(body)
|
|
594
750
|
}
|
|
595
|
-
let ok = true
|
|
751
|
+
let ok = true, proven = false
|
|
752
|
+
// A param in a numeric-operand slot is a PROVING use; recurse into a non-param sub-expr.
|
|
753
|
+
const numOperand = (n) => { if (names.has(n)) proven = true; else walk(n) }
|
|
754
|
+
// Positional call args, flattening the `(, a b c)` node multi-arg calls parse to —
|
|
755
|
+
// without this a forward like `fbm(x, y, t, …)` never matched its param positions.
|
|
756
|
+
const flat1 = (a) => Array.isArray(a) && a[0] === ',' ? a.slice(1).flatMap(flat1) : [a]
|
|
757
|
+
const callArgList = (n) => n.slice(2).flatMap(flat1)
|
|
596
758
|
const walk = (node) => {
|
|
597
759
|
if (!ok) return
|
|
598
760
|
if (typeof node === 'string') { if (names.has(node)) ok = false; return } // bare use → reject
|
|
@@ -617,19 +779,161 @@ function paramAllUsesNumeric(body, name) {
|
|
|
617
779
|
}
|
|
618
780
|
if (PARAM_REASSIGN_OPS.has(op) && names.has(node[1])) { ok = false; return }
|
|
619
781
|
if (NUM_BIN_OPS.has(op) && node.length === 3) { // numeric binary: operands are ToNumber'd
|
|
782
|
+
numOperand(node[1]); numOperand(node[2])
|
|
783
|
+
return
|
|
784
|
+
}
|
|
785
|
+
if (REL_OPS.has(op) && node.length === 3) { // relational: numeric unless a known string is present
|
|
786
|
+
if (isStrLiteral(node[1]) || isStrLiteral(node[2])) { ok = false; return }
|
|
787
|
+
numOperand(node[1]); numOperand(node[2])
|
|
788
|
+
return
|
|
789
|
+
}
|
|
790
|
+
// `new TypedArray(x)` / `new ArrayBuffer(x)`: the length argument is ToNumber'd
|
|
791
|
+
// on the alloc path, but a pointer arg is copied (array) or viewed (buffer).
|
|
792
|
+
// A bare param in the length slot is numeric-COMPATIBLE but not PROVING — skip it
|
|
793
|
+
// (no reject, no proof); other args walk normally. A param used *solely* as
|
|
794
|
+
// `new Float64Array(param)` thus stays unproven → keeps the polymorphic ctor (so
|
|
795
|
+
// `f(arr)` copies the array instead of mis-sizing a zero buffer).
|
|
796
|
+
if (op === '()' && typeof node[1] === 'string' && node[1].startsWith('new.')
|
|
797
|
+
&& (node[1].endsWith('Array') || node[1] === 'new.ArrayBuffer')) {
|
|
798
|
+
for (let i = 2; i < node.length; i++) if (!names.has(node[i])) walk(node[i])
|
|
799
|
+
return
|
|
800
|
+
}
|
|
801
|
+
// Call of a LOCAL closure `f(…name…)`: forwarding the param flows its value into
|
|
802
|
+
// f's positional param. If that param is itself all-numeric (recursively, with a
|
|
803
|
+
// cycle guard), `name` in that slot is numeric-COMPATIBLE — neither rejected nor
|
|
804
|
+
// proving (so a param used *only* as a forwarded arg stays unproven, like the ctor
|
|
805
|
+
// length slot). Unknown / non-numeric callees fall through and reject (a string
|
|
806
|
+
// could flow in). Covers heapsort's `heapify(n)` and crc32's `crc32(buf)`.
|
|
807
|
+
if (op === '()' && typeof node[1] === 'string' && closures.has(node[1]) && !_seen.has(node[1])) {
|
|
808
|
+
const cl = closures.get(node[1])
|
|
809
|
+
const args = callArgList(node)
|
|
810
|
+
for (let i = 0; i < args.length; i++) {
|
|
811
|
+
if (!names.has(args[i])) { walk(args[i]); continue }
|
|
812
|
+
const param = cl.params[i]
|
|
813
|
+
if (param == null || !paramAllUsesNumeric(cl.body, param, new Set([..._seen, node[1]]), false)) { ok = false; return }
|
|
814
|
+
}
|
|
815
|
+
return
|
|
816
|
+
}
|
|
817
|
+
// Same forwarding judgement for a call to a MODULE-LEVEL user function (sibling,
|
|
818
|
+
// not a body-local closure): `frame` passing its param into a helper `fbm(x,y,t,…)`.
|
|
819
|
+
// Without this the bare arg fell through and rejected, leaving an exported numeric
|
|
820
|
+
// param (plasma/raymarcher's `t`) unproven → per-pixel `__to_num` + polymorphic-`+`
|
|
821
|
+
// string forks. Judge by the callee param's own numericity (recursive, cycle-guarded).
|
|
822
|
+
if (op === '()' && typeof node[1] === 'string' && !_seen.has(node[1])) {
|
|
823
|
+
const fn = ctx.func.map?.get(node[1])
|
|
824
|
+
if (fn && fn.body && !fn.raw && Array.isArray(fn.sig?.params) && !fn.rest) {
|
|
825
|
+
const args = callArgList(node)
|
|
826
|
+
for (let i = 0; i < args.length; i++) {
|
|
827
|
+
if (!names.has(args[i])) { walk(args[i]); continue }
|
|
828
|
+
const p = fn.sig.params[i]
|
|
829
|
+
if (!p || !paramAllUsesNumeric(fn.body, p.name, new Set([..._seen, node[1]]), false)) { ok = false; return }
|
|
830
|
+
}
|
|
831
|
+
return
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
// `Math.f(...)` ToNumbers every argument (Math operates on numbers), so a bare
|
|
835
|
+
// param in any arg slot is a PROVING numeric use — same contract as `*`/`-`.
|
|
836
|
+
// Without this, `Math.sin(t)` rejected the param via the generic-call fallthrough,
|
|
837
|
+
// so a numeric kernel like `Math.sin(tick) + …` lost its NUMBER proof and paid a
|
|
838
|
+
// per-use `__to_num` + a polymorphic-`+` string-concat fork (interference example).
|
|
839
|
+
// The callee is the lowered `math.sin` string at emit time (post-autoload), or the
|
|
840
|
+
// raw `(. Math sin)` member pre-lowering — match both.
|
|
841
|
+
const isMathCall = op === '()' && (
|
|
842
|
+
(typeof node[1] === 'string' && node[1].startsWith('math.')) ||
|
|
843
|
+
(Array.isArray(node[1]) && node[1][0] === '.' && node[1][1] === 'Math'))
|
|
844
|
+
if (isMathCall) {
|
|
845
|
+
const numArg = (a) => { if (Array.isArray(a) && a[0] === ',') { numArg(a[1]); numArg(a[2]) } else numOperand(a) }
|
|
846
|
+
for (let i = 2; i < node.length; i++) numArg(node[i])
|
|
847
|
+
return
|
|
848
|
+
}
|
|
849
|
+
// Binary `+` is overloaded (numeric add | string concat). A string-literal
|
|
850
|
+
// operand means concat intent → reject. Otherwise it is numeric-COMPATIBLE but
|
|
851
|
+
// not self-PROVING (a string param would concat) — recurse the non-param operand
|
|
852
|
+
// and treat a bare param as compatible (neither prove nor reject), exactly like
|
|
853
|
+
// paramNeverString. The numeric proof must still come from a ToNumber-forcing use
|
|
854
|
+
// (`*`, `Math.*`, …); a param used ONLY in `+` stays unproven (sound).
|
|
855
|
+
if (op === '+' && node.length === 3) {
|
|
856
|
+
if (isStrLiteral(node[1]) || isStrLiteral(node[2])) { ok = false; return }
|
|
620
857
|
if (!names.has(node[1])) walk(node[1])
|
|
621
858
|
if (!names.has(node[2])) walk(node[2])
|
|
622
859
|
return
|
|
623
860
|
}
|
|
624
|
-
if (op === '-' && node.length === 2) {
|
|
625
|
-
if (op === '-' && node.length === 3) {
|
|
861
|
+
if (op === '-' && node.length === 2) { numOperand(node[1]); return } // unary negate
|
|
862
|
+
if (op === '-' && node.length === 3) { numOperand(node[1]); numOperand(node[2]); return }
|
|
626
863
|
// `u-`/`u+` are the normalized unary minus/plus (prepare rewrites `-x`/`+x`); both ToNumber.
|
|
627
|
-
if ((op === 'u-' || op === 'u+') && node.length === 2) {
|
|
628
|
-
if (op === '+' && node.length === 2) {
|
|
629
|
-
if (op === '~' && node.length === 2) {
|
|
864
|
+
if ((op === 'u-' || op === 'u+') && node.length === 2) { numOperand(node[1]); return }
|
|
865
|
+
if (op === '+' && node.length === 2) { numOperand(node[1]); return } // unary + = ToNumber
|
|
866
|
+
if (op === '~' && node.length === 2) { numOperand(node[1]); return }
|
|
630
867
|
for (let i = 1; i < node.length; i++) walk(node[i]) // bare param reaching here → rejected above
|
|
631
868
|
}
|
|
632
869
|
walk(body)
|
|
870
|
+
return requireProof ? (ok && proven) : ok
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// String methods whose receiver MUST be a string — their presence proves the
|
|
874
|
+
// param is (sometimes) string and disqualifies the boundary-numeric trust.
|
|
875
|
+
const STRING_RECV_METHODS = new Set([
|
|
876
|
+
'charCodeAt', 'charAt', 'codePointAt', 'startsWith', 'endsWith', 'toUpperCase',
|
|
877
|
+
'toLowerCase', 'normalize', 'localeCompare', 'padStart', 'padEnd', 'repeat',
|
|
878
|
+
'trim', 'trimStart', 'trimEnd', 'split', 'match', 'matchAll', 'replace',
|
|
879
|
+
'replaceAll', 'substring', 'substr', 'concat', 'indexOf', 'lastIndexOf',
|
|
880
|
+
'includes', 'slice',
|
|
881
|
+
])
|
|
882
|
+
|
|
883
|
+
/** True iff no use of exported f64 param `name` REQUIRES it to be a string — so
|
|
884
|
+
* the interop boundary contract (`wrapVal` passes a JS number straight to an f64
|
|
885
|
+
* param; a string arg is a type misuse already unsupported, returning NaN) makes
|
|
886
|
+
* it provably numeric. Weaker than `paramAllUsesNumeric`: that PROVES numericity
|
|
887
|
+
* from ToNumber-forcing ops, this DISPROVES stringness so binary `+` (the common
|
|
888
|
+
* `accumulator + cre` shape) no longer pessimistically pulls the string-concat
|
|
889
|
+
* fork into a pure float kernel. Only sound under the export boundary — never use
|
|
890
|
+
* for locals/closures, whose values can genuinely be strings.
|
|
891
|
+
*
|
|
892
|
+
* Disqualifying (string-requiring) uses:
|
|
893
|
+
* - `+` with a string-literal/template operand (`"px" + name`) — concat intent
|
|
894
|
+
* - a string-receiver method call (`name.charCodeAt(…)`, `name.split(…)`)
|
|
895
|
+
* - `name[k]` / `name.length` is NOT disqualifying (works on arrays/typed too,
|
|
896
|
+
* but an f64 param is neither — so a member access means the caller passed a
|
|
897
|
+
* pointer, out of the f64-number contract; conservatively we reject it)
|
|
898
|
+
* - passing `name` to a call / returning it / storing into an aggregate: the
|
|
899
|
+
* value escapes where it could be ToString'd; reject conservatively. */
|
|
900
|
+
function paramNeverString(body, name) {
|
|
901
|
+
if (body == null) return false
|
|
902
|
+
let ok = true
|
|
903
|
+
const walk = (node) => {
|
|
904
|
+
if (!ok || node == null) return
|
|
905
|
+
if (typeof node === 'string') { if (node === name) ok = false; return } // bare escape → reject
|
|
906
|
+
if (!Array.isArray(node)) return
|
|
907
|
+
const op = node[0]
|
|
908
|
+
if (op === '=>') return // shadowing-safe: closure handled conservatively (escape)
|
|
909
|
+
// `+` (binary): a string-literal/template operand makes it concat → reject.
|
|
910
|
+
// Otherwise the param is in an arithmetic add; recurse the non-name operand.
|
|
911
|
+
if (op === '+' && node.length === 3) {
|
|
912
|
+
if (isStrLiteral(node[1]) || isStrLiteral(node[2])) { ok = false; return }
|
|
913
|
+
for (let i = 1; i <= 2; i++) if (node[i] !== name) walk(node[i])
|
|
914
|
+
return
|
|
915
|
+
}
|
|
916
|
+
// Numeric/relational/bitwise binary + unary: param operand is fine, recurse rest.
|
|
917
|
+
if ((NUM_BIN_OPS.has(op) || REL_OPS.has(op)) && node.length === 3) {
|
|
918
|
+
for (let i = 1; i <= 2; i++) if (node[i] !== name) walk(node[i])
|
|
919
|
+
return
|
|
920
|
+
}
|
|
921
|
+
if ((op === 'u-' || op === 'u+' || op === '~') && node.length === 2) {
|
|
922
|
+
if (node[1] !== name) walk(node[1]); return
|
|
923
|
+
}
|
|
924
|
+
if (op === '-' && (node.length === 2 || node.length === 3)) {
|
|
925
|
+
for (let i = 1; i < node.length; i++) if (node[i] !== name) walk(node[i])
|
|
926
|
+
return
|
|
927
|
+
}
|
|
928
|
+
// Member access / method call on the param → it's a pointer, not an f64 number:
|
|
929
|
+
// reject (out of contract). `.`/`?.`/`[]` with the name as receiver.
|
|
930
|
+
if ((op === '.' || op === '?.' || op === '[]') && node[1] === name) { ok = false; return }
|
|
931
|
+
// `=`/compound reassignment of the param to a non-numeric value: reject if it
|
|
932
|
+
// could become a string. A reassignment makes the param mutable — conservatively
|
|
933
|
+
// require the RHS to be string-free too (recurse), and the target isn't a use.
|
|
934
|
+
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
935
|
+
}
|
|
936
|
+
walk(body)
|
|
633
937
|
return ok
|
|
634
938
|
}
|
|
635
939
|
|
|
@@ -728,6 +1032,11 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
728
1032
|
// it the pass is a no-op. `$`-prefixed to match WAT local names directly.
|
|
729
1033
|
if (funcFacts.cseLoadBases?.size)
|
|
730
1034
|
fn.cseLoadBases = new Set([...funcFacts.cseLoadBases].map(n => `$${n}`))
|
|
1035
|
+
// Param-distinctness fact (alias analysis): typed-array params proven mutually-distinct buffers
|
|
1036
|
+
// at every call site. `$`-prefixed to match WAT param names; read by hoistInvariantLoop to hoist
|
|
1037
|
+
// a load from one such param across a store to another (they can't alias).
|
|
1038
|
+
if (funcFacts.distinctParams?.size)
|
|
1039
|
+
fn.distinctParams = new Set([...funcFacts.distinctParams].map(n => `$${n}`))
|
|
731
1040
|
// Inline `(export ...)` attribute only for the syntactic inline-export
|
|
732
1041
|
// form (`export function foo`, snapshot in `func.exported` at defFunc
|
|
733
1042
|
// time). Re-exports (`function foo; export { foo }`) and aliases (`export
|
|
@@ -868,17 +1177,26 @@ function synthesizeBoundaryWrappers() {
|
|
|
868
1177
|
for (const func of ctx.func.list) {
|
|
869
1178
|
if (!isBoundaryWrapped(func)) continue
|
|
870
1179
|
const { name, sig } = func
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
// JS↔wasm
|
|
875
|
-
//
|
|
876
|
-
//
|
|
877
|
-
//
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
const
|
|
881
|
-
|
|
1180
|
+
// i64 boundary carrier (Safari-safe). A genuine number is never a NaN-box, so it crosses
|
|
1181
|
+
// as plain f64 (zero cost). Everything that can be a NaN-box — heap pointer, null/undef/
|
|
1182
|
+
// bool atom, bigint carrier, or a dynamic value — crosses as i64: JSC (Safari) canonicalizes
|
|
1183
|
+
// f64 NaN payloads at the JS↔wasm boundary, erasing the box. The wasm signature is
|
|
1184
|
+
// self-describing; interop.js wrap() reinterprets BigInt↔f64 by bits, driven by the
|
|
1185
|
+
// `jz:i64exp` section emitted below. Non-JS hosts (WASI) read the same signature — i64 is
|
|
1186
|
+
// just int64 there, no BigInt.
|
|
1187
|
+
const resultPtr = sig.ptrKind != null
|
|
1188
|
+
const resultBool = func.valResult === VAL.BOOL && !resultPtr
|
|
1189
|
+
const resultBigint = func.valResult === VAL.BIGINT && !resultPtr
|
|
1190
|
+
// Dynamic f64 result: not pointer/bool/bigint and not a proven number → may be a NaN-box
|
|
1191
|
+
// at runtime, so i64. (An i32-carrier result is numeric → stays f64 via convert below.)
|
|
1192
|
+
const resultDynamic = !resultPtr && !resultBool && !resultBigint
|
|
1193
|
+
&& sig.results[0] === 'f64' && !func._resultNumeric
|
|
1194
|
+
const resultI64 = resultPtr || resultBool || resultBigint || resultDynamic
|
|
1195
|
+
// jz:i64exp `r` marks results interop must reinterpret then `mem.read`. A bigint result is
|
|
1196
|
+
// i64 too, but the BigInt *is* the value (no reinterpret) — so it stays unmarked.
|
|
1197
|
+
const resultReinterpret = resultPtr || resultBool || resultDynamic
|
|
1198
|
+
// i64 carrier per param: pointer-ABI (offset) or a dynamic f64 param (boundaryI64).
|
|
1199
|
+
const paramIsI64 = (p) => !p.jsstring && (p.ptrKind != null || p.boundaryI64)
|
|
882
1200
|
// Inline `(export ...)` attribute only when the func decl carried the
|
|
883
1201
|
// inline-export keyword (`export function foo`). For re-exports
|
|
884
1202
|
// (`function foo; export { foo as bar }`) the `name` is the *internal*
|
|
@@ -888,26 +1206,58 @@ function synthesizeBoundaryWrappers() {
|
|
|
888
1206
|
const wrapNode = func.exported
|
|
889
1207
|
? ['func', `$${name}$exp`, ['export', `"${name}"`]]
|
|
890
1208
|
: ['func', `$${name}$exp`]
|
|
891
|
-
// jsstring params flow as externref end-to-end;
|
|
892
|
-
|
|
893
|
-
|
|
1209
|
+
// jsstring params flow as externref end-to-end; boxed params ride i64; numbers f64.
|
|
1210
|
+
const i64Params = []
|
|
1211
|
+
sig.params.forEach((p, i) => {
|
|
1212
|
+
wrapNode.push(['param', `$${p.name}`, p.jsstring ? 'externref' : paramIsI64(p) ? 'i64' : 'f64'])
|
|
1213
|
+
if (paramIsI64(p)) i64Params.push(i)
|
|
894
1214
|
})
|
|
895
|
-
|
|
1215
|
+
// Track externref param positions so interop.js can pass JS values raw (skipping
|
|
1216
|
+
// `mem.wrapVal`) at those slots — today only `jsstring` params; future externref carriers
|
|
1217
|
+
// wire here too. `extParams` is per-slot: false | { def: '...' } for a JS-side default.
|
|
1218
|
+
const extParams = sig.params.map(p => !p.jsstring ? false : p.jsstringDefault != null ? { def: p.jsstringDefault } : true)
|
|
1219
|
+
if (extParams.some(Boolean)) func._exportExtParams = extParams
|
|
1220
|
+
// Inner→wrapper argument list, shared by both single- and multi-value result shapes.
|
|
896
1221
|
const args = sig.params.map((p) => {
|
|
897
1222
|
const get = ['local.get', `$${p.name}`]
|
|
898
|
-
|
|
899
|
-
if (p.
|
|
900
|
-
//
|
|
901
|
-
if (p.ptrKind != null) return ['i32.wrap_i64', ['i64.reinterpret_f64', get]]
|
|
1223
|
+
if (p.jsstring) return get // externref flows through unchanged
|
|
1224
|
+
if (p.ptrKind != null) return ['i32.wrap_i64', get] // ptr param: inner takes the i32 offset
|
|
1225
|
+
if (p.boundaryI64) return ['f64.reinterpret_i64', get] // dynamic boxed param → f64 NaN-box carrier
|
|
902
1226
|
if (p.type === 'f64') return get
|
|
903
|
-
//
|
|
904
|
-
return ['i32.trunc_sat_f64_s', get]
|
|
1227
|
+
return ['i32.trunc_sat_f64_s', get] // numeric narrowing f64 → i32
|
|
905
1228
|
})
|
|
906
1229
|
const callIR = ['call', `$${name}`, ...args]
|
|
1230
|
+
// Multi-value return: each lane is an f64 NaN-box carrier (every `return [a,b,…]` lane is
|
|
1231
|
+
// asF64; narrowing only touches single-result funcs). A boxed lane's NaN payload is erased
|
|
1232
|
+
// at the JS boundary, so cross EVERY lane as i64 — capture the inner call's N lanes into f64
|
|
1233
|
+
// locals (last result on top of the stack ⇒ pop in reverse) and re-push each reinterpreted.
|
|
1234
|
+
// interop reads the lane tuple via mem.read / decode (both map over an array result).
|
|
1235
|
+
if (sig.results.length > 1) {
|
|
1236
|
+
sig.results.forEach(() => wrapNode.push(['result', 'i64']))
|
|
1237
|
+
// Lane temporaries — guaranteed distinct from the wrapper's params (jz doesn't reserve
|
|
1238
|
+
// `__`, so a user param could be `__mlane0`): bump the prefix until no lane name collides.
|
|
1239
|
+
const pnames = new Set(sig.params.map((p) => p.name))
|
|
1240
|
+
let pfx = '__mlane'
|
|
1241
|
+
while (sig.results.some((_, i) => pnames.has(`${pfx}${i}`))) pfx = `_${pfx}`
|
|
1242
|
+
const lanes = sig.results.map((_, i) => `$${pfx}${i}`)
|
|
1243
|
+
lanes.forEach((n) => wrapNode.push(['local', n, 'f64']))
|
|
1244
|
+
const stmts = [callIR]
|
|
1245
|
+
for (let i = lanes.length - 1; i >= 0; i--) stmts.push(['local.set', lanes[i]])
|
|
1246
|
+
for (const n of lanes) stmts.push(['i64.reinterpret_f64', ['local.get', n]])
|
|
1247
|
+
wrapNode.push(...stmts)
|
|
1248
|
+
// `m` (lane count) marks a multi-value result so interop / the test adapter decode each
|
|
1249
|
+
// lane (vs `r`'s single reinterpret). Always recorded — even with no i64 params — so the
|
|
1250
|
+
// numeric-only `(a,b)=>[a+1,b+2]` tuple still gets its lanes turned back into numbers.
|
|
1251
|
+
func._exportI64 = { p: i64Params, m: sig.results.length }
|
|
1252
|
+
wrappers.push(wrapNode)
|
|
1253
|
+
continue
|
|
1254
|
+
}
|
|
1255
|
+
wrapNode.push(['result', resultI64 ? 'i64' : 'f64'])
|
|
1256
|
+
const toI64 = (n) => ['i64.reinterpret_f64', n]
|
|
907
1257
|
let body
|
|
908
|
-
if (
|
|
1258
|
+
if (resultPtr) {
|
|
909
1259
|
const ptrType = valKindToPtr(sig.ptrKind)
|
|
910
|
-
body = mkPtrIR(ptrType, sig.ptrAux ?? 0, callIR)
|
|
1260
|
+
body = toI64(mkPtrIR(ptrType, sig.ptrAux ?? 0, callIR))
|
|
911
1261
|
} else if (resultBool) {
|
|
912
1262
|
// The inner func returns a clean 0/1 boolean carrier — never NaN. The i32
|
|
913
1263
|
// carrier already takes truthyIR's identity path; the f64 carrier would
|
|
@@ -917,28 +1267,22 @@ function synthesizeBoundaryWrappers() {
|
|
|
917
1267
|
const carrier = sig.results[0] === 'i32'
|
|
918
1268
|
? typed(callIR, 'i32')
|
|
919
1269
|
: typed(['f64.ne', callIR, ['f64.const', 0]], 'i32')
|
|
920
|
-
body = boolBoxIR(carrier)
|
|
921
|
-
} else if (resultBigint) {
|
|
922
|
-
// BigInt rides the i64-reinterpret-f64 carrier internally;
|
|
923
|
-
//
|
|
924
|
-
// callers use `$name` (the f64 carrier) untouched; only
|
|
925
|
-
body =
|
|
1270
|
+
body = toI64(boolBoxIR(carrier))
|
|
1271
|
+
} else if (resultBigint || resultDynamic) {
|
|
1272
|
+
// BigInt rides the i64-reinterpret-f64 carrier internally; a dynamic result is already an
|
|
1273
|
+
// f64 NaN-box carrier. Either way expose the raw i64 at the JS boundary for a lossless
|
|
1274
|
+
// value. Internal callers use `$name` (the f64 carrier) untouched; only `$exp` is i64.
|
|
1275
|
+
body = toI64(callIR)
|
|
926
1276
|
} else if (sig.results[0] === 'i32') {
|
|
927
1277
|
body = [sig.unsignedResult ? 'f64.convert_i32_u' : 'f64.convert_i32_s', callIR]
|
|
928
1278
|
} else {
|
|
929
1279
|
body = callIR
|
|
930
1280
|
}
|
|
931
1281
|
wrapNode.push(body)
|
|
932
|
-
//
|
|
933
|
-
//
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
// for jsstring params with a JS-side default substitution.
|
|
937
|
-
const extParams = sig.params.map(p => {
|
|
938
|
-
if (!p.jsstring) return false
|
|
939
|
-
return p.jsstringDefault != null ? { def: p.jsstringDefault } : true
|
|
940
|
-
})
|
|
941
|
-
if (extParams.some(Boolean)) func._exportExtParams = extParams
|
|
1282
|
+
// Record the i64 carrier map for interop.js (jz:i64exp). A pure-numeric export
|
|
1283
|
+
// (no i64 params, f64 result) records nothing — zero footprint off the box path.
|
|
1284
|
+
if (i64Params.length || resultReinterpret)
|
|
1285
|
+
func._exportI64 = { p: i64Params, r: resultReinterpret ? 1 : 0 }
|
|
942
1286
|
wrappers.push(wrapNode)
|
|
943
1287
|
}
|
|
944
1288
|
return wrappers
|
|
@@ -1025,6 +1369,32 @@ function emitClosureBody(cb) {
|
|
|
1025
1369
|
if (ptRow[i] === true && !ctx.func.localReps?.get(cb.params[i])?.val)
|
|
1026
1370
|
updateRep(cb.params[i], i < minArgc ? { val: VAL.NUMBER } : { val: VAL.NUMBER, nullable: true })
|
|
1027
1371
|
}
|
|
1372
|
+
// A param passed the same typed-array ctor at every direct call site is TYPED:
|
|
1373
|
+
// register its element ctor so `buf[i]` reads use the typed load (it stays an f64
|
|
1374
|
+
// NaN-box in the closure ABI, but knowing the kind avoids the dynamic `__typed_idx`
|
|
1375
|
+
// /`__len` dispatch that pulls the string runtime). Numeric trust (above) wins if it
|
|
1376
|
+
// already classified the slot — they're disjoint anyway (NUMBER vs TYPED arg).
|
|
1377
|
+
const tcRow = ctx.closure.paramTypedCtors?.get(cb.name)
|
|
1378
|
+
if (tcRow) for (let i = 0; i < cb.params.length; i++) {
|
|
1379
|
+
const ctor = tcRow[i]
|
|
1380
|
+
if (ctor && !ctx.func.localReps?.get(cb.params[i])?.val) {
|
|
1381
|
+
updateRep(cb.params[i], { val: VAL.TYPED })
|
|
1382
|
+
;(ctx.types.typedElem ||= new Map()).set(cb.params[i], ctor)
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
// Body-usage numeric trust for closure params — the same proof the export path
|
|
1386
|
+
// applies (paramAllUsesNumeric). A nested helper like heapsort's `heapify(n)` whose
|
|
1387
|
+
// param is used only in arithmetic/relational positions is VAL.NUMBER, so `(n>>1)-1`
|
|
1388
|
+
// skips the `__to_num` coercion that would otherwise drag the ToNumber string-parse
|
|
1389
|
+
// tree into a pure-integer kernel. paramAllUsesNumeric walks any AST node, so this
|
|
1390
|
+
// also covers expression-bodied arrows (`(m) => m | 0`) — the common closure shape
|
|
1391
|
+
// whose dynamic param would otherwise emit a polymorphic add/coerce that pulls the
|
|
1392
|
+
// whole string runtime in. Call-site evidence (ptRow) already covers the monomorphic
|
|
1393
|
+
// case; this also catches params the lattice left unobserved.
|
|
1394
|
+
for (const p of cb.params) {
|
|
1395
|
+
if (!ctx.func.localReps?.get(p)?.val && !cb.defaults?.[p] && paramAllUsesNumeric(cb.body, p))
|
|
1396
|
+
updateRep(p, { val: VAL.NUMBER })
|
|
1397
|
+
}
|
|
1028
1398
|
|
|
1029
1399
|
// Register captured variable locals: boxed = i32 cell pointer, otherwise f64 value
|
|
1030
1400
|
for (let i = 0; i < cb.captures.length; i++) {
|
|
@@ -1308,6 +1678,11 @@ export default function compile(ast, profiler) {
|
|
|
1308
1678
|
}
|
|
1309
1679
|
}
|
|
1310
1680
|
|
|
1681
|
+
// Whole-program constant fold of module-scope aggregate literals — `var x=[1,2,3];
|
|
1682
|
+
// y=x[0]` → `y=1`, dropping the array (no data segment, no __arr_idx_known) when
|
|
1683
|
+
// every reference is a static read. The scalar analog of the constInts fold above.
|
|
1684
|
+
timePhase(profiler, 'foldAggregates', () => foldStaticConstAggregates(ast))
|
|
1685
|
+
|
|
1311
1686
|
const programFacts = timePhase(profiler, 'plan', () => plan(ast, profiler))
|
|
1312
1687
|
|
|
1313
1688
|
// Inspect sink: editor hosts opt in via { inspect: true } to read inferred shapes.
|
|
@@ -1448,13 +1823,35 @@ export default function compile(ast, profiler) {
|
|
|
1448
1823
|
ensureThrowRuntime(sec)
|
|
1449
1824
|
|
|
1450
1825
|
timePhase(profiler, 'optimizeModule', () => optimizeModule(sec, profiler))
|
|
1826
|
+
if (ctx.transform.helperCallsites) instrumentHelperCallsites([...sec.funcs, ...sec.stdlib, ...sec.start])
|
|
1827
|
+
|
|
1828
|
+
// Fold constant `__start` global inits into immutable inline decls (drops the
|
|
1829
|
+
// store, and `__start` with it when that empties it). Runs HERE — after
|
|
1830
|
+
// stripStaticDataPrefix and optimizeModule — so any data-segment offset a hoisted
|
|
1831
|
+
// pointer carries is already in its final, shifted form (hoisting earlier would
|
|
1832
|
+
// freeze a pre-strip offset the shift pass never revisits in the global decl).
|
|
1833
|
+
hoistConstGlobalInits(sec)
|
|
1451
1834
|
|
|
1452
1835
|
// Populate globals (after __start — const folding may update declarations).
|
|
1453
1836
|
// Records build IR directly — no WAT-text parse-back.
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1837
|
+
// The wasm type comes from globalTypes (the canonical name→type map declGlobal
|
|
1838
|
+
// maintains alongside the entry), falling back to the entry's own `.type`. They
|
|
1839
|
+
// are normally identical, but a global whose entry object is later rebuilt (e.g.
|
|
1840
|
+
// hoistConstGlobalInits' `{...g, …}` spread) must not depend on that rebuild
|
|
1841
|
+
// preserving `.type` — globalTypes is the stable source, so an entry that lost
|
|
1842
|
+
// its `.type` still emits a well-typed `(global …)` rather than `(undefined.const)`.
|
|
1843
|
+
sec.globals.push(...[...ctx.scope.globals].filter(([, g]) => g).map(([n, g]) => {
|
|
1844
|
+
const ty = ctx.scope.globalTypes.get(n) ?? g.type
|
|
1845
|
+
return ['global', `$${n}`,
|
|
1846
|
+
...(g.export ? [['export', `"${g.export}"`]] : []),
|
|
1847
|
+
g.mut ? ['mut', ty] : ty,
|
|
1848
|
+
[`${ty}.const`, g.init]]
|
|
1849
|
+
}))
|
|
1850
|
+
|
|
1851
|
+
// Drop the Eisel-Lemire decimal table if no live code parses decimals at runtime — must
|
|
1852
|
+
// run after sec.globals/funcs are final (exact reachability) and before the data segment
|
|
1853
|
+
// below serializes ctx.runtime.data. See stripDeadElTable.
|
|
1854
|
+
stripDeadElTable(sec)
|
|
1458
1855
|
|
|
1459
1856
|
// Data segments (after emit — string literals append to ctx.runtime.data / strPool during emit)
|
|
1460
1857
|
// Active segment at address 0 — skipped for shared memory (would collide across modules)
|
|
@@ -1540,6 +1937,23 @@ export default function compile(ast, profiler) {
|
|
|
1540
1937
|
if (extExports.length)
|
|
1541
1938
|
sec.customs.push(['@custom', '"jz:extparam"', `"${JSON.stringify(extExports).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
1542
1939
|
|
|
1940
|
+
// jz:i64exp — per-export i64 carrier map (NaN-canonicalization dodging). Each entry
|
|
1941
|
+
// `{name, p:[i64 param indices], r:1? | m:N?}`: `p` lists params interop must pass as BigInt
|
|
1942
|
+
// (f64ToI64); `r` marks a single result to reinterpret (i64ToF64) before mem.read; `m` marks
|
|
1943
|
+
// an N-lane multi-value result whose lanes interop/the adapter decode element-wise. Pure-
|
|
1944
|
+
// numeric single-result exports emit no entry. A bigint result is i64 but unmarked (the BigInt
|
|
1945
|
+
// is the value). Written under every JS-visible alias, like jz:extparam. Each shape is built as
|
|
1946
|
+
// a direct literal (no spread) — the self-host kernel's fixed schemas don't enumerate post-hoc keys.
|
|
1947
|
+
const i64Exports = []
|
|
1948
|
+
for (const f of ctx.func.list) {
|
|
1949
|
+
if (!isExported(f) || !isBoundaryWrapped(f) || !f._exportI64) continue
|
|
1950
|
+
const { p, r, m } = f._exportI64
|
|
1951
|
+
for (const exportName of exportNamesOf(f.name))
|
|
1952
|
+
i64Exports.push(m ? { name: exportName, p, m } : r ? { name: exportName, p, r } : { name: exportName, p })
|
|
1953
|
+
}
|
|
1954
|
+
if (i64Exports.length)
|
|
1955
|
+
sec.customs.push(['@custom', '"jz:i64exp"', `"${JSON.stringify(i64Exports).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
1956
|
+
|
|
1543
1957
|
// Named export aliases: export { name } or export { source as alias }
|
|
1544
1958
|
for (const [name, val] of Object.entries(ctx.func.exports)) {
|
|
1545
1959
|
if (wasiCommandExports.has(name)) continue
|
|
@@ -1564,7 +1978,8 @@ export default function compile(ast, profiler) {
|
|
|
1564
1978
|
const { callCount } = treeshake(
|
|
1565
1979
|
[{ arr: sec.stdlib }, { arr: sec.funcs }, { arr: sec.start }],
|
|
1566
1980
|
[...sec.start, ...sec.elem, ...sec.customs, ...sec.extStdlib, ...sec.imports, ...sec.tags],
|
|
1567
|
-
{ removeDead: !optCfg || optCfg.treeshake !== false, globals: sec.globals
|
|
1981
|
+
{ removeDead: !optCfg || optCfg.treeshake !== false, globals: sec.globals, userGlobals: ctx.scope.userGlobals,
|
|
1982
|
+
userFuncs: new Set(ctx.func.list.map(f => `$${f.name}`)) }
|
|
1568
1983
|
)
|
|
1569
1984
|
|
|
1570
1985
|
pruneUnusedThrowRuntime(sec)
|