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
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Loop induction strength-reduction for `i % w` and `(i / w) | 0`.
|
|
2
|
+
//
|
|
3
|
+
// JS `%` and `/` are float ops; `i % w` with a RUNTIME divisor lowers to f64
|
|
4
|
+
// (i % 0 is NaN, so it can't be soundly typed i32 — see type.js exprType `%`).
|
|
5
|
+
// That makes the column `x = i % w` an f64 local, which cascades: every dependent
|
|
6
|
+
// neighbour index becomes f64 and each typed-array access pays an f64→i32 convert.
|
|
7
|
+
// Measured ~5× slower than V8, which speculates the divisor non-zero and uses i32.
|
|
8
|
+
//
|
|
9
|
+
// This pass replaces the per-iteration division with incremental i32 counters in a
|
|
10
|
+
// unit-stride loop: the column `cx` increments by 1 and wraps to 0 at `w`, bumping
|
|
11
|
+
// the row `cy`. No division survives in the body, so the whole index chain stays
|
|
12
|
+
// i32. Fully sound — counters are pure increment (the divisor-zero question never
|
|
13
|
+
// arises), and if `w == 0` the loop's `i < w*h` guard never admits an iteration, so
|
|
14
|
+
// the one-time seed `(i%w)|0` (NaN→0) is unused.
|
|
15
|
+
//
|
|
16
|
+
// Recognized (post-prepare AST): a `while` whose body increments one IV `i` by +1
|
|
17
|
+
// exactly once, reads `['%', i, w]` and/or `['|', ['/', i, w], LIT0]` with `w`
|
|
18
|
+
// loop-invariant, and has no `continue` / closure-capture of i,w (which could
|
|
19
|
+
// desync the counters). Number literals are sparse-array holes `[, v]` (n[0] is the
|
|
20
|
+
// hole = undefined), so literal tests use `== null`; created literals are bare numbers.
|
|
21
|
+
|
|
22
|
+
import { findMutations } from './analyze-scans.js'
|
|
23
|
+
|
|
24
|
+
const litN = (n, k) => Array.isArray(n) && n.length === 2 && n[0] == null && n[1] === k
|
|
25
|
+
const isMod = (n, i, w) => Array.isArray(n) && n[0] === '%' && n[1] === i && n[2] === w
|
|
26
|
+
const isFloorDiv = (n, i, w) =>
|
|
27
|
+
Array.isArray(n) && n[0] === '|' && litN(n[2], 0) &&
|
|
28
|
+
Array.isArray(n[1]) && n[1][0] === '/' && n[1][1] === i && n[1][2] === w
|
|
29
|
+
|
|
30
|
+
// IV a statement increments by exactly +1, or null. Covers `i++` (post-inc desugars
|
|
31
|
+
// to `(++i) - 1`), `++i`, `i += 1`, `i = i + 1`.
|
|
32
|
+
function incVarOf(stmt) {
|
|
33
|
+
if (!Array.isArray(stmt)) return null
|
|
34
|
+
let inc = stmt
|
|
35
|
+
if (stmt[0] === '-' && litN(stmt[2], 1) && Array.isArray(stmt[1]) && stmt[1][0] === '++') inc = stmt[1]
|
|
36
|
+
if (inc[0] === '++' && typeof inc[1] === 'string') return inc[1]
|
|
37
|
+
if (stmt[0] === '+=' && typeof stmt[1] === 'string' && litN(stmt[2], 1)) return stmt[1]
|
|
38
|
+
if (stmt[0] === '=' && typeof stmt[1] === 'string' && Array.isArray(stmt[2]) && stmt[2][0] === '+') {
|
|
39
|
+
const [, a, b] = stmt[2]
|
|
40
|
+
if (a === stmt[1] && litN(b, 1)) return stmt[1]
|
|
41
|
+
if (b === stmt[1] && litN(a, 1)) return stmt[1]
|
|
42
|
+
}
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const usesPattern = (n, i, w, pred) => Array.isArray(n) && (pred(n, i, w) || n.some(c => usesPattern(c, i, w, pred)))
|
|
47
|
+
const replace = (n, i, w, cx, cy) =>
|
|
48
|
+
!Array.isArray(n) ? n : isMod(n, i, w) ? cx : isFloorDiv(n, i, w) ? cy : n.map(c => replace(c, i, w, cx, cy))
|
|
49
|
+
// a `continue` that targets THIS loop (not one nested inside) — would skip the increment
|
|
50
|
+
const hasOuterContinue = (n) => Array.isArray(n) &&
|
|
51
|
+
(n[0] === 'continue' || (n[0] !== 'while' && n[0] !== 'for' && n[0] !== 'do' && n[0] !== '=>' && n.some(hasOuterContinue)))
|
|
52
|
+
// Vars assigned anywhere inside a closure in the function. Such a var can be
|
|
53
|
+
// mutated by a call in the loop body (the closure may be defined outside the loop,
|
|
54
|
+
// so a body-local findMutations misses it), so it is not safe as the IV or divisor.
|
|
55
|
+
const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '**=', '&=', '|=', '^=', '<<=', '>>=', '>>>=', '&&=', '||=', '??='])
|
|
56
|
+
const collectAssigns = (n, out) => {
|
|
57
|
+
if (!Array.isArray(n)) return
|
|
58
|
+
if (typeof n[1] === 'string' && (ASSIGN_OPS.has(n[0]) || n[0] === '++' || n[0] === '--')) out.add(n[1])
|
|
59
|
+
n.forEach(c => collectAssigns(c, out))
|
|
60
|
+
}
|
|
61
|
+
const closureMutated = (n, out) => {
|
|
62
|
+
if (!Array.isArray(n)) return out
|
|
63
|
+
if (n[0] === '=>') collectAssigns(n, out)
|
|
64
|
+
n.forEach(c => closureMutated(c, out))
|
|
65
|
+
return out
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let _uniq = 0
|
|
69
|
+
let _cm = new Set() // closure-mutated vars for the function currently being transformed
|
|
70
|
+
|
|
71
|
+
// Try to strength-reduce one `while` statement. Returns [seed, loop] or null.
|
|
72
|
+
function tryReduce(stmt) {
|
|
73
|
+
if (!Array.isArray(stmt) || stmt[0] !== 'while') return null
|
|
74
|
+
const cond = stmt[1], lbody = stmt[2]
|
|
75
|
+
if (!Array.isArray(lbody) || lbody[0] !== ';') return null
|
|
76
|
+
|
|
77
|
+
// exactly one IV incremented by +1
|
|
78
|
+
let iv = null, ivIdx = -1
|
|
79
|
+
for (let k = 1; k < lbody.length; k++) {
|
|
80
|
+
const v = incVarOf(lbody[k])
|
|
81
|
+
if (v) { if (iv) return null; iv = v; ivIdx = k }
|
|
82
|
+
}
|
|
83
|
+
if (!iv) return null
|
|
84
|
+
|
|
85
|
+
// a single invariant divisor `w` (`i % w` / `(i/w)|0` all share it)
|
|
86
|
+
let w = null
|
|
87
|
+
const findW = (n) => {
|
|
88
|
+
if (!Array.isArray(n)) return
|
|
89
|
+
const d = n[0] === '%' && n[1] === iv ? n[2]
|
|
90
|
+
: (n[0] === '|' && litN(n[2], 0) && Array.isArray(n[1]) && n[1][0] === '/' && n[1][1] === iv) ? n[1][2] : null
|
|
91
|
+
if (typeof d === 'string') { if (w == null) w = d; else if (w !== d) w = false }
|
|
92
|
+
n.forEach(findW)
|
|
93
|
+
}
|
|
94
|
+
findW(lbody)
|
|
95
|
+
if (!w || w === iv) return null
|
|
96
|
+
|
|
97
|
+
const usesMod = usesPattern(lbody, iv, w, isMod)
|
|
98
|
+
const usesDiv = usesPattern(lbody, iv, w, isFloorDiv)
|
|
99
|
+
if (!usesMod && !usesDiv) return null
|
|
100
|
+
|
|
101
|
+
// soundness: w invariant, iv written ONLY by the increment, no continue/closure capture
|
|
102
|
+
const wMut = new Set(); findMutations(lbody, new Set([w]), wMut)
|
|
103
|
+
if (wMut.has(w)) return null
|
|
104
|
+
const ivMut = new Set()
|
|
105
|
+
findMutations([';', ...lbody.slice(1).filter((_, k) => k !== ivIdx - 1)], new Set([iv]), ivMut)
|
|
106
|
+
if (ivMut.has(iv)) return null
|
|
107
|
+
if (hasOuterContinue(lbody)) return null
|
|
108
|
+
if (_cm.has(iv) || _cm.has(w)) return null // IV/divisor mutable via a closure call
|
|
109
|
+
|
|
110
|
+
const id = _uniq++
|
|
111
|
+
const cx = `__lsrx${id}`, cy = `__lsry${id}`
|
|
112
|
+
// seed (inside the w>0 branch): cx = (i%w)|0, cy = (i/w)|0 — one-time, i32 via |0
|
|
113
|
+
const seedDecls = [['=', cx, ['|', ['%', iv, w], 0]]]
|
|
114
|
+
if (usesDiv) seedDecls.push(['=', cy, ['|', ['/', iv, w], 0]])
|
|
115
|
+
const seed = ['let', ...seedDecls]
|
|
116
|
+
|
|
117
|
+
// transformed body: replace patterns, inject `cx++; if(cx>=w){cx=0; cy++}` after the increment
|
|
118
|
+
const newBody = [';']
|
|
119
|
+
for (let k = 1; k < lbody.length; k++) {
|
|
120
|
+
newBody.push(replace(lbody[k], iv, w, cx, cy))
|
|
121
|
+
if (k === ivIdx) {
|
|
122
|
+
newBody.push(['=', cx, ['+', cx, 1]])
|
|
123
|
+
if (usesDiv) newBody.push(['=', cy, ['+', cy, ['?:', ['>=', cx, w], 1, 0]]])
|
|
124
|
+
newBody.push(['=', cx, ['?:', ['>=', cx, w], 0, cx]])
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const fast = ['while', replace(cond, iv, w, cx, cy), newBody]
|
|
128
|
+
// The counters track i%w only when w>0 AND i>=0: w<=0 gives NaN / a negative-divisor
|
|
129
|
+
// modulo, and i<0 makes i%w negative (JS modulo takes the dividend's sign) — neither
|
|
130
|
+
// follows the 0..w-1 +1-wrap the counters model. A one-time `w>0 && i>=0` guard (i is
|
|
131
|
+
// the IV's entry value; it only increments, so it stays ≥0) keeps the fast i32 path
|
|
132
|
+
// for the universal positive-dimension, forward-counting case and falls back to the
|
|
133
|
+
// unmodified loop otherwise — sound for any w, any start.
|
|
134
|
+
return [['if', ['&&', ['>', w, 0], ['>=', iv, 0]], ['{}', [';', seed, fast]], stmt]]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Walk the body; transform `while` loops inside every block (post-order so a nested
|
|
138
|
+
// loop is reduced before its enclosing one is examined).
|
|
139
|
+
function walk(node) {
|
|
140
|
+
if (!Array.isArray(node)) return node
|
|
141
|
+
const n = node.map(walk)
|
|
142
|
+
if (n[0] !== ';') return n
|
|
143
|
+
const out = [';']
|
|
144
|
+
for (let k = 1; k < n.length; k++) {
|
|
145
|
+
const r = tryReduce(n[k])
|
|
146
|
+
if (r) out.push(...r)
|
|
147
|
+
else out.push(n[k])
|
|
148
|
+
}
|
|
149
|
+
return out
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function strengthReduceLoopDivMod(body) {
|
|
153
|
+
_cm = closureMutated(body, new Set())
|
|
154
|
+
return walk(body)
|
|
155
|
+
}
|
package/src/compile/narrow.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* function `sig` records change.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { ctx, warn } from '../ctx.js'
|
|
10
|
+
import { ctx, warn, err } from '../ctx.js'
|
|
11
11
|
import { isBlockBody, alwaysReturns, hasBareReturn, returnExprs } from '../ast.js'
|
|
12
12
|
import { isLiteralStr, I32_MIN, I32_MAX } from '../ir.js'
|
|
13
13
|
import {
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from '../param-reps.js'
|
|
25
25
|
import {
|
|
26
26
|
inferArrElemSchema, inferArrElemValType,
|
|
27
|
-
inferSchemaId, inferValType, inferTypedCtor,
|
|
27
|
+
inferSchemaId, inferValType, inferTypedCtor, inferParams,
|
|
28
28
|
} from './infer.js'
|
|
29
29
|
|
|
30
30
|
const PTR_ABI_KINDS = new Set([VAL.OBJECT, VAL.SET, VAL.MAP, VAL.BUFFER])
|
|
@@ -156,12 +156,32 @@ function refreshCallerValTypes(callerCtx) {
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// Per-caller typed-elem context: the caller's body-local typed arrays, layered
|
|
160
|
+
// over the module's typed-array globals so a call like `f(globalArr)` resolves
|
|
161
|
+
// `globalArr`'s ctor (inferTypedCtor reads only this map for a bare-name arg).
|
|
162
|
+
// A global is visible UNLESS the caller shadows the name with a param or local
|
|
163
|
+
// of its own — only then could the name denote a non-typed value. Globals are
|
|
164
|
+
// sound to consult: globalTypedElem holds a name only when EVERY assignment to
|
|
165
|
+
// it is the same single typed-array ctor (scope.js invalidates on any conflict),
|
|
166
|
+
// so it can't denote a different kind at the call site.
|
|
167
|
+
function callerTypedElemsFor(func, globalTE) {
|
|
168
|
+
const local = analyzeBody(func.body).typedElems
|
|
169
|
+
if (!globalTE.size) return local
|
|
170
|
+
const shadowed = new Set(analyzeBody(func.body).locals.keys())
|
|
171
|
+
for (const p of func.sig?.params || []) shadowed.add(p.name)
|
|
172
|
+
const merged = new Map()
|
|
173
|
+
for (const [k, v] of globalTE) if (!shadowed.has(k)) merged.set(k, v)
|
|
174
|
+
for (const [k, v] of local) merged.set(k, v) // local typed binding shadows the global
|
|
175
|
+
return merged
|
|
176
|
+
}
|
|
177
|
+
|
|
159
178
|
function buildCallerTypedCtx() {
|
|
160
179
|
const callerTypedCtx = new Map()
|
|
161
|
-
|
|
180
|
+
const globalTE = ctx.scope.globalTypedElem || new Map()
|
|
181
|
+
callerTypedCtx.set(null, globalTE)
|
|
162
182
|
for (const func of ctx.func.list) {
|
|
163
183
|
if (!func.body || func.raw) continue
|
|
164
|
-
callerTypedCtx.set(func,
|
|
184
|
+
callerTypedCtx.set(func, callerTypedElemsFor(func, globalTE))
|
|
165
185
|
}
|
|
166
186
|
return callerTypedCtx
|
|
167
187
|
}
|
|
@@ -1127,6 +1147,87 @@ export function adviseJsstringCarrier(paramReps, valueUsed) {
|
|
|
1127
1147
|
}
|
|
1128
1148
|
}
|
|
1129
1149
|
|
|
1150
|
+
// Two value-kinds CONFLICT when passing one where the other is expected would
|
|
1151
|
+
// rely on a JS boundary coercion jz does not implement (so the result diverges).
|
|
1152
|
+
// NUMBER↔STRING is the canonical pair: `"5" * 2` is 10 in JS but NaN in jz, and a
|
|
1153
|
+
// STRING passed to a numeric param reads its NaN-boxed bits as an f64. ARRAY/OBJECT
|
|
1154
|
+
// vs NUMBER/STRING likewise. We treat the four primitive-ish kinds as mutually
|
|
1155
|
+
// exclusive; BOOL is omitted (it nanboxes to 0/1 and numeric code tolerates it).
|
|
1156
|
+
const STRICT_CONFLICT = {
|
|
1157
|
+
[VAL.NUMBER]: new Set([VAL.STRING, VAL.ARRAY, VAL.OBJECT]),
|
|
1158
|
+
[VAL.STRING]: new Set([VAL.NUMBER, VAL.ARRAY, VAL.OBJECT]),
|
|
1159
|
+
[VAL.ARRAY]: new Set([VAL.NUMBER, VAL.STRING]),
|
|
1160
|
+
[VAL.OBJECT]: new Set([VAL.NUMBER, VAL.STRING]),
|
|
1161
|
+
}
|
|
1162
|
+
const kindName = (v) => ({ [VAL.NUMBER]: 'number', [VAL.STRING]: 'string', [VAL.ARRAY]: 'array', [VAL.OBJECT]: 'object' }[v] || 'value')
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* Strict-mode boundary type check (standalone; runs in both the full-narrow and
|
|
1166
|
+
* skip-narrow plan paths).
|
|
1167
|
+
*
|
|
1168
|
+
* jz infers a param's type from how it's used (`x * 2` → number, `s.charCodeAt`
|
|
1169
|
+
* → string) or from an explicit default (`x = 0` → number). In permissive mode a
|
|
1170
|
+
* caller may pass any type and jz silently computes a divergent result (`"5"*2`
|
|
1171
|
+
* is 10 in JS, NaN here). Strict mode is the canonical subset where that misuse
|
|
1172
|
+
* is a compile error instead — consistent with strict already rejecting `==`,
|
|
1173
|
+
* dynamic dispatch, and `void`.
|
|
1174
|
+
*
|
|
1175
|
+
* Fires ONLY when BOTH sides are statically certain and conflict:
|
|
1176
|
+
* - the callee param has a known kind (its default-value type, or a settled
|
|
1177
|
+
* `val` rep from body-usage / call-site inference), AND
|
|
1178
|
+
* - the argument expression has a known, conflicting kind (a literal or a
|
|
1179
|
+
* locally-typed binding).
|
|
1180
|
+
* An untyped param or untyped arg is never flagged — no false positives on
|
|
1181
|
+
* genuinely polymorphic code.
|
|
1182
|
+
*/
|
|
1183
|
+
export function strictBoundaryTypeCheck(programFacts) {
|
|
1184
|
+
if (!ctx.transform.strict) return
|
|
1185
|
+
const { callSites, paramReps } = programFacts
|
|
1186
|
+
|
|
1187
|
+
// Per-callee body-evidence cache: methodEvidence (.charCodeAt→STRING, .push→
|
|
1188
|
+
// ARRAY) keyed by param name. Computed lazily, once per callee.
|
|
1189
|
+
const bodyEvidence = new Map()
|
|
1190
|
+
const evidenceOf = (func) => {
|
|
1191
|
+
if (!bodyEvidence.has(func.name)) {
|
|
1192
|
+
const names = func.sig.params.map(p => p.name).filter(Boolean)
|
|
1193
|
+
bodyEvidence.set(func.name, func.body ? inferParams(func.body, names) : new Map())
|
|
1194
|
+
}
|
|
1195
|
+
return bodyEvidence.get(func.name)
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// Expected kind of callee param k, from any statically-certain source:
|
|
1199
|
+
// 1. explicit default value type — the source's own declaration (x = 0 → number)
|
|
1200
|
+
// 2. settled call-site/usage rep `val` (paramReps; present after full narrow)
|
|
1201
|
+
// 3. type-exclusive body evidence (methodEvidence; works in skip-narrow path too)
|
|
1202
|
+
// First certain source wins; null when the param is genuinely polymorphic.
|
|
1203
|
+
const paramKind = (func, k) => {
|
|
1204
|
+
const p = func.sig.params[k]
|
|
1205
|
+
if (!p) return null
|
|
1206
|
+
const def = func.defaults?.[p.name]
|
|
1207
|
+
if (def != null) { const dv = valTypeOf(def); if (dv) return dv }
|
|
1208
|
+
const repVal = paramReps?.get(func.name)?.get(k)?.val
|
|
1209
|
+
if (repVal != null) return repVal
|
|
1210
|
+
return evidenceOf(func).get(p.name)?.val ?? null
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
for (const cs of callSites) {
|
|
1214
|
+
const func = ctx.func.map.get(cs.callee)
|
|
1215
|
+
if (!func || func.raw || !func.sig) continue
|
|
1216
|
+
if (func.rest) continue // rest packs args into an array
|
|
1217
|
+
for (let k = 0; k < cs.argList.length && k < func.sig.params.length; k++) {
|
|
1218
|
+
const want = paramKind(func, k)
|
|
1219
|
+
if (want == null) continue
|
|
1220
|
+
const conflicts = STRICT_CONFLICT[want]
|
|
1221
|
+
if (!conflicts) continue
|
|
1222
|
+
const got = valTypeOf(cs.argList[k])
|
|
1223
|
+
if (got != null && conflicts.has(got)) {
|
|
1224
|
+
const pname = func.sig.params[k].name
|
|
1225
|
+
err(`strict mode: ${kindName(want)} parameter '${pname}' of '${cs.callee}' received a ${kindName(got)} argument — jz does not coerce ${kindName(got)}→${kindName(want)} at the call boundary (the result would diverge from JS). Pass a ${kindName(want)}, or { strict: false }.`)
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1130
1231
|
/**
|
|
1131
1232
|
* Phase: bimorphic typed-array param specialization.
|
|
1132
1233
|
*
|
|
@@ -1161,13 +1262,9 @@ export function specializeBimorphicTyped(programFacts) {
|
|
|
1161
1262
|
if (list) list.push(cs); else sitesByCallee.set(cs.callee, [cs])
|
|
1162
1263
|
}
|
|
1163
1264
|
|
|
1164
|
-
// Per-caller typedElem map
|
|
1165
|
-
|
|
1166
|
-
callerTypedCtx
|
|
1167
|
-
for (const func of ctx.func.list) {
|
|
1168
|
-
if (!func.body || func.raw) continue
|
|
1169
|
-
callerTypedCtx.set(func, analyzeBody(func.body).typedElems)
|
|
1170
|
-
}
|
|
1265
|
+
// Per-caller typedElem map: body-local `new TypedArray(N)` bindings layered
|
|
1266
|
+
// over the module's typed globals (shared with buildCallerTypedCtx).
|
|
1267
|
+
const callerTypedCtx = buildCallerTypedCtx()
|
|
1171
1268
|
// Per-caller typed-param map: caller's own params that F/G already narrowed
|
|
1172
1269
|
// (so transitive `sum(arr)` inside a func that took `arr` from above resolves).
|
|
1173
1270
|
const callerTypedParamsCtx = new Map()
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// Edge-clamp peeling for box-filter / stencil loops.
|
|
2
|
+
//
|
|
3
|
+
// A stencil loop reads `arr[clamp(iv + k, 0, BOUND-1)]` for a window of taps k ∈
|
|
4
|
+
// [-r, r]. The clamp guards the array edges, but for the interior iv ∈ [r, BOUND-r)
|
|
5
|
+
// every `iv + k` is already in range, so the clamp is a proven no-op there. Per-tap
|
|
6
|
+
// the branch is cheap-but-not-free, and it blocks the marching-pointer / SIMD lift
|
|
7
|
+
// of the inner accumulation. Measured ~18% of the box-blur pass.
|
|
8
|
+
//
|
|
9
|
+
// Split the loop over `iv` (whose bound is the clamp's BOUND) into three runs —
|
|
10
|
+
// left edge [0, xs), clamp-free interior [xs, xe), right edge [xe, BOUND) — where
|
|
11
|
+
// xs = min(r, BOUND), xe = max(xs, BOUND - r). The interior copy has the clamp `if`
|
|
12
|
+
// dropped (the bare `iv + k` index remains). Bit-exact: for iv ∈ [r, BOUND-r),
|
|
13
|
+
// iv+k ∈ [iv-r, iv+r] ⊆ [0, BOUND-1].
|
|
14
|
+
//
|
|
15
|
+
// Recognized (post-prepare AST): a `while (iv < BOUND)` loop whose body contains a
|
|
16
|
+
// clamp `ci = iv + k; if (ci < 0) ci = 0; else if (ci >= BOUND) ci = BOUND-1` whose
|
|
17
|
+
// BOUND is the SAME var as the loop bound and whose `k` is a tap-loop IV ranging
|
|
18
|
+
// [-r, r] (`k = -r … k <= r`). Both hblur (peel the x-loop) and vblur (peel the
|
|
19
|
+
// y-loop) match. Number literals are sparse-array holes (`n[0]` is undefined), so
|
|
20
|
+
// literal tests use `== null`; created literals are bare numbers.
|
|
21
|
+
|
|
22
|
+
import { findMutations } from './analyze-scans.js'
|
|
23
|
+
|
|
24
|
+
const litN = (n, k) => Array.isArray(n) && n.length === 2 && n[0] == null && n[1] === k
|
|
25
|
+
const isVar = (n) => typeof n === 'string'
|
|
26
|
+
|
|
27
|
+
// `k = -r`: prepared as ['u-', r] (unary minus) or ['-', 0, r].
|
|
28
|
+
const negOf = (n) => Array.isArray(n) && n[0] === 'u-' ? n[1]
|
|
29
|
+
: (Array.isArray(n) && n[0] === '-' && litN(n[1], 0) ? n[2] : null)
|
|
30
|
+
|
|
31
|
+
// Every write to `iv` in `node` is a strictly-positive step (++iv / iv+=1 / iv=iv+1),
|
|
32
|
+
// so iv advances monotonically — required so the three split loops partition [0,bound)
|
|
33
|
+
// and the clamp-free interior never re-runs at an edge index. Any other write → false.
|
|
34
|
+
const ASSIGN = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '>>>=', '**=', '&&=', '||=', '??='])
|
|
35
|
+
function ivMonotonic(node, iv) {
|
|
36
|
+
if (!Array.isArray(node)) return true
|
|
37
|
+
if ((node[0] === '++' || node[0] === '--') && node[1] === iv) return node[0] === '++'
|
|
38
|
+
if (ASSIGN.has(node[0]) && node[1] === iv) {
|
|
39
|
+
if (node[0] === '+=' && litN(node[2], 1)) return true
|
|
40
|
+
if (node[0] === '=' && Array.isArray(node[2]) && node[2][0] === '+'
|
|
41
|
+
&& ((node[2][1] === iv && litN(node[2][2], 1)) || (node[2][2] === iv && litN(node[2][1], 1)))) return true
|
|
42
|
+
return false // any other assignment to iv (=, -=, *=, …) breaks monotonicity
|
|
43
|
+
}
|
|
44
|
+
return node.every(c => ivMonotonic(c, iv))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Vars assigned inside any closure in the function — a call in the loop can mutate
|
|
48
|
+
// them even though findMutations (direct writes only) misses it. iv/bound/r in this
|
|
49
|
+
// set must bail (same class as the loop-SR closure-mutation guard).
|
|
50
|
+
const collectAssigns = (n, out) => {
|
|
51
|
+
if (!Array.isArray(n)) return
|
|
52
|
+
if (typeof n[1] === 'string' && (ASSIGN.has(n[0]) || n[0] === '++' || n[0] === '--')) out.add(n[1])
|
|
53
|
+
n.forEach(c => collectAssigns(c, out))
|
|
54
|
+
}
|
|
55
|
+
const closureMutated = (n, out) => {
|
|
56
|
+
if (!Array.isArray(n)) return out
|
|
57
|
+
if (n[0] === '=>') collectAssigns(n, out)
|
|
58
|
+
n.forEach(c => closureMutated(c, out))
|
|
59
|
+
return out
|
|
60
|
+
}
|
|
61
|
+
let _cm = new Set()
|
|
62
|
+
|
|
63
|
+
// Find, anywhere in `node`, a clamp `if (ci < 0) ci = 0; else if (ci >= B) ci = B-1`
|
|
64
|
+
// over a var `ci` and bound var `B`. Returns { ci, bound } or null (first match).
|
|
65
|
+
function findClamp(node) {
|
|
66
|
+
if (!Array.isArray(node)) return null
|
|
67
|
+
if (node[0] === 'if') {
|
|
68
|
+
const [, cond, then, els] = node
|
|
69
|
+
// outer: if (ci < 0) ci = 0; else <inner>
|
|
70
|
+
if (Array.isArray(cond) && cond[0] === '<' && isVar(cond[1]) && litN(cond[2], 0)
|
|
71
|
+
&& Array.isArray(then) && then[0] === '=' && then[1] === cond[1] && litN(then[2], 0)
|
|
72
|
+
&& Array.isArray(els) && els[0] === 'if') {
|
|
73
|
+
const ci = cond[1], [, c2, t2] = els
|
|
74
|
+
// inner: if (ci >= B) ci = B-1
|
|
75
|
+
if (Array.isArray(c2) && c2[0] === '>=' && c2[1] === ci && isVar(c2[2])
|
|
76
|
+
&& Array.isArray(t2) && t2[0] === '=' && t2[1] === ci
|
|
77
|
+
&& Array.isArray(t2[2]) && t2[2][0] === '-' && t2[2][1] === c2[2] && litN(t2[2][2], 1))
|
|
78
|
+
return { ci, bound: c2[2], node }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const c of node) { const r = findClamp(c); if (r) return r }
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// `ci = iv + k` (or `k + iv`) assignment/decl: returns { iv, tap } given the clamp var.
|
|
86
|
+
function clampSource(node, ci) {
|
|
87
|
+
let found = null
|
|
88
|
+
const visit = (n) => {
|
|
89
|
+
if (found || !Array.isArray(n)) return
|
|
90
|
+
if (n[0] === '=' && n[1] === ci && Array.isArray(n[2]) && n[2][0] === '+') {
|
|
91
|
+
const [, a, b] = n[2]
|
|
92
|
+
if (isVar(a) && isVar(b)) found = { a, b }
|
|
93
|
+
}
|
|
94
|
+
n.forEach(visit)
|
|
95
|
+
}
|
|
96
|
+
visit(node)
|
|
97
|
+
return found
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The tap IV must range EXACTLY [-r, r]: a loop `while (t <= r)` AND `t` seeded to
|
|
101
|
+
// `-r` (same `r`). Returns the radius var `r` only when both match; null otherwise.
|
|
102
|
+
// Asymmetric / wider ranges would make xs=r, xe=bound-r unsound, so they bail.
|
|
103
|
+
function tapRadius(loopBody, tap) {
|
|
104
|
+
let boundR = null, initR = null
|
|
105
|
+
const visit = (n) => {
|
|
106
|
+
if (!Array.isArray(n)) return
|
|
107
|
+
// tap loop bound `k <= r`: while (cond at [1]) or for (cond at [2]).
|
|
108
|
+
const cond = n[0] === 'while' ? n[1] : n[0] === 'for' ? n[2] : null
|
|
109
|
+
if (Array.isArray(cond) && cond[0] === '<=' && cond[1] === tap && isVar(cond[2])) boundR = cond[2]
|
|
110
|
+
// tap init `k = -r` (a bare assignment, or inside the for's `let` init clause).
|
|
111
|
+
if (n[0] === '=' && n[1] === tap) { const neg = negOf(n[2]); if (isVar(neg)) initR = neg }
|
|
112
|
+
n.forEach(visit)
|
|
113
|
+
}
|
|
114
|
+
visit(loopBody)
|
|
115
|
+
return boundR != null && boundR === initR ? boundR : null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Count every write (=, compound-assign, ++/--) to variable `v` in `node`.
|
|
119
|
+
function countWrites(node, v) {
|
|
120
|
+
let n = 0
|
|
121
|
+
const visit = (x) => {
|
|
122
|
+
if (!Array.isArray(x)) return
|
|
123
|
+
if ((x[0] === '++' || x[0] === '--') && x[1] === v) n++
|
|
124
|
+
else if (ASSIGN.has(x[0]) && x[1] === v) n++
|
|
125
|
+
x.forEach(visit)
|
|
126
|
+
}
|
|
127
|
+
visit(node)
|
|
128
|
+
return n
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Count tap-shaped seeds (`tap = -r`) and bounds (`tap <= r`) in `body`. The peel is
|
|
132
|
+
// sound only for a SINGLE tap loop; two loops sharing the tap var make tapRadius pick
|
|
133
|
+
// the wrong (last-seen) radius, so xs=r/xe=bound-r no longer match the clamped loop.
|
|
134
|
+
function tapStructures(body, tap) {
|
|
135
|
+
let seeds = 0, bounds = 0
|
|
136
|
+
const visit = (n) => {
|
|
137
|
+
if (!Array.isArray(n)) return
|
|
138
|
+
const cond = n[0] === 'while' ? n[1] : n[0] === 'for' ? n[2] : null
|
|
139
|
+
if (Array.isArray(cond) && cond[0] === '<=' && cond[1] === tap && isVar(cond[2])) bounds++
|
|
140
|
+
if (n[0] === '=' && n[1] === tap && isVar(negOf(n[2]))) seeds++
|
|
141
|
+
n.forEach(visit)
|
|
142
|
+
}
|
|
143
|
+
visit(body)
|
|
144
|
+
return { seeds, bounds }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// True if `iv` is written anywhere INSIDE a nested loop within `body`. Then iv is not
|
|
148
|
+
// constant across the tap accumulation (e.g. an `iv++` living inside the tap loop, so
|
|
149
|
+
// the real outer step is 2r+1), and the per-outer-iteration peel is unsound.
|
|
150
|
+
function ivWrittenInNestedLoop(body, iv) {
|
|
151
|
+
let found = false
|
|
152
|
+
const visit = (n, inLoop) => {
|
|
153
|
+
if (!Array.isArray(n)) return
|
|
154
|
+
if (inLoop && (((n[0] === '++' || n[0] === '--') && n[1] === iv) || (ASSIGN.has(n[0]) && n[1] === iv))) found = true
|
|
155
|
+
const deeper = inLoop || n[0] === 'while' || n[0] === 'for'
|
|
156
|
+
n.forEach(c => visit(c, deeper))
|
|
157
|
+
}
|
|
158
|
+
visit(body, false)
|
|
159
|
+
return found
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let _uniq = 0
|
|
163
|
+
|
|
164
|
+
// Drop the clamp `if` node from a (cloned) body, leaving the bare `ci = iv + k`.
|
|
165
|
+
const dropClamp = (node, clampNode) =>
|
|
166
|
+
!Array.isArray(node) ? node
|
|
167
|
+
: node === clampNode ? ['{}', [';']] // empty block (the if is a statement)
|
|
168
|
+
: node.map(c => dropClamp(c, clampNode))
|
|
169
|
+
|
|
170
|
+
// A strictly-positive +1 step on `iv` (the for-loop increment): i++, ++i, i+=1, i=i+1.
|
|
171
|
+
const stepIsPosInc = (s, iv) => {
|
|
172
|
+
if (!Array.isArray(s)) return false
|
|
173
|
+
let inc = s
|
|
174
|
+
if (s[0] === '-' && litN(s[2], 1) && Array.isArray(s[1]) && s[1][0] === '++') inc = s[1]
|
|
175
|
+
if (inc[0] === '++' && inc[1] === iv) return true
|
|
176
|
+
if (s[0] === '+=' && s[1] === iv && litN(s[2], 1)) return true
|
|
177
|
+
return s[0] === '=' && s[1] === iv && Array.isArray(s[2]) && s[2][0] === '+'
|
|
178
|
+
&& ((s[2][1] === iv && litN(s[2][2], 1)) || (s[2][2] === iv && litN(s[2][1], 1)))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function tryPeel(stmt) {
|
|
182
|
+
if (!Array.isArray(stmt)) return null
|
|
183
|
+
// Normalize while / for into (iv, bound, body, init, step). For a `while`, the
|
|
184
|
+
// increment is inside the body; for a `for` it is the separate step clause, which
|
|
185
|
+
// we re-append to each split loop's body (converting the for into init + whiles).
|
|
186
|
+
let iv, bound, body, init = null, step = null
|
|
187
|
+
if (stmt[0] === 'while') {
|
|
188
|
+
const cond = stmt[1]
|
|
189
|
+
if (!Array.isArray(cond) || cond[0] !== '<' || !isVar(cond[1])) return null
|
|
190
|
+
iv = cond[1]; bound = cond[2]; body = stmt[2]
|
|
191
|
+
} else if (stmt[0] === 'for') {
|
|
192
|
+
init = stmt[1]; const cond = stmt[2]; step = stmt[3]; body = stmt[4]
|
|
193
|
+
if (!Array.isArray(cond) || cond[0] !== '<' || !isVar(cond[1])) return null
|
|
194
|
+
iv = cond[1]; bound = cond[2]
|
|
195
|
+
if (!stepIsPosInc(step, iv)) return null
|
|
196
|
+
} else return null
|
|
197
|
+
if (!isVar(bound) || !Array.isArray(body)) return null
|
|
198
|
+
// A loop whose body is a single statement (e.g. an outer row loop wrapping one
|
|
199
|
+
// inner loop — the vertical blur pass) isn't a `;` sequence; normalize it.
|
|
200
|
+
if (body[0] !== ';') body = [';', body]
|
|
201
|
+
|
|
202
|
+
const clamp = findClamp(body)
|
|
203
|
+
if (!clamp || clamp.bound !== bound) return null // clamp bound must be this loop's bound
|
|
204
|
+
const src = clampSource(body, clamp.ci)
|
|
205
|
+
if (!src) return null
|
|
206
|
+
// ci = a + b: one operand is the loop IV, the other the tap IV
|
|
207
|
+
const tap = src.a === iv ? src.b : src.b === iv ? src.a : null
|
|
208
|
+
if (!tap) return null
|
|
209
|
+
const r = tapRadius(body, tap)
|
|
210
|
+
if (r == null) return null
|
|
211
|
+
|
|
212
|
+
// The clamp var must be written EXACTLY three times: its `ci = iv+k` source and the
|
|
213
|
+
// two clamp branches (`ci=0`, `ci=bound-1`). Any extra write — `ci = ci-1`, `ci = -ci`
|
|
214
|
+
// between the source and the clamp — changes what value the clamp actually guards, so
|
|
215
|
+
// dropping the clamp in the interior (which assumes the guarded value is iv+k) is
|
|
216
|
+
// unsound. Three is the exact count for a well-formed clamp; more ⇒ a mutation.
|
|
217
|
+
if (countWrites(body, clamp.ci) !== 3) return null
|
|
218
|
+
// Exactly one tap loop (one seed, one bound): two loops sharing the tap var would let
|
|
219
|
+
// tapRadius pick the wrong radius.
|
|
220
|
+
const ts = tapStructures(body, tap)
|
|
221
|
+
if (ts.seeds !== 1 || ts.bounds !== 1) return null
|
|
222
|
+
// iv must be constant across the tap accumulation — not bumped inside the tap loop.
|
|
223
|
+
if (ivWrittenInNestedLoop(body, iv)) return null
|
|
224
|
+
|
|
225
|
+
// Soundness: iv advances monotonically (else the interior re-runs at an edge index),
|
|
226
|
+
// and bound/r are loop-invariant (else the once-computed xs/xe go stale mid-loop).
|
|
227
|
+
if (!ivMonotonic(body, iv)) return null
|
|
228
|
+
const mut = new Set(); findMutations(body, new Set([bound, r]), mut)
|
|
229
|
+
if (mut.has(bound) || mut.has(r)) return null
|
|
230
|
+
if (_cm.has(iv) || _cm.has(bound) || _cm.has(r)) return null // closure-mutable → unsafe
|
|
231
|
+
|
|
232
|
+
const id = _uniq++
|
|
233
|
+
const xs = `__pks${id}`, xe = `__pke${id}`
|
|
234
|
+
// xs = r < bound ? r : bound ; xe = (bound - r) > xs ? bound - r : xs
|
|
235
|
+
const seed = ['let',
|
|
236
|
+
['=', xs, ['?:', ['<', r, bound], r, bound]],
|
|
237
|
+
['=', xe, ['?:', ['>', ['-', bound, r], xs], ['-', bound, r], xs]]]
|
|
238
|
+
const interiorBody = dropClamp(body, clamp.node)
|
|
239
|
+
// for: append the step to each split loop's body; while: body already increments.
|
|
240
|
+
const mk = (B, bod) => ['while', ['<', iv, B], step ? [';', ...bod.slice(1), step] : bod]
|
|
241
|
+
const loops = [mk(xs, body), mk(xe, interiorBody), mk(bound, body)]
|
|
242
|
+
return init ? [init, seed, ...loops] : [seed, ...loops]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function walk(node) {
|
|
246
|
+
if (!Array.isArray(node)) return node
|
|
247
|
+
const n = node.map(walk)
|
|
248
|
+
if (n[0] !== ';') return n
|
|
249
|
+
const out = [';']
|
|
250
|
+
for (let k = 1; k < n.length; k++) {
|
|
251
|
+
const r = tryPeel(n[k])
|
|
252
|
+
if (r) out.push(...r)
|
|
253
|
+
else out.push(n[k])
|
|
254
|
+
}
|
|
255
|
+
return out
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function peelClampedStencil(body) {
|
|
259
|
+
_cm = closureMutated(body, new Set())
|
|
260
|
+
return walk(body)
|
|
261
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ctx, warn } from '../../ctx.js'
|
|
1
|
+
import { ctx, warn, err } from '../../ctx.js'
|
|
2
2
|
import { refsName, REFS_IN_EXPR } from '../../ast.js'
|
|
3
3
|
import { intLiteralValue } from '../../static.js'
|
|
4
4
|
import { VAL } from '../../reps.js'
|
|
@@ -307,10 +307,64 @@ function adviseSimdLoops() {
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
/** Compile-time advisories at end of plan — extensible home for soft warnings. */
|
|
310
|
+
// Generic-dispatch deopt advisory. A module global indexed inside a loop whose type
|
|
311
|
+
// never resolved to a container (its VAL stays null — "any") lowers EVERY `g[i]` to
|
|
312
|
+
// the runtime tag-dispatch path (__typed_idx / string fork): ~13× slower than a proven
|
|
313
|
+
// typed load, and almost always a MISSED type rather than intentional polymorphism — a
|
|
314
|
+
// loop-hot indexed container has one kind in practice (jz's value model already handles
|
|
315
|
+
// genuinely-polymorphic parser data efficiently; that's a different regime). Like TS
|
|
316
|
+
// flagging `any`, surface the bailout so it's fixed at the source — a `new T()`-typed
|
|
317
|
+
// global, or an `instanceof`/`+` guard — instead of silently paying the cliff. Scoped to
|
|
318
|
+
// module globals: their type is final here (params/locals resolve only at emit). Strict
|
|
319
|
+
// mode, which already rejects dynamic features, escalates this to a hard error.
|
|
320
|
+
function adviseGenericDispatch() {
|
|
321
|
+
if (!ctx.warnings && !ctx.transform.strict) return
|
|
322
|
+
const globals = ctx.scope.userGlobals
|
|
323
|
+
if (!globals?.size) return
|
|
324
|
+
const isGeneric = (name) => globals.has(name) && !ctx.scope.globalValTypes?.get(name)
|
|
325
|
+
// A global narrowed by `g instanceof Ctor` / `typeof g` in this function reads
|
|
326
|
+
// through the refined fast path — the user already applied the recommended fix,
|
|
327
|
+
// so flagging it would be noise. (Refinements are emit-time; this AST probe is
|
|
328
|
+
// the sound, conservative suppressor — a guard anywhere in the fn silences it.)
|
|
329
|
+
const guarded = (body) => {
|
|
330
|
+
const set = new Set()
|
|
331
|
+
const scan = (n) => {
|
|
332
|
+
if (!Array.isArray(n)) return
|
|
333
|
+
// Raw forms (strict mode skips jzify, so `instanceof`/`typeof` survive)…
|
|
334
|
+
if ((n[0] === 'instanceof' || n[0] === 'typeof') && typeof n[1] === 'string') set.add(n[1])
|
|
335
|
+
// …and the lowered predicate jzify emits — `g instanceof Float64Array` becomes
|
|
336
|
+
// `__is_typed(g)`, `typeof g === 'string'` becomes `__is_str(g)`, etc.
|
|
337
|
+
else if (n[0] === '()' && typeof n[1] === 'string' && n[1].startsWith('__is') && typeof n[2] === 'string') set.add(n[2])
|
|
338
|
+
for (let i = 1; i < n.length; i++) scan(n[i])
|
|
339
|
+
}
|
|
340
|
+
scan(body)
|
|
341
|
+
return set
|
|
342
|
+
}
|
|
343
|
+
for (const func of ctx.func.list) {
|
|
344
|
+
if (func.raw || !func.body) continue
|
|
345
|
+
const fn = func.name
|
|
346
|
+
const narrowed = guarded(func.body)
|
|
347
|
+
const walk = (node, inLoop) => {
|
|
348
|
+
if (!Array.isArray(node)) return
|
|
349
|
+
const op = node[0]
|
|
350
|
+
if (op === '[]' && inLoop && typeof node[1] === 'string' && isGeneric(node[1]) && !narrowed.has(node[1])) {
|
|
351
|
+
const g = node[1]
|
|
352
|
+
const msg = `'${g}' is indexed (\`${g}[…]\`) in a loop but its type never resolved — every access falls back to runtime dynamic dispatch (~10× slower than a typed load). Give it one provable kind: assign \`${g} = new Float64Array(…)\`, or narrow with \`instanceof\`/\`+\`.`
|
|
353
|
+
if (ctx.transform.strict) err(`strict mode: ${msg} Pass { strict: false } to allow dynamic dispatch.`)
|
|
354
|
+
warn('deopt-generic', msg, { fn }, node.loc)
|
|
355
|
+
}
|
|
356
|
+
const nowLoop = inLoop || HEAP_LOOP_OPS.has(op)
|
|
357
|
+
for (let i = 1; i < node.length; i++) walk(node[i], nowLoop)
|
|
358
|
+
}
|
|
359
|
+
walk(func.body, false)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
310
363
|
export function adviseProgram(programFacts) {
|
|
311
364
|
adviseHeapGrowth()
|
|
312
365
|
adviseSetMapIterationOrder()
|
|
313
366
|
if (programFacts) adviseJsstringCarrier(programFacts.paramReps, programFacts.valueUsed)
|
|
314
367
|
adviseSimdLoops()
|
|
368
|
+
adviseGenericDispatch()
|
|
315
369
|
}
|
|
316
370
|
|