jz 0.8.0 → 0.8.1
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/bench/bench.svg +14 -14
- package/dist/jz.js +897 -804
- package/module/array.js +20 -0
- package/module/math.js +148 -121
- package/module/string.js +133 -15
- package/package.json +2 -2
- package/src/abi/string.js +11 -6
- package/src/compile/emit.js +77 -17
- package/src/compile/index.js +6 -0
- package/src/compile/infer.js +8 -1
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/narrow.js +12 -3
- package/src/optimize/index.js +166 -1
- package/src/optimize/vectorize.js +10 -0
- package/src/prepare/index.js +18 -5
package/src/abi/string.js
CHANGED
|
@@ -373,15 +373,20 @@ export const sso = {
|
|
|
373
373
|
* dispatcher's string/array runtime guess (emit.js) would hijack it into a
|
|
374
374
|
* bogus array concat. A non-builtin name routes through dynamic property
|
|
375
375
|
* dispatch (load the closure slot, call it) correctly. */
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
376
|
+
// `ext` (default false) opts into the bump-EXTEND fast path — sound only when emit
|
|
377
|
+
// proves `a` is dead-after (a self-accumulation `x = x + …`). Otherwise the _fresh twin
|
|
378
|
+
// alloc+copies, never mutating the live `a` operand. (See __str_concat in module/string.js.)
|
|
379
|
+
cat: (aF64, bF64, ctx, ext = false) => {
|
|
380
|
+
const fn = ext ? '__str_concat' : '__str_concat_fresh'
|
|
381
|
+
ctx.core.includes.add(fn)
|
|
382
|
+
return ['call', '$' + fn, ssoI64(aF64), ssoI64(bF64)]
|
|
379
383
|
},
|
|
380
384
|
|
|
381
385
|
/** Concat assuming both sides are already strings (skip ToString). */
|
|
382
|
-
concatRaw: (aF64, bF64, ctx) => {
|
|
383
|
-
|
|
384
|
-
|
|
386
|
+
concatRaw: (aF64, bF64, ctx, ext = false) => {
|
|
387
|
+
const fn = ext ? '__str_concat_raw' : '__str_concat_raw_fresh'
|
|
388
|
+
ctx.core.includes.add(fn)
|
|
389
|
+
return ['call', '$' + fn, ssoI64(aF64), ssoI64(bF64)]
|
|
385
390
|
},
|
|
386
391
|
},
|
|
387
392
|
}
|
package/src/compile/emit.js
CHANGED
|
@@ -1553,6 +1553,41 @@ const isCheapPureVal = (n) => {
|
|
|
1553
1553
|
return false
|
|
1554
1554
|
}
|
|
1555
1555
|
|
|
1556
|
+
// Side-effect-free: no writes (assignment / ++ / --), no calls, no closures, no throw. UNLIKE
|
|
1557
|
+
// `isCheapPureVal` this ALLOWS loads, member reads, and `/` `%` — a side-effect-free expr may read
|
|
1558
|
+
// memory or trap. It is the right gate for an `if` CONDITION promoted to a `select` condition: the
|
|
1559
|
+
// condition is evaluated exactly once whether the lowering branches or selects (any trap fires the
|
|
1560
|
+
// same in both, the read order vs the pure value arm is immaterial), so it need only avoid MUTATING
|
|
1561
|
+
// state the value arm could read — i.e. be side-effect-free, not unconditionally-evaluable.
|
|
1562
|
+
const SIDE_EFFECT_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=',
|
|
1563
|
+
'>>>=', '||=', '&&=', '??=', '++', '--', '()', '=>', 'throw', 'new', 'await', 'yield'])
|
|
1564
|
+
const isSideEffectFree = (n) => {
|
|
1565
|
+
if (!Array.isArray(n)) return true
|
|
1566
|
+
if (typeof n[0] === 'string' && SIDE_EFFECT_OPS.has(n[0])) return false
|
|
1567
|
+
for (let i = 1; i < n.length; i++) if (!isSideEffectFree(n[i])) return false
|
|
1568
|
+
return true
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
const isLit1 = (n) => Array.isArray(n) && n[0] == null && n[1] === 1
|
|
1572
|
+
// A void statement whose whole effect is `x = <cheap pure value>` for a simple local `x` — the
|
|
1573
|
+
// shape if→select can lower to `x = cond ? value : x`. Recognizes the plain assignment plus the
|
|
1574
|
+
// increment forms `++x`/`--x` and their postfix lowerings `(++x) - 1` / `(--x) + 1` (prepare turns
|
|
1575
|
+
// `x++` in statement position into the latter; the discarded ∓1 is dead in void context, so the
|
|
1576
|
+
// net effect is the increment). Returns `{ lhs, val }` or null.
|
|
1577
|
+
function matchVoidLocalStore(s) {
|
|
1578
|
+
if (!Array.isArray(s)) return null
|
|
1579
|
+
if (s[0] === '=' && typeof s[1] === 'string' && isCheapPureVal(s[2])) return { lhs: s[1], val: s[2] }
|
|
1580
|
+
if ((s[0] === '++' || s[0] === '--') && typeof s[1] === 'string')
|
|
1581
|
+
return { lhs: s[1], val: [s[0] === '++' ? '+' : '-', s[1], [, 1]] }
|
|
1582
|
+
// postfix: `x++` → `(++x) - 1`, `x--` → `(--x) + 1`
|
|
1583
|
+
if ((s[0] === '-' || s[0] === '+') && isLit1(s[2]) && Array.isArray(s[1])
|
|
1584
|
+
&& (s[1][0] === '++' || s[1][0] === '--') && typeof s[1][1] === 'string') {
|
|
1585
|
+
const inc = s[1][0] === '++'
|
|
1586
|
+
if ((inc && s[0] === '-') || (!inc && s[0] === '+')) return { lhs: s[1][1], val: [inc ? '+' : '-', s[1][1], [, 1]] }
|
|
1587
|
+
}
|
|
1588
|
+
return null
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1556
1591
|
function emitLooseEq(a, b, negate) {
|
|
1557
1592
|
const eqOp = negate ? 'ne' : 'eq'
|
|
1558
1593
|
const sentinel = emitNum(negate ? 1 : 0)
|
|
@@ -2713,7 +2748,15 @@ export const emitter = {
|
|
|
2713
2748
|
inc('__to_num')
|
|
2714
2749
|
return writeVar(name, typed(['call', '$__to_num', asI64(emit(name))], 'f64'), void_)
|
|
2715
2750
|
}
|
|
2716
|
-
|
|
2751
|
+
// Self-accumulation `x = x + …` (incl. desugared `x += …`): the new value REPLACES x, so x's
|
|
2752
|
+
// old buffer is dead — the one context where a string concat may bump-EXTEND it in place. The
|
|
2753
|
+
// `+` handler reads this flag for its immediate concat; nested operands clear it (not the target).
|
|
2754
|
+
const selfAccum = Array.isArray(val) && val[0] === '+' && val[1] === name
|
|
2755
|
+
const prevSA = ctx.func._selfAccumConcat
|
|
2756
|
+
ctx.func._selfAccumConcat = selfAccum ? name : null
|
|
2757
|
+
const ev = emit(val)
|
|
2758
|
+
ctx.func._selfAccumConcat = prevSA
|
|
2759
|
+
return writeVar(name, ev, void_)
|
|
2717
2760
|
},
|
|
2718
2761
|
|
|
2719
2762
|
// Compound assignments: read-modify-write with type coercion
|
|
@@ -2803,15 +2846,18 @@ export const emitter = {
|
|
|
2803
2846
|
// Postfix in void: (++i)-1 / (--i)+1 → just ++i / --i
|
|
2804
2847
|
'+': (a, b) => {
|
|
2805
2848
|
if (ctx.func._expect === 'void' && isPostfix(a, '--', b)) return emit(a, 'void')
|
|
2849
|
+
// A self-accumulation `a = a + …` lets the concat bump-EXTEND `a` in place (a is dead-after).
|
|
2850
|
+
// Read it for THIS concat, then clear so nested operands (not the accumulation target) stay fresh.
|
|
2851
|
+
const selfAccum = typeof a === 'string' && a === ctx.func._selfAccumConcat
|
|
2852
|
+
ctx.func._selfAccumConcat = null
|
|
2806
2853
|
// String concatenation: pure string operands skip generic ToString coercion.
|
|
2807
2854
|
const vtA = valTypeOf(a)
|
|
2808
2855
|
const vtB = valTypeOf(b)
|
|
2809
2856
|
if (vtA === VAL.STRING && vtB === VAL.STRING) {
|
|
2810
|
-
// Fused append-byte: `buf += s[i]` skips 1-char SSO construction +
|
|
2811
|
-
//
|
|
2812
|
-
//
|
|
2813
|
-
|
|
2814
|
-
if (Array.isArray(b) && b[0] === '[]' && ctx.core.stdlib['__str_append_byte'] && ctx.core.stdlib['__char_at']) {
|
|
2857
|
+
// Fused append-byte: `buf += s[i]` skips 1-char SSO construction + generic concat dispatch
|
|
2858
|
+
// when rhs is a string-index. The byte flows straight from __char_at into memory and bump-
|
|
2859
|
+
// EXTENDS the heap-top lhs — so only when proven self-accumulating (else it mutates a live s).
|
|
2860
|
+
if (selfAccum && Array.isArray(b) && b[0] === '[]' && ctx.core.stdlib['__str_append_byte'] && ctx.core.stdlib['__char_at']) {
|
|
2815
2861
|
if (valTypeOf(b[1]) === VAL.STRING) {
|
|
2816
2862
|
inc('__str_append_byte', '__char_at')
|
|
2817
2863
|
return typed(['call', '$__str_append_byte',
|
|
@@ -2820,7 +2866,7 @@ export const emitter = {
|
|
|
2820
2866
|
], 'f64')
|
|
2821
2867
|
}
|
|
2822
2868
|
}
|
|
2823
|
-
return typed(ctx.abi.string.ops.concatRaw(asF64(emit(a)), asF64(emit(b)), ctx), 'f64')
|
|
2869
|
+
return typed(ctx.abi.string.ops.concatRaw(asF64(emit(a)), asF64(emit(b)), ctx, selfAccum), 'f64')
|
|
2824
2870
|
}
|
|
2825
2871
|
if (vtA === VAL.STRING || vtB === VAL.STRING) {
|
|
2826
2872
|
// An OBJECT operand coerces via ToPrimitive(string) at compile time —
|
|
@@ -2840,10 +2886,10 @@ export const emitter = {
|
|
|
2840
2886
|
const coercionFree = (vt) => vt === VAL.STRING || vt === VAL.OBJECT || vt === VAL.BOOL
|
|
2841
2887
|
const cfA = coercionFree(vtA), cfB = coercionFree(vtB)
|
|
2842
2888
|
const strI64 = (n) => typed(['f64.reinterpret_i64', toStrI64(n, emit(n))], 'f64')
|
|
2843
|
-
if (cfA && cfB) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strOperand(vtB, b), ctx), 'f64')
|
|
2844
|
-
if (cfA) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strI64(b), ctx), 'f64')
|
|
2845
|
-
if (cfB) return typed(ctx.abi.string.ops.concatRaw(strI64(a), strOperand(vtB, b), ctx), 'f64')
|
|
2846
|
-
return typed(ctx.abi.string.ops.cat(strOperand(vtA, a), strOperand(vtB, b), ctx), 'f64')
|
|
2889
|
+
if (cfA && cfB) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strOperand(vtB, b), ctx, selfAccum), 'f64')
|
|
2890
|
+
if (cfA) return typed(ctx.abi.string.ops.concatRaw(strOperand(vtA, a), strI64(b), ctx, selfAccum), 'f64')
|
|
2891
|
+
if (cfB) return typed(ctx.abi.string.ops.concatRaw(strI64(a), strOperand(vtB, b), ctx, selfAccum), 'f64')
|
|
2892
|
+
return typed(ctx.abi.string.ops.cat(strOperand(vtA, a), strOperand(vtB, b), ctx, selfAccum), 'f64')
|
|
2847
2893
|
}
|
|
2848
2894
|
if (vtA === VAL.BIGINT || vtB === VAL.BIGINT)
|
|
2849
2895
|
return fromI64(['i64.add', asI64(emit(a)), asI64(emit(b))])
|
|
@@ -2853,6 +2899,12 @@ export const emitter = {
|
|
|
2853
2899
|
// operand needs the runtime check.
|
|
2854
2900
|
if ((vtA == null || vtB == null) && ctx.core.stdlib['__str_concat']) {
|
|
2855
2901
|
const tA = temp('add'), tB = temp('add')
|
|
2902
|
+
// Fully-untyped `+`: the string arm is a runtime-guarded cold path that the engine reaches
|
|
2903
|
+
// only if BOTH operands are strings at runtime, so it keeps the bump-extend `__str_concat`
|
|
2904
|
+
// (its body stays out-of-line — folding it to the smaller _fresh twin would inline this
|
|
2905
|
+
// never-numeric branch into every hot integer loop). The demonstrated `t = s + "lit"` mutation
|
|
2906
|
+
// is a TYPED concat (handled by concatRaw above); a both-untyped self-mutation stays the
|
|
2907
|
+
// documented rare-aliasing tradeoff. Self-accumulation is still safe to extend.
|
|
2856
2908
|
inc('__str_concat', '__is_str_key')
|
|
2857
2909
|
const checkA = vtA == null ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tA}`, asF64(emit(a))]]] : null
|
|
2858
2910
|
const checkB = vtB == null ? ['call', '$__is_str_key', ['i64.reinterpret_f64', ['local.tee', `$${tB}`, asF64(emit(b))]]] : null
|
|
@@ -3297,13 +3349,17 @@ export const emitter = {
|
|
|
3297
3349
|
// If-conversion (speed tier): `if (cond) x = <cheap pure value>` (no else) → `x = cond ? value
|
|
3298
3350
|
// : x`, which lowers to a branchless `select`. Removes the data-dependent branch (and its
|
|
3299
3351
|
// misprediction) from min/max/clamp reductions — e.g. levenshtein's `if (ins < m) m = ins`,
|
|
3300
|
-
// ~27% faster
|
|
3301
|
-
//
|
|
3302
|
-
//
|
|
3303
|
-
|
|
3352
|
+
// ~27% faster — and from heapsort's child pick `if (a[c] < a[c+1]) c++`, the canonical
|
|
3353
|
+
// unpredictable compare that costs jz on x86 (Cranelift/V8-x64 keep the branch; Binaryen, which
|
|
3354
|
+
// AS uses, selects it). The condition is evaluated exactly once whether we branch or select, so
|
|
3355
|
+
// it need only be SIDE-EFFECT-FREE (loads allowed — sort's `a[c] < a[c+1]`); only the assigned
|
|
3356
|
+
// VALUE is evaluated unconditionally, hence must be a cheap, trap-free pure expr. `x++`/`x--`
|
|
3357
|
+
// are admitted as `x = x ± 1`. The already-emitted condition `ce` is reused (`__emitted`), so a
|
|
3358
|
+
// load-bearing condition is not emitted twice.
|
|
3359
|
+
if (els == null && ctx.transform.optimize?.boolConvertToSelect && isSideEffectFree(cond)) {
|
|
3304
3360
|
const asg = Array.isArray(then) && then[0] === ';' && then.length === 2 ? then[1] : then
|
|
3305
|
-
|
|
3306
|
-
|
|
3361
|
+
const sel = matchVoidLocalStore(asg)
|
|
3362
|
+
if (sel) return emitVoid(['=', sel.lhs, ['?:', ['__emitted', ce], sel.val, sel.lhs]])
|
|
3307
3363
|
}
|
|
3308
3364
|
const c = ce.type === 'i32' ? ce : toBoolFromEmitted(ce)
|
|
3309
3365
|
// Flow-sensitive type refinement: narrow types within each branch based on the guard.
|
|
@@ -3566,6 +3622,10 @@ export function emit(node, expect) {
|
|
|
3566
3622
|
if (node.loc != null) ctx.error.loc = node.loc
|
|
3567
3623
|
}
|
|
3568
3624
|
if (node == null) return null
|
|
3625
|
+
// Pre-emitted IR passthrough: `['__emitted', ir]` returns `ir` untouched. Lets a caller that
|
|
3626
|
+
// already emitted a subtree (e.g. the `if` handler's condition) splice it into an AST-shaped
|
|
3627
|
+
// re-emit (a `?:` for if→select conversion) without emitting it a second time.
|
|
3628
|
+
if (Array.isArray(node) && node[0] === '__emitted') return node[1]
|
|
3569
3629
|
// Boolean literals carry VAL.BOOL for type observation (valTypeOf reads the
|
|
3570
3630
|
// AST), but their working representation is the plain number 0/1 — identical
|
|
3571
3631
|
// codegen to the pre-carrier `[, 1]`/`[, 0]` folding, so no perf is paid.
|
package/src/compile/index.js
CHANGED
|
@@ -40,6 +40,7 @@ import { inferLocals } from './infer.js'
|
|
|
40
40
|
import { optimizeFunc, treeshake } from '../optimize/index.js'
|
|
41
41
|
import { strengthReduceLoopDivMod } from './loop-divmod.js'
|
|
42
42
|
import { narrowBoundedSquare } from './loop-square.js'
|
|
43
|
+
import { unrollRecurrence } from './loop-recurrence.js'
|
|
43
44
|
import { peelClampedStencil } from './peel-stencil.js'
|
|
44
45
|
import { cseLoads } from './cse-load.js'
|
|
45
46
|
|
|
@@ -426,6 +427,11 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
426
427
|
// so the sieve's product/counter chain carries i32 instead of f64. Before analyze so the
|
|
427
428
|
// Math.imul typed/narrows like any i32. Off at L0 / `loopSquare:false`.
|
|
428
429
|
if (_o && _o.loopSquare !== false && isBlockBody(func.body)) func.body = narrowBoundedSquare(func.body)
|
|
430
|
+
// Array-recurrence unroll: a unit-stride DP/scan that reads arr[j-1] and writes arr[j] carries
|
|
431
|
+
// its value through memory (store→load) and re-pays loop overhead per cell — both of which V8
|
|
432
|
+
// hides but Cranelift/baseline don't. Scalar-replace the recurrence + unroll ×2 (clang's fix).
|
|
433
|
+
// Off at L0 / `unrollRecurrence:false`.
|
|
434
|
+
if (_o && _o.unrollRecurrence !== false && isBlockBody(func.body)) func.body = unrollRecurrence(func.body)
|
|
429
435
|
// Edge-clamp peeling: split a clamped stencil loop into clamp-free interior + edges
|
|
430
436
|
// (the interior then lifts to SIMD). Before analyze so the new loops are analyzed.
|
|
431
437
|
if (_o && _o.clampPeel !== false && isBlockBody(func.body)) func.body = peelClampedStencil(func.body)
|
package/src/compile/infer.js
CHANGED
|
@@ -130,11 +130,18 @@ export const inferParams = (body, candidates) => {
|
|
|
130
130
|
// regardless of prior evidence — a later method call can't re-induce a shape
|
|
131
131
|
// already contradicted by an earlier scalar assignment.
|
|
132
132
|
|
|
133
|
+
// Methods that exist ONLY on String.prototype — seeing one on a bare binding proves
|
|
134
|
+
// it is a string. `indexOf`/`includes`/`lastIndexOf`/`concat`/`slice`/`at` are NOT here:
|
|
135
|
+
// Array.prototype has them too, so the receiver is genuinely ambiguous (and the argument
|
|
136
|
+
// can't disambiguate — String coerces it, Arrays hold strings). Those keep the runtime
|
|
137
|
+
// __ptr_type fork, which is correct for both; forcing `lastIndexOf` to string here used to
|
|
138
|
+
// miscompile `arr.lastIndexOf(x)` to -1. A string-using param still narrows via any of the
|
|
139
|
+
// real discriminators below (charCodeAt, a string assignment, a string-passing call site).
|
|
133
140
|
const STRING_ONLY_METHODS = new Set([
|
|
134
141
|
'charCodeAt', 'charAt', 'codePointAt', 'startsWith', 'endsWith',
|
|
135
142
|
'toUpperCase', 'toLowerCase', 'toLocaleLowerCase', 'normalize', 'localeCompare',
|
|
136
143
|
'padStart', 'padEnd', 'repeat', 'trimStart', 'trimEnd', 'trim',
|
|
137
|
-
'matchAll', 'match', 'replace', 'replaceAll', 'split',
|
|
144
|
+
'matchAll', 'match', 'replace', 'replaceAll', 'split',
|
|
138
145
|
])
|
|
139
146
|
const ARRAY_ONLY_POISON = new Set([
|
|
140
147
|
'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'fill', 'reverse',
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Partial unroll (×2) + scalar replacement of a unit-stride ARRAY RECURRENCE.
|
|
2
|
+
//
|
|
3
|
+
// A DP/scan loop that reads `arr[j-1]` and writes `arr[j]` (unit stride) carries the
|
|
4
|
+
// just-written value to the next iteration THROUGH MEMORY: it stores `arr[j]`, then the next
|
|
5
|
+
// iteration loads `arr[j-1]` — the very cell it just wrote. V8/TurboFan forwards that store→load
|
|
6
|
+
// and unrolls the loop internally; Cranelift/wasmtime and the baseline tiers do neither, so the
|
|
7
|
+
// loop pays a store→load round trip plus full per-iteration overhead on every cell. clang/gcc
|
|
8
|
+
// fix exactly this with this transform — measured 2.15× on wasmtime for the Levenshtein DP
|
|
9
|
+
// (V8-neutral, bit-exact).
|
|
10
|
+
//
|
|
11
|
+
// Recognized (post-prepare AST): a unit-stride `for (let j = LO; j </<= HI; j++)` whose body, for
|
|
12
|
+
// ONE array `arr`, has a single store `arr[j] = <var>` and ≥1 read `arr[j-1]`, accesses `arr` at
|
|
13
|
+
// no other index, never aliases `arr` elsewhere, and contains no call / nested loop / break /
|
|
14
|
+
// continue / return / closure. The `arr[j-1]` read becomes a scalar `left` seeded from `arr[LO-1]`
|
|
15
|
+
// and refreshed after each store; the body is then unrolled ×2 (with a 1-cell tail) so the carry
|
|
16
|
+
// between the paired cells lives in a register and the loop overhead is halved. A `LO <= HI` guard
|
|
17
|
+
// keeps the seed load in step with the original (which reads `arr[LO-1]` only when it iterates),
|
|
18
|
+
// and falls back to the untouched loop on the empty range — sound for any trip count.
|
|
19
|
+
|
|
20
|
+
import { findMutations } from './analyze-scans.js'
|
|
21
|
+
import { litVal, litN, unitIncVar, normalizeLoop, freshLoopId } from './loop-model.js'
|
|
22
|
+
import { rewriteBlocks, closureMutatedVars } from './loop-model.js'
|
|
23
|
+
|
|
24
|
+
const isArr = (n) => Array.isArray(n) // wrap (not alias): the self-host kernel rejects a builtin used as a first-class value
|
|
25
|
+
const clone = (n) => isArr(n) ? n.map(clone) : n
|
|
26
|
+
const isIvMinus1 = (n, iv) => isArr(n) && n[0] === '-' && n[1] === iv && litN(n[2], 1) // (iv - 1)
|
|
27
|
+
|
|
28
|
+
// Ops whose presence makes duplicating the body in place unsound (control that escapes the cell,
|
|
29
|
+
// or a call that could alias/mutate `arr` or reorder side effects).
|
|
30
|
+
const REJECT = new Set(['for', 'while', 'do', 'for-in', 'for-of', 'break', 'continue', 'return',
|
|
31
|
+
'throw', 'switch', 'try', 'catch', 'finally', '=>', 'label'])
|
|
32
|
+
const hasUnsafe = (n) => {
|
|
33
|
+
if (!isArr(n)) return false
|
|
34
|
+
if (REJECT.has(n[0])) return true
|
|
35
|
+
if (n[0] === '()' && typeof n[1] === 'string') return true // function call `f(args)`
|
|
36
|
+
return n.some(hasUnsafe)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Substitute every value-reference of `iv` with (iv + 1); leave the op slot and property keys.
|
|
40
|
+
const subPlus1 = (n, iv) => {
|
|
41
|
+
if (n === iv) return ['+', iv, 1]
|
|
42
|
+
if (!isArr(n)) return n
|
|
43
|
+
if (n[0] === '.' && n.length === 3) return ['.', subPlus1(n[1], iv), n[2]]
|
|
44
|
+
return [n[0], ...n.slice(1).map(c => subPlus1(c, iv))]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Rename every let/const-DECLARED var in `stmts` with a suffix, throughout — so the 2nd unrolled
|
|
48
|
+
// cell's locals don't collide with the 1st. Loop-carried outer vars (assigned, not declared here)
|
|
49
|
+
// are untouched, so the recurrence still threads through them.
|
|
50
|
+
function renameDecls(stmts, suf) {
|
|
51
|
+
const declared = new Set()
|
|
52
|
+
const collect = (n) => {
|
|
53
|
+
if (!isArr(n)) return
|
|
54
|
+
if (n[0] === 'let' || n[0] === 'const')
|
|
55
|
+
for (let k = 1; k < n.length; k++) if (isArr(n[k]) && n[k][0] === '=' && typeof n[k][1] === 'string') declared.add(n[k][1])
|
|
56
|
+
n.forEach(collect)
|
|
57
|
+
}
|
|
58
|
+
stmts.forEach(collect)
|
|
59
|
+
if (!declared.size) return stmts
|
|
60
|
+
const ren = (n) => {
|
|
61
|
+
if (typeof n === 'string') return declared.has(n) ? n + suf : n
|
|
62
|
+
if (!isArr(n)) return n
|
|
63
|
+
if (n[0] === '.' && n.length === 3) return ['.', ren(n[1]), n[2]]
|
|
64
|
+
return [n[0], ...n.slice(1).map(ren)]
|
|
65
|
+
}
|
|
66
|
+
return stmts.map(ren)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Replace `arr[iv-1]` reads with `left`; keep the store; emit `left = storeVal` after each store.
|
|
70
|
+
function scalarReplace(stmts, arr, iv, left, storeVal) {
|
|
71
|
+
const repl = (n) => {
|
|
72
|
+
if (!isArr(n)) return n
|
|
73
|
+
if (n[0] === '[]' && n[1] === arr && isIvMinus1(n[2], iv)) return left
|
|
74
|
+
if (n[0] === '.' && n.length === 3) return ['.', repl(n[1]), n[2]]
|
|
75
|
+
return [n[0], ...n.slice(1).map(repl)]
|
|
76
|
+
}
|
|
77
|
+
const out = []
|
|
78
|
+
for (const s of stmts) {
|
|
79
|
+
out.push(repl(s))
|
|
80
|
+
if (isArr(s) && s[0] === '=' && isArr(s[1]) && s[1][0] === '[]' && s[1][1] === arr && s[1][2] === iv)
|
|
81
|
+
out.push(['=', left, storeVal])
|
|
82
|
+
}
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function tryUnroll(stmt, cm) {
|
|
87
|
+
const L = normalizeLoop(stmt)
|
|
88
|
+
if (!L || L.kind !== 'for') return null
|
|
89
|
+
const body = L.body
|
|
90
|
+
if (!isArr(body) || body[0] !== ';') return null
|
|
91
|
+
const iv = unitIncVar(L.step)
|
|
92
|
+
if (!iv) return null
|
|
93
|
+
|
|
94
|
+
// init `let iv = LO`, LO a literal ≥ 1 (so arr[LO-1] is a valid in-bounds index)
|
|
95
|
+
if (!(isArr(L.init) && L.init[0] === 'let' && isArr(L.init[1]) && L.init[1][0] === '=' && L.init[1][1] === iv)) return null
|
|
96
|
+
const LO = L.init[1][2], loVal = litVal(LO)
|
|
97
|
+
if (loVal == null || loVal < 1) return null
|
|
98
|
+
|
|
99
|
+
// cond `iv <= HI` / `iv < HI`, HI loop-invariant
|
|
100
|
+
if (!(isArr(L.cond) && (L.cond[0] === '<=' || L.cond[0] === '<') && L.cond[1] === iv)) return null
|
|
101
|
+
const cmpOp = L.cond[0], HI = L.cond[2]
|
|
102
|
+
if (!(typeof HI === 'string' || litVal(HI) != null)) return null
|
|
103
|
+
|
|
104
|
+
if (hasUnsafe(body)) return null
|
|
105
|
+
const stmts = body.slice(1)
|
|
106
|
+
|
|
107
|
+
// exactly one store `arr[iv] = <var>` — the recurrence array + carried value
|
|
108
|
+
let arr = null, storeVal = null, nStore = 0
|
|
109
|
+
for (const s of stmts)
|
|
110
|
+
if (isArr(s) && s[0] === '=' && isArr(s[1]) && s[1][0] === '[]' && s[1][2] === iv) {
|
|
111
|
+
if (typeof s[2] !== 'string') return null
|
|
112
|
+
arr = s[1][1]; storeVal = s[2]; nStore++
|
|
113
|
+
}
|
|
114
|
+
if (nStore !== 1 || typeof arr !== 'string') return null
|
|
115
|
+
if (storeVal === iv || storeVal === arr) return null
|
|
116
|
+
|
|
117
|
+
// every `arr[...]` is `arr[iv]` or `arr[iv-1]`, the only write is the store, ≥1 recurrence read,
|
|
118
|
+
// and `arr` never appears bare (passed/aliased)
|
|
119
|
+
let hasRec = false, bad = false
|
|
120
|
+
const scan = (n) => {
|
|
121
|
+
if (!isArr(n)) return
|
|
122
|
+
if (n[0] === '[]' && n[1] === arr) {
|
|
123
|
+
if (n[2] === iv) {} else if (isIvMinus1(n[2], iv)) hasRec = true; else bad = true
|
|
124
|
+
}
|
|
125
|
+
if (n[0] === '=' && isArr(n[1]) && n[1][0] === '[]' && n[1][1] === arr && n[1][2] !== iv) bad = true
|
|
126
|
+
if (!(n[0] === '[]' || n[0] === '.')) for (let k = 1; k < n.length; k++) if (n[k] === arr) bad = true
|
|
127
|
+
n.forEach(scan)
|
|
128
|
+
}
|
|
129
|
+
scan(body)
|
|
130
|
+
if (bad || !hasRec) return null
|
|
131
|
+
|
|
132
|
+
// The carry `left = storeVal` is emitted right after the store, so a recurrence read AFTER the
|
|
133
|
+
// store would see this cell's value, not arr[iv-1]. Require every arr[iv-1] read to precede it.
|
|
134
|
+
const storeIdx = stmts.findIndex(s => isArr(s) && s[0] === '=' && isArr(s[1]) && s[1][0] === '[]' && s[1][1] === arr && s[1][2] === iv)
|
|
135
|
+
const readsRec = (n) => isArr(n) && ((n[0] === '[]' && n[1] === arr && isIvMinus1(n[2], iv)) || n.some(readsRec))
|
|
136
|
+
for (let k = storeIdx + 1; k < stmts.length; k++) if (readsRec(stmts[k])) return null
|
|
137
|
+
|
|
138
|
+
// iv assigned only by the step; iv/arr/HI loop-invariant (not mutated, incl. via a closure call)
|
|
139
|
+
const ivMut = new Set(); findMutations(body, new Set([iv]), ivMut)
|
|
140
|
+
if (ivMut.has(iv)) return null
|
|
141
|
+
if (cm.has(iv) || cm.has(arr)) return null
|
|
142
|
+
if (typeof HI === 'string') { const hiMut = new Set(); findMutations(body, new Set([HI]), hiMut); if (hiMut.has(HI) || cm.has(HI)) return null }
|
|
143
|
+
|
|
144
|
+
// --- transform ---
|
|
145
|
+
const id = freshLoopId()
|
|
146
|
+
const left = `__rec${id}`
|
|
147
|
+
const bodyS = scalarReplace(stmts, arr, iv, left, storeVal)
|
|
148
|
+
const cellJ = () => bodyS.map(clone)
|
|
149
|
+
const cellJ1 = renameDecls(bodyS.map(s => subPlus1(clone(s), iv)), `$r${id}`)
|
|
150
|
+
|
|
151
|
+
const seed = ['let', ['=', left, ['[]', arr, loVal - 1]]] // left = arr[LO-1]
|
|
152
|
+
const letIv = ['let', ['=', iv, clone(LO)]] // let iv = LO
|
|
153
|
+
const twoFit = cmpOp === '<=' ? ['<', iv, clone(HI)] : ['<', iv, ['-', clone(HI), 1]]
|
|
154
|
+
const main = ['while', twoFit,
|
|
155
|
+
[';', ['{}', [';', ...cellJ()]], ['{}', [';', ...cellJ1]], ['=', iv, ['+', iv, 2]]]]
|
|
156
|
+
const tail = ['if', [cmpOp, iv, clone(HI)],
|
|
157
|
+
['{}', [';', ...cellJ(), ['=', iv, ['+', iv, 1]]]]]
|
|
158
|
+
const block = ['{}', [';', letIv, seed, main, tail]]
|
|
159
|
+
// Run the unrolled form only on a non-empty range (so the seed's arr[LO-1] load matches the
|
|
160
|
+
// original, which reads it only when it iterates); otherwise the untouched loop.
|
|
161
|
+
return [['if', [cmpOp, clone(LO), clone(HI)], block, stmt]]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function unrollRecurrence(body) {
|
|
165
|
+
const cm = closureMutatedVars(body)
|
|
166
|
+
return rewriteBlocks(body, stmt => tryUnroll(stmt, cm))
|
|
167
|
+
}
|
package/src/compile/narrow.js
CHANGED
|
@@ -62,12 +62,17 @@ function filterLiveCallSites(callSites, valueUsed) {
|
|
|
62
62
|
|
|
63
63
|
function buildCallerCtx() {
|
|
64
64
|
const callerCtx = new Map()
|
|
65
|
-
|
|
65
|
+
const globalTE = ctx.scope.globalTypedElem || new Map()
|
|
66
|
+
callerCtx.set(null, { callerLocals: ctx.scope.globalTypes, callerValTypes: ctx.scope.globalValTypes, callerTypedElems: globalTE })
|
|
66
67
|
for (const func of ctx.func.list) {
|
|
67
68
|
if (!func.body || func.raw) continue
|
|
68
69
|
const facts = analyzeBody(func.body)
|
|
69
70
|
for (const p of func.sig.params) if (!facts.locals.has(p.name)) facts.locals.set(p.name, p.type)
|
|
70
|
-
|
|
71
|
+
// Shadow-aware local+global typed-array map: a `const buf = new Int32Array(…)`
|
|
72
|
+
// local makes `buf[i]` arg reads type i32 at this caller's sites, so a callee
|
|
73
|
+
// param fed only such elements narrows (else it stays f64 and `1 << p` drags in
|
|
74
|
+
// __to_num → the whole string↔number stdlib). Mirrors callerTypedElemsFor.
|
|
75
|
+
callerCtx.set(func, { callerLocals: facts.locals, callerValTypes: facts.valTypes, callerTypedElems: callerTypedElemsFor(func, globalTE) })
|
|
71
76
|
}
|
|
72
77
|
return callerCtx
|
|
73
78
|
}
|
|
@@ -725,6 +730,7 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
725
730
|
callee, callerFunc, argList, func, restIdx,
|
|
726
731
|
callerLocals: ctxEntry.callerLocals,
|
|
727
732
|
callerValTypes: ctxEntry.callerValTypes,
|
|
733
|
+
callerTypedElems: ctxEntry.callerTypedElems,
|
|
728
734
|
callerParamFacts(key) {
|
|
729
735
|
if (!paramFacts.has(key)) paramFacts.set(key, paramFactsOf(paramReps, callerFunc, key))
|
|
730
736
|
return paramFacts.get(key)
|
|
@@ -847,7 +853,10 @@ export default function narrowSignatures(programFacts, ast) {
|
|
|
847
853
|
if (state.callee === state.callerFunc?.name &&
|
|
848
854
|
isRecurIntExpr(arg, new Set(state.func.sig.params.map(p => p.name)), state.callerLocals)) return 'i32'
|
|
849
855
|
if (!state._teOverlay) {
|
|
850
|
-
|
|
856
|
+
// Caller's typed arrays (locals + non-shadowed globals, precomputed shadow-aware
|
|
857
|
+
// in buildCallerCtx) so a LOCAL `const buf = new Int32Array(…)` makes `buf[i]`
|
|
858
|
+
// type i32 here — not just module globals / typedCtor-narrowed params.
|
|
859
|
+
const m = new Map(state.callerTypedElems || ctx.scope.globalTypedElem || [])
|
|
851
860
|
const pf = state.callerParamFacts('typedCtor')
|
|
852
861
|
if (pf) for (const [name, ctor] of pf) if (ctor != null) m.set(name, ctor)
|
|
853
862
|
state._teOverlay = m
|