jz 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -43
- package/bench/README.md +128 -78
- package/bench/bench.svg +32 -42
- package/cli.js +73 -7
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +190 -34
- package/interop.js +71 -27
- package/layout.js +5 -0
- package/module/array.js +71 -39
- package/module/collection.js +41 -5
- package/module/core.js +2 -4
- package/module/math.js +138 -2
- package/module/number.js +21 -0
- package/module/object.js +26 -0
- package/module/simd.js +37 -5
- package/module/string.js +41 -12
- package/module/typedarray.js +415 -26
- package/package.json +21 -6
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +38 -3
- package/src/compile/emit-assign.js +9 -6
- package/src/compile/emit.js +347 -36
- package/src/compile/index.js +307 -29
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +155 -0
- package/src/compile/narrow.js +108 -11
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/index.js +4 -0
- package/src/compile/plan/inline.js +5 -2
- package/src/compile/plan/literals.js +220 -5
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +45 -7
- package/src/ir.js +55 -7
- package/src/kind-traits.js +27 -0
- package/src/kind.js +65 -3
- package/src/optimize/index.js +344 -45
- package/src/optimize/vectorize.js +2968 -182
- package/src/prepare/index.js +64 -7
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +3 -2
- package/src/static.js +9 -0
- package/src/type.js +10 -6
- package/src/wat/assemble.js +195 -13
- package/src/wat/optimize.js +363 -185
package/src/compile/index.js
CHANGED
|
@@ -37,6 +37,8 @@ import { typedElemAux } from '../../layout.js'
|
|
|
37
37
|
import { VAL, updateRep, REP_FIELDS } from '../reps.js'
|
|
38
38
|
import { inferLocals } from './infer.js'
|
|
39
39
|
import { optimizeFunc, treeshake } from '../optimize/index.js'
|
|
40
|
+
import { strengthReduceLoopDivMod } from './loop-divmod.js'
|
|
41
|
+
import { peelClampedStencil } from './peel-stencil.js'
|
|
40
42
|
import { emit, emitter, emitVoid, emitBlockBody } from './emit.js'
|
|
41
43
|
import { emitCharDecompPrologue, JSS_IMPORT_SIGS } from '../abi/string.js'
|
|
42
44
|
import {
|
|
@@ -54,9 +56,10 @@ import {
|
|
|
54
56
|
I32_MIN, I32_MAX, dollar,
|
|
55
57
|
} from '../ir.js'
|
|
56
58
|
import plan from './plan/index.js'
|
|
59
|
+
import { foldStaticConstAggregates } from './plan/literals.js'
|
|
57
60
|
import {
|
|
58
61
|
buildStartFn, dedupClosureBodies, finalizeClosureTable,
|
|
59
|
-
pullStdlib, syncImports, optimizeModule, stripStaticDataPrefix,
|
|
62
|
+
pullStdlib, syncImports, optimizeModule, stripStaticDataPrefix, hoistConstGlobalInits,
|
|
60
63
|
} from '../wat/assemble.js'
|
|
61
64
|
|
|
62
65
|
// =============================================================================
|
|
@@ -346,6 +349,7 @@ function scanAndTagNonEscapingClosures(body) {
|
|
|
346
349
|
// their synthetic labels can't collide with the parent frame's.
|
|
347
350
|
function enterFunc(sig, body, { uniq = 0, directClosures = null } = {}) {
|
|
348
351
|
ctx.func.stack = []
|
|
352
|
+
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
353
|
ctx.func.maybeNullish = new Set() // bindings assigned a nullish literal → coerce in arithmetic (null-flow)
|
|
350
354
|
ctx.func.pendingLabel = null // label awaiting its loop, for `continue <label>`
|
|
351
355
|
ctx.func.uniq = uniq
|
|
@@ -379,6 +383,15 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
379
383
|
const { paramReps } = programFacts
|
|
380
384
|
if (func.raw) return null
|
|
381
385
|
|
|
386
|
+
// Strength-reduce per-iteration `i % w` / `(i/w)|0` to incremental i32 counters
|
|
387
|
+
// (idempotent: a reduced loop has no modulo left to match). Before analyze so the
|
|
388
|
+
// counters are typed/narrowed like any i32 local. Off at L0 / `loopIVDivMod:false`.
|
|
389
|
+
const _o = ctx.transform.optimize
|
|
390
|
+
if (_o && _o.loopIVDivMod !== false && isBlockBody(func.body)) func.body = strengthReduceLoopDivMod(func.body)
|
|
391
|
+
// Edge-clamp peeling: split a clamped stencil loop into clamp-free interior + edges
|
|
392
|
+
// (the interior then lifts to SIMD). Before analyze so the new loops are analyzed.
|
|
393
|
+
if (_o && _o.clampPeel !== false && isBlockBody(func.body)) func.body = peelClampedStencil(func.body)
|
|
394
|
+
|
|
382
395
|
const { name, body, sig } = func
|
|
383
396
|
enterFunc(sig, body)
|
|
384
397
|
|
|
@@ -417,7 +430,11 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
417
430
|
if (p.type === 'f64' && p.ptrKind == null && !p.jsstring
|
|
418
431
|
&& !func.defaults?.[p.name] && !ctx.func.boxed?.has(p.name)
|
|
419
432
|
&& !ctx.func.localReps?.get(p.name)?.val
|
|
420
|
-
|
|
433
|
+
// Numeric either by PROOF (ToNumber-forcing uses) or by the export
|
|
434
|
+
// boundary contract (never used as a string → wrapVal guarantees a
|
|
435
|
+
// number). The latter catches `acc + cre` float kernels whose `+` would
|
|
436
|
+
// otherwise pull a per-iteration string-concat fork (julia, floatbeats).
|
|
437
|
+
&& (paramAllUsesNumeric(body, p.name) || paramNeverString(body, p.name)))
|
|
421
438
|
updateRep(p.name, { val: VAL.NUMBER })
|
|
422
439
|
}
|
|
423
440
|
}
|
|
@@ -434,6 +451,8 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
434
451
|
if (bodyFacts?.valTypes) {
|
|
435
452
|
for (const [name, vt] of bodyFacts.valTypes) updateRep(name, { val: vt })
|
|
436
453
|
}
|
|
454
|
+
// Never-relocated array bindings — the `[]` reader skips the forwarding follow.
|
|
455
|
+
if (bodyFacts?.neverGrown) for (const name of bodyFacts.neverGrown) updateRep(name, { neverGrown: true })
|
|
437
456
|
// Proven uint32 accumulator locals — readVar tags reads `.unsigned` so the
|
|
438
457
|
// f64 round-trip widens with convert_i32_u (not _s).
|
|
439
458
|
if (bodyFacts?.unsignedLocals) for (const n of bodyFacts.unsignedLocals) updateRep(n, { unsigned: true })
|
|
@@ -526,21 +545,54 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
526
545
|
}
|
|
527
546
|
|
|
528
547
|
function seedLocalIntConsts(body) {
|
|
548
|
+
// Fold each never-reassigned local `const`/`let NAME = EXPR` to a known i32, so a
|
|
549
|
+
// divisor / bound / size built from earlier consts (`rr = R|0; win = 2*rr+1`) becomes
|
|
550
|
+
// a compile-time literal — which lets the int-divide lowering hand the wasm backend a
|
|
551
|
+
// constant divisor to magic-multiply (no runtime sdiv), array bounds resolve, etc.
|
|
552
|
+
// Mirrors the module-scope fold (evalConst above); a string ref resolves through the
|
|
553
|
+
// intConst already recorded on its rep, and the fixpoint lets a later const see an
|
|
554
|
+
// earlier one regardless of declaration order. Skips nested functions (own scope).
|
|
555
|
+
const evalC = (n) => {
|
|
556
|
+
if (typeof n === 'number') return Number.isInteger(n) ? n : null
|
|
557
|
+
if (Array.isArray(n) && n[0] == null && typeof n[1] === 'number') return Number.isInteger(n[1]) ? n[1] : null
|
|
558
|
+
if (typeof n === 'string') return intLiteralValue(n) // a seeded intConst / literal local
|
|
559
|
+
if (!Array.isArray(n)) return null
|
|
560
|
+
const [op, a, b] = n
|
|
561
|
+
const va = evalC(a); if (va == null) return null
|
|
562
|
+
if (op === 'u-' || (op === '-' && b === undefined)) return -va
|
|
563
|
+
const vb = evalC(b); if (vb == null) return null
|
|
564
|
+
switch (op) {
|
|
565
|
+
case '+': return va + vb; case '-': return va - vb; case '*': return va * vb
|
|
566
|
+
case '&': return va & vb; case '|': return va | vb; case '^': return va ^ vb
|
|
567
|
+
case '<<': return va << vb; case '>>': return va >> vb; case '>>>': return va >>> vb
|
|
568
|
+
default: return null
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const decls = []
|
|
529
572
|
const walk = (node) => {
|
|
530
573
|
if (!Array.isArray(node)) return
|
|
531
574
|
const [op, ...args] = node
|
|
532
575
|
if (op === '=>') return
|
|
533
576
|
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
|
-
}
|
|
577
|
+
for (const decl of args)
|
|
578
|
+
if (Array.isArray(decl) && decl[0] === '=' && typeof decl[1] === 'string' && !isReassigned(body, decl[1])) decls.push(decl)
|
|
539
579
|
return
|
|
540
580
|
}
|
|
541
581
|
for (const arg of args) walk(arg)
|
|
542
582
|
}
|
|
543
583
|
walk(body)
|
|
584
|
+
const seeded = new Set()
|
|
585
|
+
let changed = true
|
|
586
|
+
while (changed) {
|
|
587
|
+
changed = false
|
|
588
|
+
for (const decl of decls) {
|
|
589
|
+
if (seeded.has(decl[1])) continue
|
|
590
|
+
const value = evalC(decl[2])
|
|
591
|
+
if (value != null && Number.isInteger(value) && value >= I32_MIN && value <= I32_MAX) {
|
|
592
|
+
updateRep(decl[1], { intConst: value }); seeded.add(decl[1]); changed = true
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
544
596
|
}
|
|
545
597
|
|
|
546
598
|
// ── Loop-invariant exported-param coercion hoist ────────────────────────────
|
|
@@ -562,21 +614,47 @@ function seedLocalIntConsts(body) {
|
|
|
562
614
|
const PARAM_REASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=',
|
|
563
615
|
'^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??=', '++', '--'])
|
|
564
616
|
// Binary ops that unconditionally ToNumber BOTH operands, so a bare param operand
|
|
565
|
-
// is a pure numeric use. `+` is excluded (may concatenate);
|
|
566
|
-
//
|
|
617
|
+
// is a pure numeric use. `+` is excluded (may concatenate); `===`/`==` are excluded
|
|
618
|
+
// (they branch on type, never coerce a string operand to number).
|
|
567
619
|
const NUM_BIN_OPS = new Set(['*', '/', '%', '**', '&', '|', '^', '<<', '>>', '>>>'])
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
620
|
+
// Relational ops: jz has no lexicographic compare for an untyped operand — `<`
|
|
621
|
+
// lowers to `f64.lt`, taking the string path only when a *known-string* operand
|
|
622
|
+
// is present (emit.js cmpOp). So a bare param compared against a non-string is a
|
|
623
|
+
// pure numeric use, same as NUM_BIN_OPS. A string-literal counterpart (`x < "m"`)
|
|
624
|
+
// signals string intent and is rejected (handled in the walk below).
|
|
625
|
+
const REL_OPS = new Set(['<', '<=', '>', '>='])
|
|
626
|
+
// A string literal/template operand poisons relational numeric inference.
|
|
627
|
+
const isStrLiteral = (n) => Array.isArray(n) && (n[0] === 'str' || n[0] === 'template')
|
|
628
|
+
|
|
629
|
+
/** True iff every use of param `name` in `body` is numeric-COMPATIBLE *and* at
|
|
630
|
+
* least one use is numeric-PROVING — so coercing it to a number once at entry is
|
|
631
|
+
* observationally exact. Two verdict levels guard against a polymorphic slot
|
|
632
|
+
* passing on absence of evidence:
|
|
633
|
+
* - PROVING (`proven=true`): arithmetic / relational / bitwise / unary operand —
|
|
634
|
+
* JS ToNumbers these, and a string/array value would have shown a disqualifying
|
|
635
|
+
* use elsewhere.
|
|
636
|
+
* - COMPATIBLE-ONLY: the length slot of `new TypedArray(x)` / `new ArrayBuffer(x)`.
|
|
637
|
+
* A number sizes the buffer, but an array is COPIED and a buffer VIEWED — so a
|
|
638
|
+
* bare param here proves nothing. A param used *solely* as `new Float64Array(arr)`
|
|
639
|
+
* stays unproven and keeps the polymorphic ctor dispatch (else array-copy is lost).
|
|
640
|
+
* Any other appearance (member/call-arg/return/concat/`===`/reassignment) rejects.
|
|
641
|
+
* Two transparencies:
|
|
573
642
|
* - copy aliases: `let x = name` makes `x` carry the same value, so `x`'s uses
|
|
574
643
|
* must be numeric too (fixpoint-collected). Catches `let T = t` then `…T…`.
|
|
575
644
|
* - captured closures: a non-shadowing inner arrow captures the binding by
|
|
576
645
|
* reference, so its body's uses count — we recurse instead of rejecting.
|
|
577
646
|
* Catches floatbeat helpers `let s=(f)=>…t…` that read the param numerically. */
|
|
578
|
-
|
|
647
|
+
// requireProof=true (default): the param has a ToNumber-FORCING use (PROVES numeric).
|
|
648
|
+
// requireProof=false: the param merely has NO string-requiring use (numeric-COMPATIBLE).
|
|
649
|
+
// Forwarding recursions use the latter — a callee receiving the param need only be
|
|
650
|
+
// string-free (e.g. fbm's `ph`, used additively inside Math.sin), since the OUTER
|
|
651
|
+
// param earns its own proof from its own uses; requiring the callee be self-proven
|
|
652
|
+
// wrongly rejected forwards into additive-only params.
|
|
653
|
+
function paramAllUsesNumeric(body, name, _seen = new Set(), requireProof = true) {
|
|
579
654
|
if (body == null) return false
|
|
655
|
+
// Local closure defs (`let f = (p,…) => …`) so a call `f(name)` can be judged by
|
|
656
|
+
// f's own param numericity (see the call-arg handler in the walk).
|
|
657
|
+
const closures = new Map() // name → { params:[string], body }
|
|
580
658
|
// Fixpoint-collect copy aliases: `let/const x = <name-or-alias>`.
|
|
581
659
|
const names = new Set([name])
|
|
582
660
|
for (let grew = true; grew;) {
|
|
@@ -584,15 +662,25 @@ function paramAllUsesNumeric(body, name) {
|
|
|
584
662
|
const collect = (node) => {
|
|
585
663
|
if (!Array.isArray(node)) return
|
|
586
664
|
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
|
|
665
|
+
&& Array.isArray(node[1]) && node[1][0] === '=' && typeof node[1][1] === 'string') {
|
|
666
|
+
const init = node[1][2]
|
|
667
|
+
if (typeof init === 'string' && names.has(init) && !names.has(node[1][1])) { names.add(node[1][1]); grew = true }
|
|
668
|
+
else if (Array.isArray(init) && init[0] === '=>' && !closures.has(node[1][1])) {
|
|
669
|
+
const ps = Array.isArray(init[1]) ? init[1].slice(1) : [init[1]] // ['()', p0, p1] → [p0,p1]
|
|
670
|
+
if (ps.every(p => typeof p === 'string')) closures.set(node[1][1], { params: ps, body: init[2] })
|
|
671
|
+
}
|
|
590
672
|
}
|
|
591
673
|
for (let i = 1; i < node.length; i++) collect(node[i])
|
|
592
674
|
}
|
|
593
675
|
collect(body)
|
|
594
676
|
}
|
|
595
|
-
let ok = true
|
|
677
|
+
let ok = true, proven = false
|
|
678
|
+
// A param in a numeric-operand slot is a PROVING use; recurse into a non-param sub-expr.
|
|
679
|
+
const numOperand = (n) => { if (names.has(n)) proven = true; else walk(n) }
|
|
680
|
+
// Positional call args, flattening the `(, a b c)` node multi-arg calls parse to —
|
|
681
|
+
// without this a forward like `fbm(x, y, t, …)` never matched its param positions.
|
|
682
|
+
const flat1 = (a) => Array.isArray(a) && a[0] === ',' ? a.slice(1).flatMap(flat1) : [a]
|
|
683
|
+
const callArgList = (n) => n.slice(2).flatMap(flat1)
|
|
596
684
|
const walk = (node) => {
|
|
597
685
|
if (!ok) return
|
|
598
686
|
if (typeof node === 'string') { if (names.has(node)) ok = false; return } // bare use → reject
|
|
@@ -617,19 +705,161 @@ function paramAllUsesNumeric(body, name) {
|
|
|
617
705
|
}
|
|
618
706
|
if (PARAM_REASSIGN_OPS.has(op) && names.has(node[1])) { ok = false; return }
|
|
619
707
|
if (NUM_BIN_OPS.has(op) && node.length === 3) { // numeric binary: operands are ToNumber'd
|
|
708
|
+
numOperand(node[1]); numOperand(node[2])
|
|
709
|
+
return
|
|
710
|
+
}
|
|
711
|
+
if (REL_OPS.has(op) && node.length === 3) { // relational: numeric unless a known string is present
|
|
712
|
+
if (isStrLiteral(node[1]) || isStrLiteral(node[2])) { ok = false; return }
|
|
713
|
+
numOperand(node[1]); numOperand(node[2])
|
|
714
|
+
return
|
|
715
|
+
}
|
|
716
|
+
// `new TypedArray(x)` / `new ArrayBuffer(x)`: the length argument is ToNumber'd
|
|
717
|
+
// on the alloc path, but a pointer arg is copied (array) or viewed (buffer).
|
|
718
|
+
// A bare param in the length slot is numeric-COMPATIBLE but not PROVING — skip it
|
|
719
|
+
// (no reject, no proof); other args walk normally. A param used *solely* as
|
|
720
|
+
// `new Float64Array(param)` thus stays unproven → keeps the polymorphic ctor (so
|
|
721
|
+
// `f(arr)` copies the array instead of mis-sizing a zero buffer).
|
|
722
|
+
if (op === '()' && typeof node[1] === 'string' && node[1].startsWith('new.')
|
|
723
|
+
&& (node[1].endsWith('Array') || node[1] === 'new.ArrayBuffer')) {
|
|
724
|
+
for (let i = 2; i < node.length; i++) if (!names.has(node[i])) walk(node[i])
|
|
725
|
+
return
|
|
726
|
+
}
|
|
727
|
+
// Call of a LOCAL closure `f(…name…)`: forwarding the param flows its value into
|
|
728
|
+
// f's positional param. If that param is itself all-numeric (recursively, with a
|
|
729
|
+
// cycle guard), `name` in that slot is numeric-COMPATIBLE — neither rejected nor
|
|
730
|
+
// proving (so a param used *only* as a forwarded arg stays unproven, like the ctor
|
|
731
|
+
// length slot). Unknown / non-numeric callees fall through and reject (a string
|
|
732
|
+
// could flow in). Covers heapsort's `heapify(n)` and crc32's `crc32(buf)`.
|
|
733
|
+
if (op === '()' && typeof node[1] === 'string' && closures.has(node[1]) && !_seen.has(node[1])) {
|
|
734
|
+
const cl = closures.get(node[1])
|
|
735
|
+
const args = callArgList(node)
|
|
736
|
+
for (let i = 0; i < args.length; i++) {
|
|
737
|
+
if (!names.has(args[i])) { walk(args[i]); continue }
|
|
738
|
+
const param = cl.params[i]
|
|
739
|
+
if (param == null || !paramAllUsesNumeric(cl.body, param, new Set([..._seen, node[1]]), false)) { ok = false; return }
|
|
740
|
+
}
|
|
741
|
+
return
|
|
742
|
+
}
|
|
743
|
+
// Same forwarding judgement for a call to a MODULE-LEVEL user function (sibling,
|
|
744
|
+
// not a body-local closure): `frame` passing its param into a helper `fbm(x,y,t,…)`.
|
|
745
|
+
// Without this the bare arg fell through and rejected, leaving an exported numeric
|
|
746
|
+
// param (plasma/raymarcher's `t`) unproven → per-pixel `__to_num` + polymorphic-`+`
|
|
747
|
+
// string forks. Judge by the callee param's own numericity (recursive, cycle-guarded).
|
|
748
|
+
if (op === '()' && typeof node[1] === 'string' && !_seen.has(node[1])) {
|
|
749
|
+
const fn = ctx.func.map?.get(node[1])
|
|
750
|
+
if (fn && fn.body && !fn.raw && Array.isArray(fn.sig?.params) && !fn.rest) {
|
|
751
|
+
const args = callArgList(node)
|
|
752
|
+
for (let i = 0; i < args.length; i++) {
|
|
753
|
+
if (!names.has(args[i])) { walk(args[i]); continue }
|
|
754
|
+
const p = fn.sig.params[i]
|
|
755
|
+
if (!p || !paramAllUsesNumeric(fn.body, p.name, new Set([..._seen, node[1]]), false)) { ok = false; return }
|
|
756
|
+
}
|
|
757
|
+
return
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// `Math.f(...)` ToNumbers every argument (Math operates on numbers), so a bare
|
|
761
|
+
// param in any arg slot is a PROVING numeric use — same contract as `*`/`-`.
|
|
762
|
+
// Without this, `Math.sin(t)` rejected the param via the generic-call fallthrough,
|
|
763
|
+
// so a numeric kernel like `Math.sin(tick) + …` lost its NUMBER proof and paid a
|
|
764
|
+
// per-use `__to_num` + a polymorphic-`+` string-concat fork (interference example).
|
|
765
|
+
// The callee is the lowered `math.sin` string at emit time (post-autoload), or the
|
|
766
|
+
// raw `(. Math sin)` member pre-lowering — match both.
|
|
767
|
+
const isMathCall = op === '()' && (
|
|
768
|
+
(typeof node[1] === 'string' && node[1].startsWith('math.')) ||
|
|
769
|
+
(Array.isArray(node[1]) && node[1][0] === '.' && node[1][1] === 'Math'))
|
|
770
|
+
if (isMathCall) {
|
|
771
|
+
const numArg = (a) => { if (Array.isArray(a) && a[0] === ',') { numArg(a[1]); numArg(a[2]) } else numOperand(a) }
|
|
772
|
+
for (let i = 2; i < node.length; i++) numArg(node[i])
|
|
773
|
+
return
|
|
774
|
+
}
|
|
775
|
+
// Binary `+` is overloaded (numeric add | string concat). A string-literal
|
|
776
|
+
// operand means concat intent → reject. Otherwise it is numeric-COMPATIBLE but
|
|
777
|
+
// not self-PROVING (a string param would concat) — recurse the non-param operand
|
|
778
|
+
// and treat a bare param as compatible (neither prove nor reject), exactly like
|
|
779
|
+
// paramNeverString. The numeric proof must still come from a ToNumber-forcing use
|
|
780
|
+
// (`*`, `Math.*`, …); a param used ONLY in `+` stays unproven (sound).
|
|
781
|
+
if (op === '+' && node.length === 3) {
|
|
782
|
+
if (isStrLiteral(node[1]) || isStrLiteral(node[2])) { ok = false; return }
|
|
620
783
|
if (!names.has(node[1])) walk(node[1])
|
|
621
784
|
if (!names.has(node[2])) walk(node[2])
|
|
622
785
|
return
|
|
623
786
|
}
|
|
624
|
-
if (op === '-' && node.length === 2) {
|
|
625
|
-
if (op === '-' && node.length === 3) {
|
|
787
|
+
if (op === '-' && node.length === 2) { numOperand(node[1]); return } // unary negate
|
|
788
|
+
if (op === '-' && node.length === 3) { numOperand(node[1]); numOperand(node[2]); return }
|
|
626
789
|
// `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) {
|
|
790
|
+
if ((op === 'u-' || op === 'u+') && node.length === 2) { numOperand(node[1]); return }
|
|
791
|
+
if (op === '+' && node.length === 2) { numOperand(node[1]); return } // unary + = ToNumber
|
|
792
|
+
if (op === '~' && node.length === 2) { numOperand(node[1]); return }
|
|
630
793
|
for (let i = 1; i < node.length; i++) walk(node[i]) // bare param reaching here → rejected above
|
|
631
794
|
}
|
|
632
795
|
walk(body)
|
|
796
|
+
return requireProof ? (ok && proven) : ok
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// String methods whose receiver MUST be a string — their presence proves the
|
|
800
|
+
// param is (sometimes) string and disqualifies the boundary-numeric trust.
|
|
801
|
+
const STRING_RECV_METHODS = new Set([
|
|
802
|
+
'charCodeAt', 'charAt', 'codePointAt', 'startsWith', 'endsWith', 'toUpperCase',
|
|
803
|
+
'toLowerCase', 'normalize', 'localeCompare', 'padStart', 'padEnd', 'repeat',
|
|
804
|
+
'trim', 'trimStart', 'trimEnd', 'split', 'match', 'matchAll', 'replace',
|
|
805
|
+
'replaceAll', 'substring', 'substr', 'concat', 'indexOf', 'lastIndexOf',
|
|
806
|
+
'includes', 'slice',
|
|
807
|
+
])
|
|
808
|
+
|
|
809
|
+
/** True iff no use of exported f64 param `name` REQUIRES it to be a string — so
|
|
810
|
+
* the interop boundary contract (`wrapVal` passes a JS number straight to an f64
|
|
811
|
+
* param; a string arg is a type misuse already unsupported, returning NaN) makes
|
|
812
|
+
* it provably numeric. Weaker than `paramAllUsesNumeric`: that PROVES numericity
|
|
813
|
+
* from ToNumber-forcing ops, this DISPROVES stringness so binary `+` (the common
|
|
814
|
+
* `accumulator + cre` shape) no longer pessimistically pulls the string-concat
|
|
815
|
+
* fork into a pure float kernel. Only sound under the export boundary — never use
|
|
816
|
+
* for locals/closures, whose values can genuinely be strings.
|
|
817
|
+
*
|
|
818
|
+
* Disqualifying (string-requiring) uses:
|
|
819
|
+
* - `+` with a string-literal/template operand (`"px" + name`) — concat intent
|
|
820
|
+
* - a string-receiver method call (`name.charCodeAt(…)`, `name.split(…)`)
|
|
821
|
+
* - `name[k]` / `name.length` is NOT disqualifying (works on arrays/typed too,
|
|
822
|
+
* but an f64 param is neither — so a member access means the caller passed a
|
|
823
|
+
* pointer, out of the f64-number contract; conservatively we reject it)
|
|
824
|
+
* - passing `name` to a call / returning it / storing into an aggregate: the
|
|
825
|
+
* value escapes where it could be ToString'd; reject conservatively. */
|
|
826
|
+
function paramNeverString(body, name) {
|
|
827
|
+
if (body == null) return false
|
|
828
|
+
let ok = true
|
|
829
|
+
const walk = (node) => {
|
|
830
|
+
if (!ok || node == null) return
|
|
831
|
+
if (typeof node === 'string') { if (node === name) ok = false; return } // bare escape → reject
|
|
832
|
+
if (!Array.isArray(node)) return
|
|
833
|
+
const op = node[0]
|
|
834
|
+
if (op === '=>') return // shadowing-safe: closure handled conservatively (escape)
|
|
835
|
+
// `+` (binary): a string-literal/template operand makes it concat → reject.
|
|
836
|
+
// Otherwise the param is in an arithmetic add; recurse the non-name operand.
|
|
837
|
+
if (op === '+' && node.length === 3) {
|
|
838
|
+
if (isStrLiteral(node[1]) || isStrLiteral(node[2])) { ok = false; return }
|
|
839
|
+
for (let i = 1; i <= 2; i++) if (node[i] !== name) walk(node[i])
|
|
840
|
+
return
|
|
841
|
+
}
|
|
842
|
+
// Numeric/relational/bitwise binary + unary: param operand is fine, recurse rest.
|
|
843
|
+
if ((NUM_BIN_OPS.has(op) || REL_OPS.has(op)) && node.length === 3) {
|
|
844
|
+
for (let i = 1; i <= 2; i++) if (node[i] !== name) walk(node[i])
|
|
845
|
+
return
|
|
846
|
+
}
|
|
847
|
+
if ((op === 'u-' || op === 'u+' || op === '~') && node.length === 2) {
|
|
848
|
+
if (node[1] !== name) walk(node[1]); return
|
|
849
|
+
}
|
|
850
|
+
if (op === '-' && (node.length === 2 || node.length === 3)) {
|
|
851
|
+
for (let i = 1; i < node.length; i++) if (node[i] !== name) walk(node[i])
|
|
852
|
+
return
|
|
853
|
+
}
|
|
854
|
+
// Member access / method call on the param → it's a pointer, not an f64 number:
|
|
855
|
+
// reject (out of contract). `.`/`?.`/`[]` with the name as receiver.
|
|
856
|
+
if ((op === '.' || op === '?.' || op === '[]') && node[1] === name) { ok = false; return }
|
|
857
|
+
// `=`/compound reassignment of the param to a non-numeric value: reject if it
|
|
858
|
+
// could become a string. A reassignment makes the param mutable — conservatively
|
|
859
|
+
// require the RHS to be string-free too (recurse), and the target isn't a use.
|
|
860
|
+
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
861
|
+
}
|
|
862
|
+
walk(body)
|
|
633
863
|
return ok
|
|
634
864
|
}
|
|
635
865
|
|
|
@@ -1025,6 +1255,32 @@ function emitClosureBody(cb) {
|
|
|
1025
1255
|
if (ptRow[i] === true && !ctx.func.localReps?.get(cb.params[i])?.val)
|
|
1026
1256
|
updateRep(cb.params[i], i < minArgc ? { val: VAL.NUMBER } : { val: VAL.NUMBER, nullable: true })
|
|
1027
1257
|
}
|
|
1258
|
+
// A param passed the same typed-array ctor at every direct call site is TYPED:
|
|
1259
|
+
// register its element ctor so `buf[i]` reads use the typed load (it stays an f64
|
|
1260
|
+
// NaN-box in the closure ABI, but knowing the kind avoids the dynamic `__typed_idx`
|
|
1261
|
+
// /`__len` dispatch that pulls the string runtime). Numeric trust (above) wins if it
|
|
1262
|
+
// already classified the slot — they're disjoint anyway (NUMBER vs TYPED arg).
|
|
1263
|
+
const tcRow = ctx.closure.paramTypedCtors?.get(cb.name)
|
|
1264
|
+
if (tcRow) for (let i = 0; i < cb.params.length; i++) {
|
|
1265
|
+
const ctor = tcRow[i]
|
|
1266
|
+
if (ctor && !ctx.func.localReps?.get(cb.params[i])?.val) {
|
|
1267
|
+
updateRep(cb.params[i], { val: VAL.TYPED })
|
|
1268
|
+
;(ctx.types.typedElem ||= new Map()).set(cb.params[i], ctor)
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
// Body-usage numeric trust for closure params — the same proof the export path
|
|
1272
|
+
// applies (paramAllUsesNumeric). A nested helper like heapsort's `heapify(n)` whose
|
|
1273
|
+
// param is used only in arithmetic/relational positions is VAL.NUMBER, so `(n>>1)-1`
|
|
1274
|
+
// skips the `__to_num` coercion that would otherwise drag the ToNumber string-parse
|
|
1275
|
+
// tree into a pure-integer kernel. paramAllUsesNumeric walks any AST node, so this
|
|
1276
|
+
// also covers expression-bodied arrows (`(m) => m | 0`) — the common closure shape
|
|
1277
|
+
// whose dynamic param would otherwise emit a polymorphic add/coerce that pulls the
|
|
1278
|
+
// whole string runtime in. Call-site evidence (ptRow) already covers the monomorphic
|
|
1279
|
+
// case; this also catches params the lattice left unobserved.
|
|
1280
|
+
for (const p of cb.params) {
|
|
1281
|
+
if (!ctx.func.localReps?.get(p)?.val && !cb.defaults?.[p] && paramAllUsesNumeric(cb.body, p))
|
|
1282
|
+
updateRep(p, { val: VAL.NUMBER })
|
|
1283
|
+
}
|
|
1028
1284
|
|
|
1029
1285
|
// Register captured variable locals: boxed = i32 cell pointer, otherwise f64 value
|
|
1030
1286
|
for (let i = 0; i < cb.captures.length; i++) {
|
|
@@ -1308,6 +1564,11 @@ export default function compile(ast, profiler) {
|
|
|
1308
1564
|
}
|
|
1309
1565
|
}
|
|
1310
1566
|
|
|
1567
|
+
// Whole-program constant fold of module-scope aggregate literals — `var x=[1,2,3];
|
|
1568
|
+
// y=x[0]` → `y=1`, dropping the array (no data segment, no __arr_idx_known) when
|
|
1569
|
+
// every reference is a static read. The scalar analog of the constInts fold above.
|
|
1570
|
+
timePhase(profiler, 'foldAggregates', () => foldStaticConstAggregates(ast))
|
|
1571
|
+
|
|
1311
1572
|
const programFacts = timePhase(profiler, 'plan', () => plan(ast, profiler))
|
|
1312
1573
|
|
|
1313
1574
|
// Inspect sink: editor hosts opt in via { inspect: true } to read inferred shapes.
|
|
@@ -1449,12 +1710,28 @@ export default function compile(ast, profiler) {
|
|
|
1449
1710
|
|
|
1450
1711
|
timePhase(profiler, 'optimizeModule', () => optimizeModule(sec, profiler))
|
|
1451
1712
|
|
|
1713
|
+
// Fold constant `__start` global inits into immutable inline decls (drops the
|
|
1714
|
+
// store, and `__start` with it when that empties it). Runs HERE — after
|
|
1715
|
+
// stripStaticDataPrefix and optimizeModule — so any data-segment offset a hoisted
|
|
1716
|
+
// pointer carries is already in its final, shifted form (hoisting earlier would
|
|
1717
|
+
// freeze a pre-strip offset the shift pass never revisits in the global decl).
|
|
1718
|
+
hoistConstGlobalInits(sec)
|
|
1719
|
+
|
|
1452
1720
|
// Populate globals (after __start — const folding may update declarations).
|
|
1453
1721
|
// Records build IR directly — no WAT-text parse-back.
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1722
|
+
// The wasm type comes from globalTypes (the canonical name→type map declGlobal
|
|
1723
|
+
// maintains alongside the entry), falling back to the entry's own `.type`. They
|
|
1724
|
+
// are normally identical, but a global whose entry object is later rebuilt (e.g.
|
|
1725
|
+
// hoistConstGlobalInits' `{...g, …}` spread) must not depend on that rebuild
|
|
1726
|
+
// preserving `.type` — globalTypes is the stable source, so an entry that lost
|
|
1727
|
+
// its `.type` still emits a well-typed `(global …)` rather than `(undefined.const)`.
|
|
1728
|
+
sec.globals.push(...[...ctx.scope.globals].filter(([, g]) => g).map(([n, g]) => {
|
|
1729
|
+
const ty = ctx.scope.globalTypes.get(n) ?? g.type
|
|
1730
|
+
return ['global', `$${n}`,
|
|
1731
|
+
...(g.export ? [['export', `"${g.export}"`]] : []),
|
|
1732
|
+
g.mut ? ['mut', ty] : ty,
|
|
1733
|
+
[`${ty}.const`, g.init]]
|
|
1734
|
+
}))
|
|
1458
1735
|
|
|
1459
1736
|
// Data segments (after emit — string literals append to ctx.runtime.data / strPool during emit)
|
|
1460
1737
|
// Active segment at address 0 — skipped for shared memory (would collide across modules)
|
|
@@ -1564,7 +1841,8 @@ export default function compile(ast, profiler) {
|
|
|
1564
1841
|
const { callCount } = treeshake(
|
|
1565
1842
|
[{ arr: sec.stdlib }, { arr: sec.funcs }, { arr: sec.start }],
|
|
1566
1843
|
[...sec.start, ...sec.elem, ...sec.customs, ...sec.extStdlib, ...sec.imports, ...sec.tags],
|
|
1567
|
-
{ removeDead: !optCfg || optCfg.treeshake !== false, globals: sec.globals
|
|
1844
|
+
{ removeDead: !optCfg || optCfg.treeshake !== false, globals: sec.globals, userGlobals: ctx.scope.userGlobals,
|
|
1845
|
+
userFuncs: new Set(ctx.func.list.map(f => `$${f.name}`)) }
|
|
1568
1846
|
)
|
|
1569
1847
|
|
|
1570
1848
|
pruneUnusedThrowRuntime(sec)
|
package/src/compile/infer.js
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
import { ctx } from '../ctx.js'
|
|
47
47
|
import { collectParamNames, ASSIGN_OPS } from '../ast.js'
|
|
48
48
|
import { analyzeValTypes, analyzeIntCertain } from './analyze.js'
|
|
49
|
-
import { staticObjectProps } from '../static.js'
|
|
49
|
+
import { staticObjectProps, staticArrayElems } from '../static.js'
|
|
50
50
|
import { typedElemCtor } from '../type.js'
|
|
51
51
|
import { ctorFromElemAux } from '../../layout.js'
|
|
52
52
|
import { shapeOfObjectLiteralAst, valTypeOf } from '../kind.js'
|
|
@@ -341,6 +341,41 @@ export function recordGlobalRep(name, expr) {
|
|
|
341
341
|
}
|
|
342
342
|
const ctor = typedElemCtor(expr)
|
|
343
343
|
if (ctor) (ctx.scope.globalTypedElem ||= new Map()).set(name, ctor)
|
|
344
|
+
// Module-level const array literal with a uniform element val-type (e.g. a numeric
|
|
345
|
+
// table `const FREQS = [261.63, …]`): record it so `FREQS[i]` reads in any using
|
|
346
|
+
// function are typed (NUMBER) rather than untyped. Without this an untyped element
|
|
347
|
+
// read makes `s += FREQS[i]` take the polymorphic +/ToString path — dragging the
|
|
348
|
+
// entire string runtime (~5 kB) into a kernel that uses no strings. A function-local
|
|
349
|
+
// array gets this from analyzeValTypes; a module-level one is invisible to the using
|
|
350
|
+
// function's body walk, so capture it here. Soundness for a later `FREQS[i]=…` is the
|
|
351
|
+
// read-site dynWriteVars guard in valTypeOf (kind.js) — this is just the literal fact.
|
|
352
|
+
if (vt === VAL.ARRAY) {
|
|
353
|
+
const elems = staticArrayElems(expr)
|
|
354
|
+
if (elems && elems.length && elems.every(e => e != null)) {
|
|
355
|
+
let common = valTypeOf(elems[0])
|
|
356
|
+
for (let k = 1; k < elems.length && common != null; k++)
|
|
357
|
+
if (valTypeOf(elems[k]) !== common) common = null
|
|
358
|
+
if (common != null) updateGlobalRep(name, { arrayElemValType: common })
|
|
359
|
+
// Array-of-arrays numeric table (`const C = [[0,4,7], …]`): also record the
|
|
360
|
+
// nested element kind so `C[i][j]` (and `ch = C[i]; ch[j]`) reads stay typed —
|
|
361
|
+
// the same string-runtime drop as the flat case, one level down. Single-level
|
|
362
|
+
// (mirrors analyzeValTypes' local arrElemElemValTypes); deeper nesting falls back.
|
|
363
|
+
if (common === VAL.ARRAY) {
|
|
364
|
+
let nested = null, seen = false, ok = true
|
|
365
|
+
for (const el of elems) {
|
|
366
|
+
const inner = staticArrayElems(el)
|
|
367
|
+
if (!inner || !inner.length || !inner.every(e => e != null)) { ok = false; break }
|
|
368
|
+
for (const ie of inner) {
|
|
369
|
+
const ivt = valTypeOf(ie)
|
|
370
|
+
if (!seen) { nested = ivt; seen = true }
|
|
371
|
+
else if (ivt !== nested) { ok = false; break }
|
|
372
|
+
}
|
|
373
|
+
if (!ok) break
|
|
374
|
+
}
|
|
375
|
+
if (ok && nested != null) updateGlobalRep(name, { arrayElemElemValType: nested })
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
344
379
|
// Static-shape capture for module-level object literals — lets `{ ...G.path }`
|
|
345
380
|
// resolve its source schema by walking the global rep's shape tree at the
|
|
346
381
|
// spread site (see shape walk in analyze.js / resolveSchema in object.js).
|