jz 0.5.1 → 0.6.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 +288 -314
- package/bench/README.md +319 -0
- package/bench/bench.svg +112 -0
- package/cli.js +32 -24
- package/index.js +177 -55
- package/interop.js +88 -159
- package/jz.svg +5 -0
- package/jzify/arguments.js +97 -0
- package/jzify/bundler.js +382 -0
- package/jzify/classes.js +328 -0
- package/jzify/hoist-vars.js +177 -0
- package/jzify/index.js +51 -0
- package/jzify/names.js +37 -0
- package/jzify/switch.js +106 -0
- package/jzify/transform.js +349 -0
- package/layout.js +179 -0
- package/module/array.js +322 -153
- package/module/collection.js +603 -145
- package/module/console.js +55 -43
- package/module/core.js +266 -153
- package/module/date.js +15 -3
- package/module/function.js +73 -6
- package/module/index.js +2 -1
- package/module/json.js +226 -61
- package/module/math.js +414 -186
- package/module/number.js +306 -60
- package/module/object.js +448 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +85 -0
- package/module/string.js +586 -220
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +45 -48
- package/package.json +41 -12
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +26 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +661 -0
- package/src/compile/analyze.js +1565 -0
- package/src/compile/emit-assign.js +408 -0
- package/src/compile/emit.js +3201 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +497 -125
- package/src/{infer.js → compile/infer.js} +27 -98
- package/src/{narrow.js → compile/narrow.js} +302 -96
- package/src/compile/plan/advise.js +316 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +118 -0
- package/src/compile/plan/inline.js +679 -0
- package/src/compile/plan/literals.js +984 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +573 -0
- package/src/compile/program-facts.js +404 -0
- package/src/ctx.js +176 -58
- package/src/ir.js +540 -171
- package/src/kind-traits.js +105 -0
- package/src/kind.js +462 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1106 -446
- package/src/optimize/vectorize.js +1874 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +589 -202
- package/src/reps.js +115 -0
- package/src/resolve.js +12 -3
- package/src/static.js +199 -0
- package/src/type.js +647 -0
- package/src/{assemble.js → wat/assemble.js} +86 -48
- package/src/wat/optimize.js +3760 -0
- package/transform.js +21 -0
- package/wasi.js +47 -5
- package/src/analyze.js +0 -3818
- package/src/emit.js +0 -3040
- package/src/jzify.js +0 -1580
- package/src/plan.js +0 -2132
- package/src/vectorize.js +0 -1088
- /package/src/{codegen.js → wat/codegen.js} +0 -0
package/src/type.js
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WASM local typing + typed-array metadata + integer proofs.
|
|
3
|
+
*
|
|
4
|
+
* - exprType: i32 vs f64 for locals/params
|
|
5
|
+
* - typedElemCtor / ternaryCtorOfRhs: detect typed-array ctor from an AST rhs
|
|
6
|
+
* (the pure PTR.TYPED aux codec lives in layout.js)
|
|
7
|
+
* - scanBoundedLoops / inBoundsCharCodeAt: charCodeAt i32 contract proof
|
|
8
|
+
* - loop unroll helpers: smallConstForTripCount, cloneWithSubst, …
|
|
9
|
+
* - intCertainMap / intExprChecker: integer-shaped binding analysis
|
|
10
|
+
*
|
|
11
|
+
* @module type
|
|
12
|
+
*/
|
|
13
|
+
import { isI32, isReassigned } from './ast.js'
|
|
14
|
+
import { ctx } from './ctx.js'
|
|
15
|
+
import { VAL, lookupValType } from './reps.js'
|
|
16
|
+
import { valTypeOf } from './kind.js'
|
|
17
|
+
import { NO_VALUE, staticValue, intLiteralValue } from './static.js'
|
|
18
|
+
import { typedElemAux } from '../layout.js'
|
|
19
|
+
|
|
20
|
+
/** Byte-backed constructors whose `new X()` yields a PTR.TYPED / PTR.BUFFER value:
|
|
21
|
+
* the typed-array views + ArrayBuffer + DataView. Mirrors autoload's TYPED_CTORS
|
|
22
|
+
* (kept local to avoid a type↔module import cycle). Every other ctor — Map, Set,
|
|
23
|
+
* Date, Array, RegExp, user classes — has its own VAL kind via CALLEE_VAL and must
|
|
24
|
+
* NOT be mistaken for a typed-array construction (else its global misdispatches as
|
|
25
|
+
* a TypedArray, e.g. `map.set(k,v)` lowering to `arr.set(src,offset)`). */
|
|
26
|
+
const TYPED_FAMILY_CTORS = new Set([
|
|
27
|
+
'Int8Array', 'Uint8Array', 'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array',
|
|
28
|
+
'Float32Array', 'Float64Array', 'BigInt64Array', 'BigUint64Array', 'ArrayBuffer', 'DataView',
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
/** Extract typed-array ctor name ('new.Float32Array', 'new.Int8Array.view', etc) from RHS,
|
|
32
|
+
* or null if RHS isn't a typed-array/ArrayBuffer/DataView constructor. */
|
|
33
|
+
export function typedElemCtor(rhs) {
|
|
34
|
+
if (!Array.isArray(rhs) || rhs[0] !== '()' || typeof rhs[1] !== 'string' || !rhs[1].startsWith('new.')) return null
|
|
35
|
+
if (!TYPED_FAMILY_CTORS.has(rhs[1].slice(4))) return null
|
|
36
|
+
const args = rhs[2]
|
|
37
|
+
const isView = rhs[1].endsWith('Array') && rhs[1] !== 'new.ArrayBuffer'
|
|
38
|
+
&& Array.isArray(args) && args[0] === ',' && args.length >= 4
|
|
39
|
+
return isView ? rhs[1] + '.view' : rhs[1]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Sentinel returned by `ternaryCtorOfRhs` when ternary branches resolve to
|
|
43
|
+
* different typed-array ctors — caller should drop any cached entry rather
|
|
44
|
+
* than leave a stale ctor (which would lock the wrong store width). */
|
|
45
|
+
export const MIXED_CTORS = Symbol('MIXED_CTORS')
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
/** A `?:`/`&&`/`||` expression — value depends on a condition, so its ctor
|
|
49
|
+
* must be derived by walking branches (handled by `ternaryCtorOfRhs`). */
|
|
50
|
+
export const isCondExpr = e => Array.isArray(e) && (e[0] === '?:' || e[0] === '&&' || e[0] === '||')
|
|
51
|
+
|
|
52
|
+
/** Walk a `?:`/`&&`/`||` expression and return:
|
|
53
|
+
* - a single ctor string when every branch resolves to the same ctor,
|
|
54
|
+
* - MIXED_CTORS when branches resolve to different ctors,
|
|
55
|
+
* - null when no branch resolves (caller's behavior unchanged).
|
|
56
|
+
*
|
|
57
|
+
* `resolveName(name)` (optional) maps a *variable-name* branch to its known
|
|
58
|
+
* typed-array ctor — without it a branch like `cond ? bufA : bufB` (two typed
|
|
59
|
+
* bindings rather than two `new` literals) resolves to null and the binding
|
|
60
|
+
* falls back to the dynamic `$__typed_idx` read path. The classic ping-pong
|
|
61
|
+
* `let cur = flip ? a : b; cur[i]` needs this to keep fast typed loads. */
|
|
62
|
+
export function ternaryCtorOfRhs(rhs, resolveName) {
|
|
63
|
+
if (typeof rhs === 'string') return resolveName?.(rhs) ?? null
|
|
64
|
+
if (!Array.isArray(rhs)) return null
|
|
65
|
+
const op = rhs[0]
|
|
66
|
+
const lo = op === '?:' ? 2 : (op === '&&' || op === '||') ? 1 : 0
|
|
67
|
+
if (!lo) return null
|
|
68
|
+
const a = ternaryCtorOfRhs(rhs[lo], resolveName) ?? typedElemCtor(rhs[lo])
|
|
69
|
+
const b = ternaryCtorOfRhs(rhs[lo + 1], resolveName) ?? typedElemCtor(rhs[lo + 1])
|
|
70
|
+
return a && b ? (a === b ? a : MIXED_CTORS) : (a || b || null)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// =============================================================================
|
|
74
|
+
// charCodeAt in-bounds proof
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// `String.prototype.charCodeAt` returns NaN for an out-of-range index, so the
|
|
77
|
+
// generic codegen contract is an f64 result (see module/string.js). When the
|
|
78
|
+
// index is the induction variable of a `for (let i = C; i < recv.length; i++)`
|
|
79
|
+
// loop, every `recv.charCodeAt(i)` in the loop body is statically inside
|
|
80
|
+
// `[0, recv.length)` — OOB is impossible — so the call may use the cheaper i32
|
|
81
|
+
// (raw-byte) contract instead. This is a static guarantee, not a guess.
|
|
82
|
+
|
|
83
|
+
/** Step expression of a `for` that increments `name` by exactly 1. */
|
|
84
|
+
export function isUnitIncrement(step, name) {
|
|
85
|
+
if (!Array.isArray(step)) return false
|
|
86
|
+
if (step[0] === '++' && step[1] === name) return true
|
|
87
|
+
// postfix `i++` in value position lowers to `(++i) - 1`
|
|
88
|
+
if (step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++'
|
|
89
|
+
&& step[1][1] === name && intLiteralValue(step[2]) === 1) return true
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `let`/`const` re-declaration of `name` within `node` — does not cross `=>`
|
|
94
|
+
* (a closure has its own scope; collection already stops at closure boundaries). */
|
|
95
|
+
function redeclaresName(node, name) {
|
|
96
|
+
if (!Array.isArray(node) || node[0] === '=>') return false
|
|
97
|
+
if (node[0] === 'let' || node[0] === 'const') {
|
|
98
|
+
for (let k = 1; k < node.length; k++) {
|
|
99
|
+
const d = node[k]
|
|
100
|
+
if (d === name) return true
|
|
101
|
+
if (Array.isArray(d) && d[0] === '=' && d[1] === name) return true
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (let k = 1; k < node.length; k++) if (redeclaresName(node[k], name)) return true
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Collect `recv.charCodeAt(idxVar)` callee nodes within `node`. Stops at `=>`:
|
|
109
|
+
* a closure may run after the loop, when `idxVar` has reached `recv.length`. */
|
|
110
|
+
function collectBoundedCC(node, recv, idxVar, set) {
|
|
111
|
+
if (!Array.isArray(node) || node[0] === '=>') return
|
|
112
|
+
if (node[0] === '()' && node.length === 3 && node[2] === idxVar
|
|
113
|
+
&& Array.isArray(node[1]) && node[1][0] === '.'
|
|
114
|
+
&& node[1][1] === recv && node[1][2] === 'charCodeAt')
|
|
115
|
+
set.add(node[1])
|
|
116
|
+
for (let k = 1; k < node.length; k++) collectBoundedCC(node[k], recv, idxVar, set)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Receiver of a `.length` expression, possibly wrapped in `(… | 0)` — the
|
|
120
|
+
* shape `prepare` produces when it hoists a for-cond bound. */
|
|
121
|
+
function lengthRecv(expr) {
|
|
122
|
+
if (Array.isArray(expr) && expr[0] === '|' && intLiteralValue(expr[2]) === 0) expr = expr[1]
|
|
123
|
+
if (Array.isArray(expr) && expr[0] === '.' && expr[2] === 'length'
|
|
124
|
+
&& typeof expr[1] === 'string') return expr[1]
|
|
125
|
+
// `Math.min(X, recv.length)` (either arg order): min ≤ recv.length regardless
|
|
126
|
+
// of X, so the bound proof carries through. This is the shape
|
|
127
|
+
// splitCharScanLoops plants for the in-bounds main loop of a split scan.
|
|
128
|
+
if (Array.isArray(expr) && expr[0] === '()' && expr[1] === 'math.min') {
|
|
129
|
+
const argsNode = expr[2]
|
|
130
|
+
const args = Array.isArray(argsNode) && argsNode[0] === ',' ? argsNode.slice(1) : [argsNode]
|
|
131
|
+
for (const a of args) { const r = lengthRecv(a); if (r) return r }
|
|
132
|
+
}
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Flatten `let`/`const` declarations (incl. `;`-joined groups) into `out`,
|
|
137
|
+
* mapping each declared name to its initializer expression. */
|
|
138
|
+
function collectDecls(node, out) {
|
|
139
|
+
if (!Array.isArray(node)) return
|
|
140
|
+
if (node[0] === ';') { for (let k = 1; k < node.length; k++) collectDecls(node[k], out); return }
|
|
141
|
+
if (node[0] === 'let' || node[0] === 'const') {
|
|
142
|
+
for (let k = 1; k < node.length; k++) {
|
|
143
|
+
const d = node[k]
|
|
144
|
+
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') out.set(d[1], d[2])
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Walk `node`, recording in `set` the `charCodeAt` callee nodes proven in-bounds
|
|
150
|
+
* by an enclosing canonical induction loop `for (let i = C; i < recv.length; i++)`.
|
|
151
|
+
* Matches the post-`prepare` shape, where the `.length` bound is hoisted into a
|
|
152
|
+
* temp (`cond` becomes `i < lenTmp`, `lenTmp` declared in `init`). */
|
|
153
|
+
export function scanBoundedLoops(node, set) {
|
|
154
|
+
if (!Array.isArray(node)) return
|
|
155
|
+
if (node[0] === 'for' && node.length === 5) {
|
|
156
|
+
const [, init, cond, step, body] = node
|
|
157
|
+
let idx = null, recv = null, boundVar = null
|
|
158
|
+
if (Array.isArray(cond) && cond[0] === '<' && typeof cond[1] === 'string') {
|
|
159
|
+
const decls = new Map()
|
|
160
|
+
collectDecls(init, decls)
|
|
161
|
+
idx = cond[1]
|
|
162
|
+
// index must be declared in `init` as `let i = C`, C an integer literal ≥ 0
|
|
163
|
+
const start = decls.has(idx) ? intLiteralValue(decls.get(idx)) : null
|
|
164
|
+
if (start == null || start < 0) idx = null
|
|
165
|
+
// bound is `recv.length`, directly or via a hoisted temp declared in `init`
|
|
166
|
+
let bound = cond[2]
|
|
167
|
+
if (typeof bound === 'string') { boundVar = bound; bound = decls.get(bound) }
|
|
168
|
+
recv = lengthRecv(bound)
|
|
169
|
+
}
|
|
170
|
+
// step `i++`; body never writes `i`/`recv`/the bound temp (incl. via
|
|
171
|
+
// closures) and never re-declares `i`. Then every bare `i` in the body
|
|
172
|
+
// satisfies `0 ≤ C ≤ i < recv.length`.
|
|
173
|
+
if (idx && recv && idx !== recv && isUnitIncrement(step, idx)
|
|
174
|
+
&& !isReassigned(body, idx) && !isReassigned(body, recv)
|
|
175
|
+
&& (boundVar == null || !isReassigned(body, boundVar))
|
|
176
|
+
&& !redeclaresName(body, idx))
|
|
177
|
+
collectBoundedCC(body, recv, idx, set)
|
|
178
|
+
}
|
|
179
|
+
for (let k = 1; k < node.length; k++) scanBoundedLoops(node[k], set)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const NO_BOUNDED_CC = new Set() // shared immutable empty result
|
|
183
|
+
|
|
184
|
+
/** Set of `['.', recv, 'charCodeAt']` callee nodes in the current function whose
|
|
185
|
+
* index argument is provably within `[0, recv.length)`. Memoised per body. */
|
|
186
|
+
export function inBoundsCharCodeAt(ctx) {
|
|
187
|
+
const body = ctx.func?.body
|
|
188
|
+
if (!Array.isArray(body)) return NO_BOUNDED_CC
|
|
189
|
+
if (ctx.func._ccBody === body) return ctx.func.ccInBounds
|
|
190
|
+
const set = new Set()
|
|
191
|
+
scanBoundedLoops(body, set)
|
|
192
|
+
ctx.func.ccInBounds = set
|
|
193
|
+
ctx.func._ccBody = body
|
|
194
|
+
return set
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Collect proven-in-bounds `recv[idxVar]` accesses within a canonical induction
|
|
198
|
+
* loop. Stores `"recv\x00idxVar"` keys — `\x00` isn't a valid identifier char so
|
|
199
|
+
* the pair is unambiguous. Stops at `=>` (a closure may run after the loop, when
|
|
200
|
+
* `idxVar` has reached `recv.length`). */
|
|
201
|
+
function collectBoundedArrIdx(node, recv, idxVar, set) {
|
|
202
|
+
if (!Array.isArray(node) || node[0] === '=>') return
|
|
203
|
+
if (node[0] === '[]' && node.length === 3 && node[1] === recv && node[2] === idxVar)
|
|
204
|
+
set.add(recv + '\x00' + idxVar)
|
|
205
|
+
for (let k = 1; k < node.length; k++) collectBoundedArrIdx(node[k], recv, idxVar, set)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Walk `node`, recording `"recv\x00idx"` pairs for `recv[idx]` reads proven within
|
|
209
|
+
* `[0, recv.length)` by an enclosing canonical loop `for (let i = C; i < recv.length;
|
|
210
|
+
* i++)`. Same loop contract as `scanBoundedLoops` (charCodeAt) — sibling proof for
|
|
211
|
+
* the ARRAY indexed-read fast path in `module/array.js`. */
|
|
212
|
+
export function scanBoundedArrIdx(node, set) {
|
|
213
|
+
if (!Array.isArray(node)) return
|
|
214
|
+
if (node[0] === 'for' && node.length === 5) {
|
|
215
|
+
const [, init, cond, step, body] = node
|
|
216
|
+
let idx = null, recv = null, boundVar = null
|
|
217
|
+
if (Array.isArray(cond) && cond[0] === '<' && typeof cond[1] === 'string') {
|
|
218
|
+
const decls = new Map()
|
|
219
|
+
collectDecls(init, decls)
|
|
220
|
+
idx = cond[1]
|
|
221
|
+
const start = decls.has(idx) ? intLiteralValue(decls.get(idx)) : null
|
|
222
|
+
if (start == null || start < 0) idx = null
|
|
223
|
+
let bound = cond[2]
|
|
224
|
+
if (typeof bound === 'string') { boundVar = bound; bound = decls.get(bound) }
|
|
225
|
+
recv = lengthRecv(bound)
|
|
226
|
+
}
|
|
227
|
+
if (idx && recv && idx !== recv && isUnitIncrement(step, idx)
|
|
228
|
+
&& !isReassigned(body, idx) && !isReassigned(body, recv)
|
|
229
|
+
&& (boundVar == null || !isReassigned(body, boundVar))
|
|
230
|
+
&& !redeclaresName(body, idx))
|
|
231
|
+
collectBoundedArrIdx(body, recv, idx, set)
|
|
232
|
+
}
|
|
233
|
+
for (let k = 1; k < node.length; k++) scanBoundedArrIdx(node[k], set)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Set of `"recv\x00idx"` keys for `recv[idx]` reads in the current function proven
|
|
237
|
+
* in-bounds. Memoised per body (separate slot from the charCodeAt proof). */
|
|
238
|
+
export function inBoundsArrIdx(ctx) {
|
|
239
|
+
const body = ctx.func?.body
|
|
240
|
+
if (!Array.isArray(body)) return NO_BOUNDED_CC
|
|
241
|
+
if (ctx.func._aiBody === body) return ctx.func.aiInBounds
|
|
242
|
+
const set = new Set()
|
|
243
|
+
scanBoundedArrIdx(body, set)
|
|
244
|
+
ctx.func.aiInBounds = set
|
|
245
|
+
ctx.func._aiBody = body
|
|
246
|
+
return set
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// === Loop unroll / AST transforms (emit + plan) ===
|
|
250
|
+
|
|
251
|
+
export const MAX_SMALL_FOR_UNROLL = 8
|
|
252
|
+
export const MAX_NESTED_FOR_UNROLL = 64
|
|
253
|
+
|
|
254
|
+
export function containsNestedClosure(body) {
|
|
255
|
+
if (!Array.isArray(body)) return false
|
|
256
|
+
if (body[0] === '=>') return true
|
|
257
|
+
for (let i = 1; i < body.length; i++) if (containsNestedClosure(body[i])) return true
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function containsNestedLoop(body) {
|
|
262
|
+
if (!Array.isArray(body)) return false
|
|
263
|
+
const op = body[0]
|
|
264
|
+
if (op === 'for' || op === 'while' || op === 'do') return true
|
|
265
|
+
if (op === '=>') return false
|
|
266
|
+
for (let i = 1; i < body.length; i++) if (containsNestedLoop(body[i])) return true
|
|
267
|
+
return false
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function nestedSmallLoopBudget(body) {
|
|
271
|
+
if (!Array.isArray(body)) return 1
|
|
272
|
+
if (body[0] === '=>') return 1
|
|
273
|
+
if (body[0] === 'for') {
|
|
274
|
+
const [, init, cond, step, loopBody] = body
|
|
275
|
+
const n = smallConstForTripCount(init, cond, step)
|
|
276
|
+
return n == null ? MAX_NESTED_FOR_UNROLL + 1 : n * nestedSmallLoopBudget(loopBody)
|
|
277
|
+
}
|
|
278
|
+
let max = 1
|
|
279
|
+
for (let i = 1; i < body.length; i++) max = Math.max(max, nestedSmallLoopBudget(body[i]))
|
|
280
|
+
return max
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function containsDeclOf(body, name) {
|
|
284
|
+
if (!Array.isArray(body)) return false
|
|
285
|
+
const op = body[0]
|
|
286
|
+
if (op === '=>') return false
|
|
287
|
+
if (op === 'let' || op === 'const') {
|
|
288
|
+
for (let i = 1; i < body.length; i++) {
|
|
289
|
+
const d = body[i]
|
|
290
|
+
if (d === name) return true
|
|
291
|
+
if (Array.isArray(d) && d[0] === '=' && d[1] === name) return true
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
for (let i = 1; i < body.length; i++) if (containsDeclOf(body[i], name)) return true
|
|
295
|
+
return false
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Clone AST with substitutions/renames. Skips into `=>` bodies. */
|
|
299
|
+
export function cloneWithSubst(node, subst, rename = null) {
|
|
300
|
+
if (!(subst instanceof Map)) {
|
|
301
|
+
const name = subst, value = rename
|
|
302
|
+
if (node === name) return [null, value]
|
|
303
|
+
if (!Array.isArray(node)) return node
|
|
304
|
+
if (node[0] === '=>') return node
|
|
305
|
+
return node.map(x => cloneWithSubst(x, name, value))
|
|
306
|
+
}
|
|
307
|
+
const ren = rename instanceof Map ? rename : new Map()
|
|
308
|
+
if (typeof node === 'string') {
|
|
309
|
+
if (subst.has(node)) return clonePlain(subst.get(node))
|
|
310
|
+
return ren.get(node) || node
|
|
311
|
+
}
|
|
312
|
+
if (!Array.isArray(node)) return node
|
|
313
|
+
const op = node[0]
|
|
314
|
+
if (op === 'str') return node.slice()
|
|
315
|
+
if (op === '=>') return node
|
|
316
|
+
if (op === '.' || op === '?.') return [op, cloneWithSubst(node[1], subst, ren), node[2]]
|
|
317
|
+
if (op === ':') return [op, node[1], cloneWithSubst(node[2], subst, ren)]
|
|
318
|
+
return node.map((part, i) => i === 0 ? part : cloneWithSubst(part, subst, ren))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const clonePlain = node => Array.isArray(node) ? node.map(clonePlain) : node
|
|
322
|
+
|
|
323
|
+
export function containsKnownTypedArrayIndex(body) {
|
|
324
|
+
if (!Array.isArray(body)) return false
|
|
325
|
+
if (body[0] === '=>') return false
|
|
326
|
+
if (body[0] === '[]' && typeof body[1] === 'string' && ctx.types.typedElem?.has(body[1])) return true
|
|
327
|
+
for (let i = 1; i < body.length; i++) if (containsKnownTypedArrayIndex(body[i])) return true
|
|
328
|
+
return false
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Trip count for `for (let i=0; i<N; i++)` when structurally obvious, else null. */
|
|
332
|
+
export function smallConstForTripCount(init, cond, step, maxEnd = MAX_SMALL_FOR_UNROLL) {
|
|
333
|
+
if (!Array.isArray(init) || init[0] !== 'let' || init.length !== 2) return null
|
|
334
|
+
const decl = init[1]
|
|
335
|
+
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') return null
|
|
336
|
+
const name = decl[1]
|
|
337
|
+
const start = intLiteralValue(decl[2])
|
|
338
|
+
if (start !== 0) return null
|
|
339
|
+
if (!Array.isArray(cond) || cond[0] !== '<' || cond[1] !== name) return null
|
|
340
|
+
const end = intLiteralValue(cond[2])
|
|
341
|
+
if (end == null || end < 0 || end > maxEnd) return null
|
|
342
|
+
const stepOk = Array.isArray(step) && (
|
|
343
|
+
(step[0] === '++' && step[1] === name) ||
|
|
344
|
+
(step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++' && step[1][1] === name && intLiteralValue(step[2]) === 1)
|
|
345
|
+
)
|
|
346
|
+
return stepOk ? end : null
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Does `body` always exit via return/throw/break/continue? */
|
|
350
|
+
export function isTerminator(body) {
|
|
351
|
+
if (!Array.isArray(body)) return false
|
|
352
|
+
const op = body[0]
|
|
353
|
+
if (op === 'return' || op === 'throw' || op === 'break' || op === 'continue') return true
|
|
354
|
+
if (op === '{}' || op === ';') {
|
|
355
|
+
for (let i = body.length - 1; i >= 1; i--) {
|
|
356
|
+
const s = body[i]
|
|
357
|
+
if (s == null) continue
|
|
358
|
+
return isTerminator(s)
|
|
359
|
+
}
|
|
360
|
+
return false
|
|
361
|
+
}
|
|
362
|
+
return false
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const isUnsignedI32Expr = (e) => Array.isArray(e) && (
|
|
366
|
+
e[0] === '>>>' ||
|
|
367
|
+
(e[0] === '()' && typeof e[1] === 'string' && ctx.func.map?.get(e[1])?.sig?.unsignedResult === true)
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Infer expression result type from AST (without emitting).
|
|
372
|
+
* Used to determine local variable types before compilation.
|
|
373
|
+
* Looks up `locals` first, then current-function params (for i32-specialized params).
|
|
374
|
+
*/
|
|
375
|
+
export function exprType(expr, locals) {
|
|
376
|
+
if (expr == null) return 'f64'
|
|
377
|
+
if (typeof expr === 'number')
|
|
378
|
+
return isI32(expr) ? 'i32' : 'f64'
|
|
379
|
+
if (typeof expr === 'string') {
|
|
380
|
+
if (locals?.has?.(expr)) return locals.get(expr)
|
|
381
|
+
const paramType = ctx.func.current?.params?.find(p => p.name === expr)?.type
|
|
382
|
+
if (paramType) return paramType
|
|
383
|
+
// Module-level numeric consts (top-level `const N = 128`) are emitted as
|
|
384
|
+
// wasm globals with a known wasm type. Without this lookup, references to
|
|
385
|
+
// them inside functions fall back to f64, widening counters bounded by the
|
|
386
|
+
// const (`for (let r = 0; r < N_ROUNDS; r++)`) to f64 via the comparison
|
|
387
|
+
// pass. Only propagate primitive numeric kinds — i64 globals are reserved
|
|
388
|
+
// for the NaN-box carrier ABI and shouldn't influence local typing.
|
|
389
|
+
const gt = ctx.scope?.globalTypes?.get?.(expr)
|
|
390
|
+
if (gt === 'i32' || gt === 'f64') return gt
|
|
391
|
+
return 'f64'
|
|
392
|
+
}
|
|
393
|
+
if (!Array.isArray(expr)) return 'f64'
|
|
394
|
+
|
|
395
|
+
const [op, ...args] = expr
|
|
396
|
+
if (op == null) return exprType(args[0], locals) // literal [, value]
|
|
397
|
+
|
|
398
|
+
// Statically evaluable to -0 (e.g. -1 * 0) — i32 would lose the sign.
|
|
399
|
+
const sv = staticValue(expr)
|
|
400
|
+
if (sv !== NO_VALUE && typeof sv === 'number' && Object.is(sv, -0)) return 'f64'
|
|
401
|
+
|
|
402
|
+
// Always f64
|
|
403
|
+
if (op === '/' || op === '**' || op === '[' || op === '{}' || op === 'str') return 'f64'
|
|
404
|
+
// arr[i] — typed integer arrays return i32. Only Int8/Uint8/Int16/Uint16/Int32
|
|
405
|
+
// (every value fits in signed i32). Skip Uint32: 0..2^32-1 overflows signed.
|
|
406
|
+
// During analyzeBody the in-progress typedElems is in localTypedElemsOverlay;
|
|
407
|
+
// post-analyze passes read from ctx.types.typedElem.
|
|
408
|
+
if (op === '[]') {
|
|
409
|
+
if (typeof args[0] === 'string') {
|
|
410
|
+
const ctor = ctx.func.localTypedElemsOverlay?.get(args[0]) ?? ctx.types.typedElem?.get(args[0])
|
|
411
|
+
if (ctor) {
|
|
412
|
+
const aux = typedElemAux(ctor)
|
|
413
|
+
if (aux != null && (aux & 7) <= 4) return 'i32'
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return 'f64'
|
|
417
|
+
}
|
|
418
|
+
// `.length` on a known sized receiver returns i32 directly (__len/__str_byteLen
|
|
419
|
+
// both return i32). Letting it stay i32 lets analyzeBody keep the counter
|
|
420
|
+
// local i32 too, eliminating the per-iteration `f64.convert_i32_s` widen and
|
|
421
|
+
// the matching `i32.trunc_sat_f64_s` truncs at every `arr[i]` / `i*k` site.
|
|
422
|
+
// Only safe when receiver type is statically known to expose an integer length.
|
|
423
|
+
if (op === '.') {
|
|
424
|
+
if (args[1] === 'length' && typeof args[0] === 'string') {
|
|
425
|
+
const vt = lookupValType(args[0])
|
|
426
|
+
if (vt === VAL.TYPED || vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.BUFFER) return 'i32'
|
|
427
|
+
}
|
|
428
|
+
if (args[1] === 'size' && typeof args[0] === 'string') {
|
|
429
|
+
const vt = lookupValType(args[0])
|
|
430
|
+
if (vt === VAL.SET || vt === VAL.MAP) return 'i32'
|
|
431
|
+
}
|
|
432
|
+
if (args[1] === 'byteLength' && typeof args[0] === 'string') {
|
|
433
|
+
const vt = lookupValType(args[0])
|
|
434
|
+
if (vt === VAL.BUFFER || vt === VAL.TYPED) return 'i32'
|
|
435
|
+
}
|
|
436
|
+
return 'f64'
|
|
437
|
+
}
|
|
438
|
+
// Comparisons, logical-not, and unsigned shift always yield an i32 — a boolean,
|
|
439
|
+
// or a ToUint32 result. True even on BigInt operands (`>>>` throws on bigint, so
|
|
440
|
+
// it never reaches here with one).
|
|
441
|
+
if (['>', '<', '>=', '<=', '==', '!=', '!', '>>>'].includes(op)) return 'i32'
|
|
442
|
+
// Bitwise & signed-shift: i32 on numbers, but f64 when operands are BigInt — the
|
|
443
|
+
// result is a bigint carried in the i64-bits-as-f64 ABI, not a 32-bit int.
|
|
444
|
+
if (['&', '|', '^', '~', '<<', '>>'].includes(op))
|
|
445
|
+
return valTypeOf(expr) === VAL.BIGINT ? 'f64' : 'i32'
|
|
446
|
+
// Preserve i32 if both operands i32
|
|
447
|
+
if (op === '+' || op === '-') {
|
|
448
|
+
const ta = exprType(args[0], locals)
|
|
449
|
+
const tb = args[1] != null ? exprType(args[1], locals) : ta // unary: inherit
|
|
450
|
+
if (ta !== 'i32' || tb !== 'i32') return 'f64'
|
|
451
|
+
// A uint32 operand ([0, 2^32)) makes the result exceed signed i32 range, so
|
|
452
|
+
// emit widens to f64 (see emit.js `+`/`-`). exprType must agree — else
|
|
453
|
+
// narrowing the result back to i32 would trunc_sat-saturate the f64 to INT32_MAX.
|
|
454
|
+
if (isUnsignedI32Expr(args[0]) || (args[1] != null && isUnsignedI32Expr(args[1]))) return 'f64'
|
|
455
|
+
return 'i32'
|
|
456
|
+
}
|
|
457
|
+
// `%` is i32 only when emit takes the i32.rem_s path: both operands i32, neither
|
|
458
|
+
// unsigned, AND the divisor is a nonzero integer constant. A 0 or runtime divisor
|
|
459
|
+
// yields NaN via f64rem (f64), so result-narrowing must NOT see i32 here — else a
|
|
460
|
+
// NaN remainder gets i32.trunc_sat'd to 0. Mirrors the emit.js `%` guard exactly.
|
|
461
|
+
if (op === '%') {
|
|
462
|
+
const ta = exprType(args[0], locals), tb = exprType(args[1], locals)
|
|
463
|
+
if (ta !== 'i32' || tb !== 'i32') return 'f64'
|
|
464
|
+
if (isUnsignedI32Expr(args[0]) || isUnsignedI32Expr(args[1])) return 'f64'
|
|
465
|
+
const dv = staticValue(args[1])
|
|
466
|
+
return (dv !== NO_VALUE && typeof dv === 'number' && dv !== 0 && Number.isInteger(dv)) ? 'i32' : 'f64'
|
|
467
|
+
}
|
|
468
|
+
// `*` — a JS multiply is an f64 operation; `i32.mul` reproduces it faithfully
|
|
469
|
+
// only while the exact product is f64-exact. Stay i32 when both operands are
|
|
470
|
+
// i32 *and* the product provably fits: a fully-static product checked
|
|
471
|
+
// directly, otherwise a literal operand small enough that |literal|·2^31 ≤
|
|
472
|
+
// 2^53 (mirrors emit.js `mulFitsI32` — keeps `i*4` i32, widens `h*16777619`).
|
|
473
|
+
if (op === '*') {
|
|
474
|
+
const ta = exprType(args[0], locals), tb = exprType(args[1], locals)
|
|
475
|
+
if (ta !== 'i32' || tb !== 'i32') return 'f64'
|
|
476
|
+
// uint32 operand: product can exceed i32; emit widens to f64 (see emit.js `*`).
|
|
477
|
+
if (isUnsignedI32Expr(args[0]) || isUnsignedI32Expr(args[1])) return 'f64'
|
|
478
|
+
if (sv !== NO_VALUE && typeof sv === 'number') return isI32(sv) ? 'i32' : 'f64'
|
|
479
|
+
const small = e => {
|
|
480
|
+
const v = staticValue(e)
|
|
481
|
+
return v !== NO_VALUE && typeof v === 'number' && Math.abs(v) <= 0x400000
|
|
482
|
+
}
|
|
483
|
+
return small(args[0]) || small(args[1]) ? 'i32' : 'f64'
|
|
484
|
+
}
|
|
485
|
+
// Unary preserves type
|
|
486
|
+
if (op === 'u-' || op === 'u+') return exprType(args[0], locals)
|
|
487
|
+
// Ternary / logical: conciliate
|
|
488
|
+
if (op === '?:' || op === '&&' || op === '||') {
|
|
489
|
+
const branches = op === '?:' ? [args[1], args[2]] : [args[0], args[1]]
|
|
490
|
+
const ta = exprType(branches[0], locals), tb = exprType(branches[1], locals)
|
|
491
|
+
return ta === 'i32' && tb === 'i32' ? 'i32' : 'f64'
|
|
492
|
+
}
|
|
493
|
+
if (op === '[') return 'f64'
|
|
494
|
+
// Builtin calls with known i32 result. Math.imul / Math.clz32 always produce
|
|
495
|
+
// a 32-bit integer; recognising this here keeps `let x = Math.imul(...)` (and
|
|
496
|
+
// chains like `x = Math.imul(x, k) + 12345`) on the i32 ABI all the way
|
|
497
|
+
// through, instead of widening the local to f64 because exprType defaulted.
|
|
498
|
+
if (op === '()') {
|
|
499
|
+
if (args[0] === 'math.imul' || args[0] === 'math.clz32') return 'i32'
|
|
500
|
+
// SIMD intrinsics → v128 lane vector, except lane-extract / reductions which
|
|
501
|
+
// hand a scalar back (i32x4.lane / v128.anyTrue / v128.allTrue → i32;
|
|
502
|
+
// f32x4.lane → f64). See module/simd.js.
|
|
503
|
+
if (typeof args[0] === 'string' && (args[0].startsWith('f32x4.') || args[0].startsWith('i32x4.') || args[0].startsWith('f64x2.') || args[0].startsWith('v128.'))) {
|
|
504
|
+
if (args[0] === 'f32x4.lane') return 'f64'
|
|
505
|
+
if (args[0] === 'i32x4.lane' || args[0] === 'v128.anyTrue' || args[0] === 'v128.allTrue') return 'i32'
|
|
506
|
+
return 'v128'
|
|
507
|
+
}
|
|
508
|
+
// charCodeAt: i32 when the index is provably in `[0, recv.length)` (an
|
|
509
|
+
// induction variable bounded by `recv.length` — OOB impossible). Otherwise
|
|
510
|
+
// f64: the JS-spec OOB result is NaN, which is not representable as i32.
|
|
511
|
+
if (Array.isArray(args[0]) && args[0][0] === '.' && args[0][2] === 'charCodeAt'
|
|
512
|
+
&& inBoundsCharCodeAt(ctx).has(args[0])) return 'i32'
|
|
513
|
+
// User-function call: consult the callee's narrowed result type. By the time
|
|
514
|
+
// analyzeBody runs in emitFunc, narrowSignatures has set sig.results[0]='i32'
|
|
515
|
+
// on every body-i32-only func. Propagating this lets `let h = userFn(...)`
|
|
516
|
+
// (mix in callback bench: i32-FNV) keep h as an i32 local instead of widening
|
|
517
|
+
// to f64 and round-tripping i32↔f64 every iteration.
|
|
518
|
+
if (typeof args[0] === 'string') {
|
|
519
|
+
const f = ctx.func.map?.get(args[0])
|
|
520
|
+
if (f?.sig?.results?.length === 1 && f.sig.results[0] === 'i32' && f.sig.ptrKind == null) return 'i32'
|
|
521
|
+
if (f?.sig?.results?.length === 1 && f.sig.results[0] === 'v128') return 'v128' // SIMD helper
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return 'f64'
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// === Integer-certainty fixpoint (shared by analyzeIntCertain + program-facts) ===
|
|
528
|
+
|
|
529
|
+
const INT_BIT_OPS = new Set(['|', '&', '^', '~', '<<', '>>', '>>>'])
|
|
530
|
+
const INT_CMP_OPS = new Set(['<', '>', '<=', '>=', '==', '!=', '===', '!==', '!'])
|
|
531
|
+
const INT_CLOSED_OPS = new Set(['+', '-', '*']) // `%` handled separately — int only for nonzero divisor
|
|
532
|
+
const INT_MATH_FNS = new Set(['imul', 'clz32', 'floor', 'ceil', 'round', 'trunc'])
|
|
533
|
+
|
|
534
|
+
function collectIntDefs(body) {
|
|
535
|
+
const defs = new Map()
|
|
536
|
+
const pushDef = (name, rhs) => {
|
|
537
|
+
let list = defs.get(name)
|
|
538
|
+
if (!list) { list = []; defs.set(name, list) }
|
|
539
|
+
list.push(rhs)
|
|
540
|
+
}
|
|
541
|
+
const collect = (node) => {
|
|
542
|
+
if (!Array.isArray(node)) return
|
|
543
|
+
const [op, ...args] = node
|
|
544
|
+
if (op === '=>') return
|
|
545
|
+
if (op === 'let' || op === 'const') {
|
|
546
|
+
for (const a of args)
|
|
547
|
+
if (Array.isArray(a) && a[0] === '=' && typeof a[1] === 'string') pushDef(a[1], a[2])
|
|
548
|
+
} else if (op === '=' && typeof args[0] === 'string') {
|
|
549
|
+
pushDef(args[0], args[1])
|
|
550
|
+
} else if (typeof op === 'string' && op.length > 1 && op.endsWith('=') &&
|
|
551
|
+
!INT_CMP_OPS.has(op) && op !== '=>' && typeof args[0] === 'string') {
|
|
552
|
+
pushDef(args[0], [op.slice(0, -1), args[0], args[1]])
|
|
553
|
+
} else if ((op === '++' || op === '--') && typeof args[0] === 'string') {
|
|
554
|
+
pushDef(args[0], [op === '++' ? '+' : '-', args[0], [null, 1]])
|
|
555
|
+
}
|
|
556
|
+
for (const a of args) collect(a)
|
|
557
|
+
}
|
|
558
|
+
collect(body)
|
|
559
|
+
return defs
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function makeIsIntExpr(intCertain) {
|
|
563
|
+
return function isIntExpr(expr) {
|
|
564
|
+
if (typeof expr === 'number') return Number.isInteger(expr) && !Object.is(expr, -0)
|
|
565
|
+
if (typeof expr === 'boolean') return true
|
|
566
|
+
if (typeof expr === 'string') return intCertain.get(expr) === true
|
|
567
|
+
if (!Array.isArray(expr)) return false
|
|
568
|
+
const sv = staticValue(expr)
|
|
569
|
+
if (sv !== NO_VALUE && typeof sv === 'number' && Object.is(sv, -0)) return false
|
|
570
|
+
const [op, ...args] = expr
|
|
571
|
+
if (op == null) {
|
|
572
|
+
const v = args[0]
|
|
573
|
+
if (typeof v === 'number') return Number.isInteger(v) && !Object.is(v, -0)
|
|
574
|
+
if (typeof v === 'boolean') return true
|
|
575
|
+
return false
|
|
576
|
+
}
|
|
577
|
+
if (INT_BIT_OPS.has(op) || INT_CMP_OPS.has(op)) return true
|
|
578
|
+
if (op === '.') {
|
|
579
|
+
if ((args[1] === 'length' || args[1] === 'byteLength') && typeof args[0] === 'string') {
|
|
580
|
+
const vt = lookupValType(args[0])
|
|
581
|
+
return vt === VAL.TYPED || vt === VAL.ARRAY || vt === VAL.STRING || vt === VAL.BUFFER
|
|
582
|
+
}
|
|
583
|
+
if (args[1] === 'size' && typeof args[0] === 'string') {
|
|
584
|
+
const vt = lookupValType(args[0])
|
|
585
|
+
return vt === VAL.SET || vt === VAL.MAP
|
|
586
|
+
}
|
|
587
|
+
return false
|
|
588
|
+
}
|
|
589
|
+
if (INT_CLOSED_OPS.has(op)) {
|
|
590
|
+
const a = isIntExpr(args[0])
|
|
591
|
+
const b = args[1] != null ? isIntExpr(args[1]) : a
|
|
592
|
+
return a && b
|
|
593
|
+
}
|
|
594
|
+
// `a % b` is integer-valued only when b is a provably-nonzero integer
|
|
595
|
+
// constant — `a % 0` is NaN, which is not an integer. A runtime or zero
|
|
596
|
+
// divisor leaves the expression non-int (f64), so result-narrowing won't
|
|
597
|
+
// truncate a NaN remainder to 0 and floor-elision won't drop a NaN.
|
|
598
|
+
if (op === '%') {
|
|
599
|
+
const bv = staticValue(args[1])
|
|
600
|
+
return bv !== NO_VALUE && typeof bv === 'number' && bv !== 0 && Number.isInteger(bv) && isIntExpr(args[0])
|
|
601
|
+
}
|
|
602
|
+
if (op === 'u-' || op === 'u+') return isIntExpr(args[0])
|
|
603
|
+
if (op === '?:') return isIntExpr(args[1]) && isIntExpr(args[2])
|
|
604
|
+
if (op === '&&' || op === '||') return isIntExpr(args[0]) && isIntExpr(args[1])
|
|
605
|
+
if (op === '()') {
|
|
606
|
+
const c = args[0]
|
|
607
|
+
if (typeof c === 'string' && c.startsWith('math.') && INT_MATH_FNS.has(c.slice(5))) return true
|
|
608
|
+
if (Array.isArray(c) && c[0] === '.' && c[1] === 'Math' && INT_MATH_FNS.has(c[2])) return true
|
|
609
|
+
}
|
|
610
|
+
return false
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** Monotone fixpoint over binding defs in `body`. Map name → intCertain. */
|
|
615
|
+
export function intCertainMap(body) {
|
|
616
|
+
const defs = collectIntDefs(body)
|
|
617
|
+
if (defs.size === 0) return new Map()
|
|
618
|
+
const intCertain = new Map()
|
|
619
|
+
for (const name of defs.keys()) intCertain.set(name, true)
|
|
620
|
+
// A parameter has no def in `body` — its entry value is whatever the caller
|
|
621
|
+
// passed. For an f64 param (JS-number ABI) that is an arbitrary real, so a
|
|
622
|
+
// reassigned f64 param is NOT integer-certain: a self/int reassignment
|
|
623
|
+
// (`p = p`, `p = p + 1`) would otherwise vacuously satisfy the optimistic
|
|
624
|
+
// fixpoint, since `isIntExpr(p)` reads p's own provisional `true`. Seed f64
|
|
625
|
+
// params false so the unknown entry value grounds the lattice; i32-narrowed
|
|
626
|
+
// params (integer ABI) stay certain. Seeding false is always conservative —
|
|
627
|
+
// at worst it re-applies a floor/round that was a runtime no-op — so a
|
|
628
|
+
// mismatched ctx.func.current (whole-program intExprChecker callers) can only
|
|
629
|
+
// forgo an optimization, never miscompile.
|
|
630
|
+
for (const p of ctx.func.current?.params || [])
|
|
631
|
+
if (p.type !== 'i32' && intCertain.has(p.name)) intCertain.set(p.name, false)
|
|
632
|
+
const isIntExpr = makeIsIntExpr(intCertain)
|
|
633
|
+
let changed = true
|
|
634
|
+
while (changed) {
|
|
635
|
+
changed = false
|
|
636
|
+
for (const [name, rhsList] of defs) {
|
|
637
|
+
if (!intCertain.get(name)) continue
|
|
638
|
+
if (!rhsList.every(isIntExpr)) { intCertain.set(name, false); changed = true }
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return intCertain
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Returns `expr => boolean` — integer-shaped expressions in `body`. */
|
|
645
|
+
export function intExprChecker(body) {
|
|
646
|
+
return makeIsIntExpr(intCertainMap(body))
|
|
647
|
+
}
|