jz 0.8.1 → 0.9.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 +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +27 -27
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6271
- package/index.js +165 -74
- package/interop.js +189 -17
- package/jzify/arguments.js +32 -1
- package/jzify/async.js +409 -0
- package/jzify/classes.js +136 -18
- package/jzify/generators.js +639 -0
- package/jzify/hoist-vars.js +73 -1
- package/jzify/index.js +123 -4
- package/jzify/names.js +1 -0
- package/jzify/transform.js +239 -1
- package/layout.js +48 -3
- package/module/array.js +299 -44
- package/module/atomics.js +144 -0
- package/module/collection.js +1429 -155
- package/module/core.js +431 -40
- package/module/date.js +142 -120
- package/module/fs.js +144 -0
- package/module/function.js +6 -3
- package/module/index.js +4 -1
- package/module/json.js +270 -49
- package/module/math.js +892 -32
- package/module/number.js +532 -163
- package/module/object.js +353 -95
- package/module/regex.js +157 -7
- package/module/schema.js +100 -2
- package/module/string.js +301 -84
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +71 -5
- package/src/ast.js +11 -5
- package/src/autoload.js +28 -1
- package/src/bridge.js +7 -2
- package/src/compile/analyze-scans.js +22 -7
- package/src/compile/analyze.js +136 -30
- package/src/compile/dyn-closure-tables.js +277 -0
- package/src/compile/emit-assign.js +281 -15
- package/src/compile/emit.js +979 -54
- package/src/compile/index.js +271 -29
- package/src/compile/infer.js +34 -3
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/narrow.js +543 -15
- package/src/compile/peel-stencil.js +5 -0
- package/src/compile/plan/index.js +45 -3
- package/src/compile/plan/inline.js +8 -1
- package/src/compile/plan/literals.js +39 -0
- package/src/compile/plan/scope.js +430 -23
- package/src/compile/program-facts.js +732 -38
- package/src/ctx.js +64 -9
- package/src/helper-counters.js +7 -1
- package/src/ir.js +113 -5
- package/src/kind-traits.js +66 -3
- package/src/kind.js +84 -12
- package/src/op-policy.js +7 -4
- package/src/optimize/index.js +1060 -750
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +972 -67
- package/src/prepare/index.js +792 -63
- package/src/prepare/math-kernel.js +331 -0
- package/src/prepare/pre-eval.js +714 -0
- package/src/snapshot.js +194 -0
- package/src/type.js +1170 -56
- package/src/wat/assemble.js +403 -65
- package/src/wat/codegen.js +74 -14
- package/transform.js +113 -4
- package/wasi.js +3 -0
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preEval — unified compile-time constant-folding pass over the PREPARED AST
|
|
3
|
+
* (runs once, right after `prepare()`, before `compile()`).
|
|
4
|
+
*
|
|
5
|
+
* Subsumes/extends the narrow const-folders already scattered through prepare
|
|
6
|
+
* (staticValue, staticStringExpr, constNum in prepare/index.js) with one pass
|
|
7
|
+
* that also folds: numeric arithmetic chains (with optional rational/extended
|
|
8
|
+
* precision — see Rational below), comparisons/equality, `%`/bitwise ops,
|
|
9
|
+
* ASCII string methods, pure `Math.*` calls (bit-exact vs jz's own kernel via
|
|
10
|
+
* math-kernel.js, NOT host Math — see that module), dead `if`/`while(false)`
|
|
11
|
+
* branches, and zero-arg pure function calls (which subsumes IIFE collapse:
|
|
12
|
+
* lift-iife.js already turns `(() => EXPR)()` into a 0-param top-level
|
|
13
|
+
* function + a 0-arg call before prepare ever runs, so "IIFE collapse" here
|
|
14
|
+
* is just the general case of a 0-arg call whose target's body reduces to a
|
|
15
|
+
* constant — same code path for a literal IIFE and an ordinary user-authored
|
|
16
|
+
* zero-arg helper).
|
|
17
|
+
*
|
|
18
|
+
* # Architecture
|
|
19
|
+
* Two cooperating passes, sharing one `env` (Map<name, EvalResult>) and one
|
|
20
|
+
* `state` ({ rationalOn, funcByName, evaluating }):
|
|
21
|
+
*
|
|
22
|
+
* evalConst(node, env, state) -> EvalResult | null
|
|
23
|
+
* Tries to reduce a whole expression subtree to a SINGLE constant value,
|
|
24
|
+
* recursing entirely in EvalResult space (never rebuilds AST nodes
|
|
25
|
+
* mid-chain) — this is what lets a numeric chain carry an exact Rational
|
|
26
|
+
* all the way to the final `+`/`-`/`*`/`/` and round only once. Also
|
|
27
|
+
* resolves Math.* calls, ASCII string methods, and — recursively, with a
|
|
28
|
+
* cycle guard — zero-arg calls to other functions.
|
|
29
|
+
*
|
|
30
|
+
* foldNode(node, env, state) -> node
|
|
31
|
+
* The tree REWRITER. At every node it first asks evalConst for a full
|
|
32
|
+
* reduction (turning that subtree into ONE literal node — the only place
|
|
33
|
+
* an exact Rational gets rounded to f64). When evalConst can't fully
|
|
34
|
+
* reduce (e.g. one operand is a runtime value), it falls back to
|
|
35
|
+
* structural per-child folding, plus statement-level dead-`if`/`while`-
|
|
36
|
+
* branch elimination (foldStmts).
|
|
37
|
+
*
|
|
38
|
+
* `env` (Map<name, EvalResult>) is the scaffolding for ONE narrowly-scoped use:
|
|
39
|
+
* evalFunctionBodyConst's zero-arg-call evaluation threads its OWN, freshly-
|
|
40
|
+
* empty env through a callee's `let`/`const`-then-`return` chain (so `helper`'s
|
|
41
|
+
* internal `const a = 1+2; return a*3` resolves) — see evalStmtsConst. The
|
|
42
|
+
* general `foldStmts`/`foldNode` walk never POPULATES it (a bare identifier
|
|
43
|
+
* reference is therefore never rewritten): several existing passes downstream
|
|
44
|
+
* pattern-match a NAMED loop bound/index/property-key expression structurally
|
|
45
|
+
* rather than by value (clamp-peel + the multi-pixel SIMD blur match,
|
|
46
|
+
* unrollSmallConstFor's trip-count shape, watr LICM's post-inline invariant
|
|
47
|
+
* recognition, static.js's schema/SRoA static-vs-dynamic key classification —
|
|
48
|
+
* discovered the hard way, by regressing each of them once). Rewriting
|
|
49
|
+
* `row = y*ww` to `row = y*64` is value-identical but silently swaps which of
|
|
50
|
+
* those shape-sensitive passes fires. Tier 1 stays inside the proven-safe
|
|
51
|
+
* boundary: fold every expression tree, never rewrite a bare-name reference.
|
|
52
|
+
*
|
|
53
|
+
* A single top-to-bottom pass over (every ctx.func.list body + the module
|
|
54
|
+
* body) is a full fixpoint: evalConst re-derives everything it needs from the
|
|
55
|
+
* RAW callee body on demand (via state.funcByName), so it never depends on
|
|
56
|
+
* another function having been folded first, regardless of declaration order.
|
|
57
|
+
*
|
|
58
|
+
* Identity preservation matters here beyond the usual "avoid needless
|
|
59
|
+
* allocation": prepare() forward-seeds compile-stage fact stores (program-
|
|
60
|
+
* facts.js's WeakMap caches, compile/infer.js's recordGlobalRep, ...) keyed by
|
|
61
|
+
* the SPECIFIC node objects it walked. `foldStmts`/`foldBlockLike` return the
|
|
62
|
+
* exact input array/node whenever nothing in it changed, all the way up, so a
|
|
63
|
+
* subtree preEval didn't touch keeps the object identity those caches rely on.
|
|
64
|
+
*
|
|
65
|
+
* # Purity / precision guards
|
|
66
|
+
* - Zero-arg call folding evaluates the callee's OWN body in a FRESH empty
|
|
67
|
+
* env (no outer capture) and bails on anything but a `let`/`const` chain
|
|
68
|
+
* ending in one `return` — any other statement shape (if/for/throw/...)
|
|
69
|
+
* is conservatively left unfolded.
|
|
70
|
+
* - String folding is ASCII-only (jz strings are UTF-8 internally; a
|
|
71
|
+
* non-ASCII `.length`/`.slice` could disagree with host JS's UTF-16
|
|
72
|
+
* view — see README divergences) and mixed string+number `+` is
|
|
73
|
+
* deliberately NOT folded (self-host's __ftoa is a 9-significant-digit
|
|
74
|
+
* dtoa, host `String(number)` is shortest-round-trip — folding could
|
|
75
|
+
* bake a MORE precise string than the unfolded kernel would produce).
|
|
76
|
+
* - `Math.pow`/`**` folds via the exact 3-way split emit.js's own
|
|
77
|
+
* constant-arg fast path already uses (math-kernel.js `pow`) — zero new
|
|
78
|
+
* divergence from today's compiled output.
|
|
79
|
+
* - `optimize.rationalConst !== false` (default ON) gates the rational
|
|
80
|
+
* carry; off, numeric folding still happens (still shrinks WAT) via
|
|
81
|
+
* plain sequential per-op f64 rounding — bit-exact vs naive JS
|
|
82
|
+
* evaluation, for callers who want that instead.
|
|
83
|
+
*
|
|
84
|
+
* @module prepare/pre-eval
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
import { extractParams, classifyParam } from '../ast.js'
|
|
88
|
+
import { ctx } from '../ctx.js'
|
|
89
|
+
import { MATH_KERNEL, powFold } from './math-kernel.js'
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Rational — exact value = n/d, n: signed BigInt, d: positive BigInt. Every
|
|
93
|
+
// finite f64 IS an exact rational (double = mantissa * 2^exponent), so a
|
|
94
|
+
// literal seeds an EXACT starting point; +,-,*,/ stay exact through a whole
|
|
95
|
+
// formula; the f64 result is materialized via correctly-rounded decimal
|
|
96
|
+
// string -> Number() ONCE, at the point the chain stops (crosses into a
|
|
97
|
+
// non-arithmetic consumer, or reaches the top of a foldable subtree).
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
const _buf = new ArrayBuffer(8)
|
|
100
|
+
const _dv = new DataView(_buf)
|
|
101
|
+
function f64Bits(x) { _dv.setFloat64(0, x, false); return _dv.getBigUint64(0, false) }
|
|
102
|
+
|
|
103
|
+
function ratGcd(a, b) { a = a < 0n ? -a : a; b = b < 0n ? -b : b; while (b) { [a, b] = [b, a % b] } return a || 1n }
|
|
104
|
+
function ratMake(n, d) {
|
|
105
|
+
if (d < 0n) { n = -n; d = -d }
|
|
106
|
+
const g = ratGcd(n, d)
|
|
107
|
+
return { n: n / g, d: d / g }
|
|
108
|
+
}
|
|
109
|
+
/** Exact rational for a finite f64 (null for NaN/±Infinity — those bail the rational chain). */
|
|
110
|
+
function f64ToRational(x) {
|
|
111
|
+
if (!Number.isFinite(x)) return null
|
|
112
|
+
if (x === 0) return { n: 0n, d: 1n }
|
|
113
|
+
const bits = f64Bits(x)
|
|
114
|
+
const sign = (bits >> 63n) & 1n
|
|
115
|
+
let exp = Number((bits >> 52n) & 0x7ffn)
|
|
116
|
+
let mant = bits & 0xfffffffffffffn
|
|
117
|
+
if (exp === 0) exp = 1
|
|
118
|
+
else mant |= 0x10000000000000n
|
|
119
|
+
const e = exp - 1075
|
|
120
|
+
let n = mant, d = 1n
|
|
121
|
+
if (e >= 0) n <<= BigInt(e)
|
|
122
|
+
else d <<= BigInt(-e)
|
|
123
|
+
if (sign) n = -n
|
|
124
|
+
return ratMake(n, d)
|
|
125
|
+
}
|
|
126
|
+
const ratAdd = (a, b) => ratMake(a.n * b.d + b.n * a.d, a.d * b.d)
|
|
127
|
+
const ratSub = (a, b) => ratMake(a.n * b.d - b.n * a.d, a.d * b.d)
|
|
128
|
+
const ratMul = (a, b) => ratMake(a.n * b.n, a.d * b.d)
|
|
129
|
+
const ratDiv = (a, b) => b.n === 0n ? null : ratMake(a.n * b.d, a.d * b.n)
|
|
130
|
+
/** Correctly-rounded rational -> f64: exact decimal expansion (generous digit budget,
|
|
131
|
+
* far beyond the 17 significant digits that suffice to round-trip any double) fed
|
|
132
|
+
* through the host's spec-mandated (round-to-nearest) string-to-Number parser. */
|
|
133
|
+
function ratToF64(r) {
|
|
134
|
+
if (r.n === 0n) return 0
|
|
135
|
+
let n = r.n, neg = n < 0n
|
|
136
|
+
if (neg) n = -n
|
|
137
|
+
const d = r.d
|
|
138
|
+
const intPart = n / d
|
|
139
|
+
let rem = n % d
|
|
140
|
+
let s = intPart.toString()
|
|
141
|
+
if (rem !== 0n) {
|
|
142
|
+
s += '.'
|
|
143
|
+
for (let i = 0; i < 60 && rem !== 0n; i++) { rem *= 10n; const dig = rem / d; s += dig.toString(); rem %= d }
|
|
144
|
+
}
|
|
145
|
+
return Number(neg ? '-' + s : s)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// EvalResult: { t: 'num'|'str'|'bool'|'null'|'undef', v?, r? }
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
const numResult = (v) => ({ t: 'num', v, r: Number.isFinite(v) ? f64ToRational(v) : null })
|
|
152
|
+
const strResult = (v) => ({ t: 'str', v })
|
|
153
|
+
const boolResult = (v) => ({ t: 'bool', v: !!v })
|
|
154
|
+
const NULL_RESULT = { t: 'null' }
|
|
155
|
+
const UNDEF_RESULT = { t: 'undef' }
|
|
156
|
+
|
|
157
|
+
const isAsciiSafe = (s) => /^[\x00-\x7F]*$/.test(s)
|
|
158
|
+
|
|
159
|
+
function isLiteralNode(node) {
|
|
160
|
+
if (!Array.isArray(node)) return false
|
|
161
|
+
const op = node[0]
|
|
162
|
+
return op == null || op === 'str' || op === 'bool'
|
|
163
|
+
}
|
|
164
|
+
/** Read an already-literal AST node into an EvalResult (no evaluation, just recognition). */
|
|
165
|
+
function literalOf(node) {
|
|
166
|
+
if (!Array.isArray(node)) return null
|
|
167
|
+
const op = node[0]
|
|
168
|
+
if (op == null) {
|
|
169
|
+
const v = node[1]
|
|
170
|
+
if (typeof v === 'number') return numResult(v)
|
|
171
|
+
if (v === null) return NULL_RESULT
|
|
172
|
+
if (v === undefined) return UNDEF_RESULT
|
|
173
|
+
if (typeof v === 'boolean') return boolResult(v)
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
176
|
+
if (op === 'str' && typeof node[1] === 'string') return strResult(node[1])
|
|
177
|
+
if (op === 'bool') return boolResult(node[1])
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
/** EvalResult -> literal AST node. The ONE place a Rational's exact value is rounded
|
|
181
|
+
* and forgotten — callers only reach here once a chain truly terminates. */
|
|
182
|
+
function nodeOf(r) {
|
|
183
|
+
switch (r.t) {
|
|
184
|
+
case 'num': return [null, r.v]
|
|
185
|
+
case 'str': return ['str', r.v]
|
|
186
|
+
case 'bool': return ['bool', r.v ? 1 : 0]
|
|
187
|
+
case 'null': return [null, null]
|
|
188
|
+
default: return [null, undefined]
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const toJSValue = (r) => r.t === 'null' ? null : r.t === 'undef' ? undefined : r.v
|
|
193
|
+
function toNumResult(r) {
|
|
194
|
+
if (r.t === 'num') return r
|
|
195
|
+
if (r.t === 'bool') return numResult(r.v ? 1 : 0)
|
|
196
|
+
if (r.t === 'null') return numResult(0)
|
|
197
|
+
if (r.t === 'undef') return numResult(NaN)
|
|
198
|
+
return null // strings: deliberately NOT ToNumber-coerced (see module doc)
|
|
199
|
+
}
|
|
200
|
+
function toBoolean(r) {
|
|
201
|
+
if (r.t === 'bool') return r.v
|
|
202
|
+
if (r.t === 'num') return r.v !== 0 && !Number.isNaN(r.v)
|
|
203
|
+
if (r.t === 'str') return r.v.length !== 0
|
|
204
|
+
return false // null/undefined
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function plainNumOp(op, a, b) {
|
|
208
|
+
switch (op) {
|
|
209
|
+
case '-': return a - b
|
|
210
|
+
case '*': return a * b
|
|
211
|
+
case '/': return a / b
|
|
212
|
+
case '%': return a % b
|
|
213
|
+
case '&': return a & b
|
|
214
|
+
case '|': return a | b
|
|
215
|
+
case '^': return a ^ b
|
|
216
|
+
case '<<': return a << b
|
|
217
|
+
case '>>': return a >> b
|
|
218
|
+
case '>>>': return a >>> b
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const NUM_ONLY_OPS = new Set(['-', '*', '/', '%', '&', '|', '^', '<<', '>>', '>>>'])
|
|
222
|
+
const RATIONAL_OPS = new Set(['-', '*', '/'])
|
|
223
|
+
const CMP_OPS = new Set(['<', '>', '<=', '>=', '==', '!=', '===', '!=='])
|
|
224
|
+
const BINARY_OPS = new Set([...NUM_ONLY_OPS, ...CMP_OPS])
|
|
225
|
+
|
|
226
|
+
/** Numeric binary fold. Carries the exact Rational through +,-,*,/ when both operands
|
|
227
|
+
* still have one (state.rationalOn); falls back to plain per-op f64 otherwise. A ZERO
|
|
228
|
+
* rational result recomputes via plain f64 arithmetic instead — signed zero (`x+(-x)`
|
|
229
|
+
* -> +0, `0*-1` -> -0) has no faithful rational encoding, and plain JS +,-,*,/ already
|
|
230
|
+
* implement IEEE754 signed-zero correctly, so falling back for that one case is exact,
|
|
231
|
+
* not approximate. A non-finite rational result (true overflow of the exact formula,
|
|
232
|
+
* e.g. `1e300*1e300/1e300`) is KEPT — that's the accuracy win rational carry promises,
|
|
233
|
+
* not a divergence to guard against. */
|
|
234
|
+
function foldNumBinary(op, L, R, rationalOn) {
|
|
235
|
+
const plain = plainNumOp(op, L.v, R.v)
|
|
236
|
+
if (!rationalOn || !RATIONAL_OPS.has(op) || !L.r || !R.r) return numResult(plain)
|
|
237
|
+
const rr = op === '-' ? ratSub(L.r, R.r) : op === '*' ? ratMul(L.r, R.r) : ratDiv(L.r, R.r)
|
|
238
|
+
if (!rr) return numResult(plain)
|
|
239
|
+
if (rr.n === 0n) return numResult(plain)
|
|
240
|
+
return { t: 'num', v: ratToF64(rr), r: rr }
|
|
241
|
+
}
|
|
242
|
+
function foldNumAdd(L, R, rationalOn) {
|
|
243
|
+
if (!rationalOn || !L.r || !R.r) return numResult(L.v + R.v)
|
|
244
|
+
const rr = ratAdd(L.r, R.r)
|
|
245
|
+
if (rr.n === 0n) return numResult(L.v + R.v)
|
|
246
|
+
return { t: 'num', v: ratToF64(rr), r: rr }
|
|
247
|
+
}
|
|
248
|
+
function foldNumUnaryNeg(a) {
|
|
249
|
+
if (a.v === 0) return numResult(-a.v) // exact sign flip incl. ±0; BigInt has no signed zero
|
|
250
|
+
return a.r ? { t: 'num', v: -a.v, r: { n: -a.r.n, d: a.r.d } } : numResult(-a.v)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function foldUnary(op, a) {
|
|
254
|
+
if (op === 'u-') { const L = toNumResult(a); return L && foldNumUnaryNeg(L) }
|
|
255
|
+
if (op === 'u+') return toNumResult(a)
|
|
256
|
+
if (op === '!') return boolResult(!toBoolean(a))
|
|
257
|
+
if (op === '~') return a.t === 'str' ? null : numResult(~toJSValue(a))
|
|
258
|
+
return null
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function foldBinary(op, a, b, rationalOn) {
|
|
262
|
+
if (op === '+') {
|
|
263
|
+
if (a.t === 'str' && b.t === 'str')
|
|
264
|
+
return (isAsciiSafe(a.v) && isAsciiSafe(b.v)) ? strResult('' + a.v + b.v) : null
|
|
265
|
+
if (a.t === 'str' || b.t === 'str') return null // mixed string+number: see module doc
|
|
266
|
+
const L = toNumResult(a), R = toNumResult(b)
|
|
267
|
+
return (L && R) ? foldNumAdd(L, R, rationalOn) : null
|
|
268
|
+
}
|
|
269
|
+
if (op === '**') {
|
|
270
|
+
const L = toNumResult(a), R = toNumResult(b)
|
|
271
|
+
return (L && R) ? numResult(powFold(L.v, R.v)) : null
|
|
272
|
+
}
|
|
273
|
+
if (NUM_ONLY_OPS.has(op)) {
|
|
274
|
+
const L = toNumResult(a), R = toNumResult(b)
|
|
275
|
+
if (!L || !R) return null
|
|
276
|
+
return op === '-' || op === '*' || op === '/' ? foldNumBinary(op, L, R, rationalOn) : numResult(plainNumOp(op, L.v, R.v))
|
|
277
|
+
}
|
|
278
|
+
if (CMP_OPS.has(op)) {
|
|
279
|
+
if ((a.t === 'str' && !isAsciiSafe(a.v)) || (b.t === 'str' && !isAsciiSafe(b.v))) return null
|
|
280
|
+
const x = toJSValue(a), y = toJSValue(b)
|
|
281
|
+
switch (op) {
|
|
282
|
+
case '<': return boolResult(x < y)
|
|
283
|
+
case '>': return boolResult(x > y)
|
|
284
|
+
case '<=': return boolResult(x <= y)
|
|
285
|
+
case '>=': return boolResult(x >= y)
|
|
286
|
+
case '==': return boolResult(x == y)
|
|
287
|
+
case '!=': return boolResult(x != y)
|
|
288
|
+
case '===': return boolResult(x === y)
|
|
289
|
+
case '!==': return boolResult(x !== y)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return null
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// Math.* / string-method call evaluation
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
const MATH_CONST = new Set(['PI', 'E', 'LN2', 'LN10', 'LOG2E', 'LOG10E', 'SQRT2', 'SQRT1_2'])
|
|
299
|
+
// prepare resolves `Math.X` to a bare `'math.X'` STRING (both a niladic-call target
|
|
300
|
+
// `['()','math.sqrt',args]` and, for the no-arg constants, a plain value reference
|
|
301
|
+
// `'math.PI'` with no wrapping `()` at all) whenever it runs through the full host
|
|
302
|
+
// pipeline's jzify/autoload service wiring — `['.', 'Math', 'X']` only survives on a
|
|
303
|
+
// bare prepare() call with no services injected. Recognize both shapes.
|
|
304
|
+
function mathCalleeName(callee) {
|
|
305
|
+
if (Array.isArray(callee) && callee[0] === '.' && callee[1] === 'Math' && typeof callee[2] === 'string') return callee[2]
|
|
306
|
+
if (typeof callee === 'string' && callee.startsWith('math.')) return callee.slice(5)
|
|
307
|
+
return null
|
|
308
|
+
}
|
|
309
|
+
// sqrt/abs/floor/ceil/trunc: IEEE754-mandated correctly-rounded in both JS and wasm.
|
|
310
|
+
// round/sign/fround: jz's WAT is deliberately engineered to reproduce these exact host
|
|
311
|
+
// JS semantics (see module/math.js). All bit-exact vs the compiled kernel by construction.
|
|
312
|
+
const HOST_EXACT_UNARY = new Set(['sqrt', 'abs', 'floor', 'ceil', 'trunc', 'round', 'fround', 'sign'])
|
|
313
|
+
|
|
314
|
+
function evalMathCall(name, vs) {
|
|
315
|
+
if (name === 'pow') return vs.length === 2 ? numResult(powFold(vs[0], vs[1])) : null
|
|
316
|
+
if (name === 'min') return numResult(vs.length ? Math.min(...vs) : Infinity)
|
|
317
|
+
if (name === 'max') return numResult(vs.length ? Math.max(...vs) : -Infinity)
|
|
318
|
+
if (name === 'imul') return vs.length === 2 ? numResult(Math.imul(vs[0], vs[1])) : null
|
|
319
|
+
if (name === 'clz32') return vs.length === 1 ? numResult(Math.clz32(vs[0])) : null
|
|
320
|
+
if (HOST_EXACT_UNARY.has(name)) return vs.length === 1 ? numResult(Math[name](vs[0])) : null
|
|
321
|
+
const kfn = MATH_KERNEL['math.' + name]
|
|
322
|
+
return kfn ? numResult(kfn(...vs)) : null
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** args: EvalResult[] (some entries may be null — the method itself validates types/arity). */
|
|
326
|
+
function evalStringMethod(name, s, args) {
|
|
327
|
+
const isNumOrAbsent = (a) => a == null || a.t === 'num'
|
|
328
|
+
if (name === 'toUpperCase' && args.length === 0) return strResult(s.toUpperCase())
|
|
329
|
+
if (name === 'toLowerCase' && args.length === 0) return strResult(s.toLowerCase())
|
|
330
|
+
if (name === 'trim' && args.length === 0) return strResult(s.trim())
|
|
331
|
+
if (name === 'slice' && args.length <= 2 && args.every(isNumOrAbsent)) {
|
|
332
|
+
const r = s.slice(args[0]?.v, args[1]?.v)
|
|
333
|
+
return isAsciiSafe(r) ? strResult(r) : null
|
|
334
|
+
}
|
|
335
|
+
if (name === 'charAt' && args.length <= 1 && isNumOrAbsent(args[0])) return strResult(s.charAt(args[0]?.v ?? 0))
|
|
336
|
+
if (name === 'indexOf' && args.length >= 1 && args[0]?.t === 'str' && isAsciiSafe(args[0].v))
|
|
337
|
+
return numResult(s.indexOf(args[0].v, args[1]?.v))
|
|
338
|
+
return null
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function collectArgs(argsNode) {
|
|
342
|
+
if (argsNode == null) return []
|
|
343
|
+
if (Array.isArray(argsNode) && argsNode[0] === ',') return argsNode.slice(1)
|
|
344
|
+
return [argsNode]
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
// evalConst — full-subtree constant evaluation (EvalResult space, no AST
|
|
349
|
+
// round-trips mid-chain — see module doc).
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
function evalConst(node, env, state) {
|
|
352
|
+
if (typeof node === 'string') {
|
|
353
|
+
const b = env.get(node)
|
|
354
|
+
if (b !== undefined) return b
|
|
355
|
+
if (node.startsWith('math.') && MATH_CONST.has(node.slice(5))) return numResult(Math[node.slice(5)])
|
|
356
|
+
return null
|
|
357
|
+
}
|
|
358
|
+
if (!Array.isArray(node)) return null
|
|
359
|
+
const op = node[0]
|
|
360
|
+
|
|
361
|
+
if (op == null) {
|
|
362
|
+
const v = node[1]
|
|
363
|
+
if (typeof v === 'number') return numResult(v)
|
|
364
|
+
if (v === null) return NULL_RESULT
|
|
365
|
+
if (v === undefined) return UNDEF_RESULT
|
|
366
|
+
if (typeof v === 'boolean') return boolResult(v)
|
|
367
|
+
return null
|
|
368
|
+
}
|
|
369
|
+
if (op === 'str') return typeof node[1] === 'string' ? strResult(node[1]) : null
|
|
370
|
+
if (op === 'bool') return boolResult(node[1])
|
|
371
|
+
|
|
372
|
+
if (op === 'u-' || op === 'u+' || op === '!' || op === '~') {
|
|
373
|
+
const a = evalConst(node[1], env, state)
|
|
374
|
+
return a && foldUnary(op, a)
|
|
375
|
+
}
|
|
376
|
+
if (op === '+' || op === '**' || BINARY_OPS.has(op)) {
|
|
377
|
+
if (node.length !== 3) return null
|
|
378
|
+
const a = evalConst(node[1], env, state)
|
|
379
|
+
if (!a) return null
|
|
380
|
+
const b = evalConst(node[2], env, state)
|
|
381
|
+
return b && foldBinary(op, a, b, state.rationalOn)
|
|
382
|
+
}
|
|
383
|
+
if (op === '&&' || op === '||' || op === '??') {
|
|
384
|
+
const a = evalConst(node[1], env, state)
|
|
385
|
+
if (!a) return null
|
|
386
|
+
const takeLeft = op === '&&' ? !toBoolean(a) : op === '||' ? toBoolean(a) : !(a.t === 'null' || a.t === 'undef')
|
|
387
|
+
const picked = takeLeft ? a : evalConst(node[2], env, state)
|
|
388
|
+
// `&&`/`||`/`??` is value-preserving at RUNTIME (it returns whichever raw operand
|
|
389
|
+
// won, untyped) — jz deliberately does NOT re-narrow that to the picked operand's
|
|
390
|
+
// own type when the two operands' types differ (e.g. `5 && true` crosses the
|
|
391
|
+
// boundary as the numeric carrier 1, not JS `true` — a documented gap, see
|
|
392
|
+
// test/booleans.js). Folding to a literal of the picked operand's OWN type would be
|
|
393
|
+
// MORE precise than that runtime behavior, i.e. a real divergence — so only fold
|
|
394
|
+
// when the operand that would have been dropped has the SAME type as the one kept
|
|
395
|
+
// (never fires here means never risks it; still folds the far more common
|
|
396
|
+
// same-type case, e.g. `x ?? 0` chains, `a && b` boolean chains).
|
|
397
|
+
if (!picked) return null
|
|
398
|
+
// Both branches must independently prove out to a value (and agree in type) —
|
|
399
|
+
// an un-evaluable other branch means its type is UNKNOWN here, which is exactly
|
|
400
|
+
// the unsafe case (see comment above): stay conservative, don't fold.
|
|
401
|
+
const other = takeLeft ? evalConst(node[2], env, state) : a
|
|
402
|
+
return (other && other.t === picked.t) ? picked : null
|
|
403
|
+
}
|
|
404
|
+
if ((op === '?:' || op === '?') && node.length === 4) {
|
|
405
|
+
const c = evalConst(node[1], env, state)
|
|
406
|
+
if (!c) return null
|
|
407
|
+
const cond = toBoolean(c)
|
|
408
|
+
const picked = cond ? evalConst(node[2], env, state) : evalConst(node[3], env, state)
|
|
409
|
+
if (!picked) return null
|
|
410
|
+
const other = cond ? evalConst(node[3], env, state) : evalConst(node[2], env, state)
|
|
411
|
+
return (other && other.t === picked.t) ? picked : null
|
|
412
|
+
}
|
|
413
|
+
if (op === '.' || op === '?.') {
|
|
414
|
+
if (node[1] === 'Math' && typeof node[2] === 'string' && MATH_CONST.has(node[2])) return numResult(Math[node[2]])
|
|
415
|
+
const recv = evalConst(node[1], env, state)
|
|
416
|
+
if (recv && recv.t === 'str' && node[2] === 'length' && isAsciiSafe(recv.v)) return numResult(recv.v.length)
|
|
417
|
+
return null
|
|
418
|
+
}
|
|
419
|
+
if (op === '()') return evalCallConst(node, env, state)
|
|
420
|
+
if (op === ',') {
|
|
421
|
+
let last = null
|
|
422
|
+
for (let i = 1; i < node.length; i++) { last = evalConst(node[i], env, state); if (!last) return null }
|
|
423
|
+
return last
|
|
424
|
+
}
|
|
425
|
+
return null
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function evalCallConst(node, env, state) {
|
|
429
|
+
const callee = node[1]
|
|
430
|
+
const args = collectArgs(node.length > 2 ? node[2] : null)
|
|
431
|
+
|
|
432
|
+
const mathName = mathCalleeName(callee)
|
|
433
|
+
if (mathName != null) {
|
|
434
|
+
const vs = []
|
|
435
|
+
for (const a of args) { const r = evalConst(a, env, state); if (!r || r.t !== 'num') return null; vs.push(r.v) }
|
|
436
|
+
return evalMathCall(mathName, vs)
|
|
437
|
+
}
|
|
438
|
+
if (Array.isArray(callee) && callee[0] === '.' && typeof callee[2] === 'string') {
|
|
439
|
+
const recv = evalConst(callee[1], env, state)
|
|
440
|
+
if (!recv || recv.t !== 'str' || !isAsciiSafe(recv.v)) return null
|
|
441
|
+
return evalStringMethod(callee[2], recv.v, args.map(a => evalConst(a, env, state)))
|
|
442
|
+
}
|
|
443
|
+
if (typeof callee === 'string' && (node.length < 3 || node[2] == null)) {
|
|
444
|
+
const f = state.funcByName.get(callee)
|
|
445
|
+
if (f && f.sig?.params?.length === 0 && !f.rest && !f.defaults) return evalFunctionBodyConst(f, state)
|
|
446
|
+
}
|
|
447
|
+
return null
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Zero-arg pure call collapse — subsumes IIFE collapse (see module doc). Evaluates the
|
|
451
|
+
* callee's OWN body in a fresh, empty env (no outer capture: purity stays trivially
|
|
452
|
+
* provable). Bails (returns null) on anything but a `let`/`const` chain of constants
|
|
453
|
+
* ending in exactly one `return`. Cycle-guarded for (mutual) self-recursive 0-arg calls. */
|
|
454
|
+
function evalFunctionBodyConst(f, state) {
|
|
455
|
+
if (state.evaluating.has(f.name)) return null
|
|
456
|
+
state.evaluating.add(f.name)
|
|
457
|
+
try { return evalBodyConst(f.body, new Map(), state) }
|
|
458
|
+
finally { state.evaluating.delete(f.name) }
|
|
459
|
+
}
|
|
460
|
+
function evalBodyConst(body, env, state) {
|
|
461
|
+
if (!Array.isArray(body)) return null
|
|
462
|
+
if (body[0] !== '{}' && body[0] !== ';') return evalConst(body, env, state)
|
|
463
|
+
return evalStmtsConst(blockToStmtArray(body), env, state)
|
|
464
|
+
}
|
|
465
|
+
function evalStmtsConst(stmts, env, state) {
|
|
466
|
+
const local = new Map(env)
|
|
467
|
+
for (const s of stmts) {
|
|
468
|
+
if (!Array.isArray(s)) return null
|
|
469
|
+
if (s[0] === 'let' || s[0] === 'const') {
|
|
470
|
+
for (let i = 1; i < s.length; i++) {
|
|
471
|
+
const d = s[i]
|
|
472
|
+
if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') return null
|
|
473
|
+
const v = evalConst(d[2], local, state)
|
|
474
|
+
if (!v) return null
|
|
475
|
+
local.set(d[1], v)
|
|
476
|
+
}
|
|
477
|
+
continue
|
|
478
|
+
}
|
|
479
|
+
if (s[0] === 'return') return s.length < 2 ? UNDEF_RESULT : evalConst(s[1], local, state)
|
|
480
|
+
return null // if/for/while/throw/expr-stmt/... — not fully constant, bail
|
|
481
|
+
}
|
|
482
|
+
return null // fell off the end without a return
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
// foldNode — the tree rewriter. Tries evalConst first (full reduction); falls
|
|
487
|
+
// back to structural per-child folding otherwise.
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
function collectParamNamesShallow(paramsNode) {
|
|
490
|
+
const names = []
|
|
491
|
+
for (const p of extractParams(paramsNode)) {
|
|
492
|
+
const c = classifyParam(p)
|
|
493
|
+
if (typeof c.name === 'string') names.push(c.name)
|
|
494
|
+
}
|
|
495
|
+
return names
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function foldNode(node, env, state) {
|
|
499
|
+
if (typeof node === 'string') {
|
|
500
|
+
const b = env.get(node)
|
|
501
|
+
return b !== undefined ? nodeOf(b) : node
|
|
502
|
+
}
|
|
503
|
+
if (!Array.isArray(node)) return node
|
|
504
|
+
const op = node[0]
|
|
505
|
+
if (op == null || op === 'str' || op === 'bool') return node
|
|
506
|
+
|
|
507
|
+
const full = evalConst(node, env, state)
|
|
508
|
+
if (full) return nodeOf(full)
|
|
509
|
+
|
|
510
|
+
if (op === '=>') {
|
|
511
|
+
const childEnv = new Map(env)
|
|
512
|
+
for (const p of collectParamNamesShallow(node[1])) childEnv.delete(p)
|
|
513
|
+
const newBody = foldNode(node[2], childEnv, state)
|
|
514
|
+
return newBody === node[2] ? node : [node[0], node[1], newBody]
|
|
515
|
+
}
|
|
516
|
+
if (op === 'for' && node.length === 5) {
|
|
517
|
+
// Leave the loop HEAD (init/cond/step — incl. for-in/for-of, already desugared to
|
|
518
|
+
// this shape by prepare) completely untouched: several downstream passes pattern-
|
|
519
|
+
// match a loop's bound/index expressions structurally (auto-vectorization's
|
|
520
|
+
// clamp-peel + 4-pixel SIMD blur match, unrollSmallConstFor's trip-count shape,
|
|
521
|
+
// watr LICM's post-inline invariant recognition, ...). Replacing a symbolic bound
|
|
522
|
+
// (`k <= rr`) with its folded literal (`k <= 4`) is VALUE-preserving but changes
|
|
523
|
+
// which of those shape-sensitive passes fires — e.g. it can silently swap a SIMD
|
|
524
|
+
// lane-vectorized loop for a fully unrolled one. The loop BODY has no such
|
|
525
|
+
// structural sensitivity and still folds normally.
|
|
526
|
+
const body = foldNode(node[4], new Map(env), state)
|
|
527
|
+
return body === node[4] ? node : [node[0], node[1], node[2], node[3], body]
|
|
528
|
+
}
|
|
529
|
+
if (op === 'return') {
|
|
530
|
+
if (node.length < 2) return node
|
|
531
|
+
const v = foldNode(node[1], env, state)
|
|
532
|
+
return v === node[1] ? node : ['return', v]
|
|
533
|
+
}
|
|
534
|
+
if (op === '&&' || op === '||' || op === '??') {
|
|
535
|
+
const a = foldNode(node[1], env, state)
|
|
536
|
+
const b = foldNode(node[2], env, state)
|
|
537
|
+
return (a === node[1] && b === node[2]) ? node : [op, a, b]
|
|
538
|
+
}
|
|
539
|
+
if ((op === '?:' || op === '?') && node.length === 4) {
|
|
540
|
+
const c = foldNode(node[1], env, state)
|
|
541
|
+
const t = foldNode(node[2], env, state)
|
|
542
|
+
const e = foldNode(node[3], env, state)
|
|
543
|
+
return (c === node[1] && t === node[2] && e === node[3]) ? node : [node[0], c, t, e]
|
|
544
|
+
}
|
|
545
|
+
if (op === '.' || op === '?.') {
|
|
546
|
+
const recv = foldNode(node[1], env, state)
|
|
547
|
+
return recv === node[1] ? node : [node[0], recv, node[2]]
|
|
548
|
+
}
|
|
549
|
+
if (op === ':' && node.length === 3) {
|
|
550
|
+
// Object-literal property `[':', key, value]`. A SHORTHAND property `{a}` desugars
|
|
551
|
+
// to `[':', 'a', 'a']` — the same bare identifier in BOTH the key slot and the value
|
|
552
|
+
// slot. Only the value is a real expression to fold/inline; the key slot is always a
|
|
553
|
+
// property NAME (or, for `{[k]: v}`, a computed-key expression) — inlining an
|
|
554
|
+
// identifier there would rewrite the property's NAME itself. Never touch it.
|
|
555
|
+
const key = node[1]
|
|
556
|
+
const value = foldNode(node[2], env, state)
|
|
557
|
+
return value === node[2] ? node : [op, key, value]
|
|
558
|
+
}
|
|
559
|
+
if (op === '[]' && node.length === 3) {
|
|
560
|
+
const base = foldNode(node[1], env, state)
|
|
561
|
+
// Never inline an identifier KEY to a literal here: prepare() already ran its
|
|
562
|
+
// static-vs-dynamic property/index classification (staticPropertyKey/staticIndexKey,
|
|
563
|
+
// static.js — schema dynProps tracking, SRoA flat-array slots) against the ORIGINAL
|
|
564
|
+
// `o[k]` shape and committed codegen decisions (e.g. for-in's dynamic-key bookkeeping)
|
|
565
|
+
// to that. Rewriting `k` to a literal post-hoc would make `o[k]` LOOK static to
|
|
566
|
+
// anything reading the AST downstream while every fact prepare recorded still says
|
|
567
|
+
// "dynamic" — a stale-vs-fresh mismatch, not a value-preserving fold. A non-identifier
|
|
568
|
+
// key (`o[i+1]`, `o[f()]`) was never eligible for that static fast path anyway, so it
|
|
569
|
+
// folds normally.
|
|
570
|
+
const key = typeof node[2] === 'string' ? node[2] : foldNode(node[2], env, state)
|
|
571
|
+
return (base === node[1] && key === node[2]) ? node : [op, base, key]
|
|
572
|
+
}
|
|
573
|
+
if (op === '()') return foldCallPartial(node, env, state)
|
|
574
|
+
if (op === ',') {
|
|
575
|
+
const parts = node.slice(1).map(n => foldNode(n, env, state))
|
|
576
|
+
return parts.some((p, i) => p !== node[i + 1]) ? [',', ...parts] : node
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Generic fallback: recurse into every child, preserving node shape. Covers
|
|
580
|
+
// for/array-literal/object-literal/call-args-of-non-foldable-callee/etc.
|
|
581
|
+
let changed = false
|
|
582
|
+
const out = node.map((c, i) => {
|
|
583
|
+
if (i === 0) return c
|
|
584
|
+
const v = foldNode(c, env, state)
|
|
585
|
+
if (v !== c) changed = true
|
|
586
|
+
return v
|
|
587
|
+
})
|
|
588
|
+
return changed ? out : node
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function foldCallPartial(node, env, state) {
|
|
592
|
+
const callee = Array.isArray(node[1]) ? foldNode(node[1], env, state) : node[1]
|
|
593
|
+
if (node.length < 3) return callee === node[1] ? node : [node[0], callee]
|
|
594
|
+
const rawArgs = collectArgs(node[2])
|
|
595
|
+
const args = rawArgs.map(a => foldNode(a, env, state))
|
|
596
|
+
if (callee === node[1] && args.every((a, i) => a === rawArgs[i])) return node
|
|
597
|
+
const newArgsNode = args.length === 0 ? null : args.length === 1 ? args[0] : [',', ...args]
|
|
598
|
+
return [node[0], callee, newArgsNode]
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ---------------------------------------------------------------------------
|
|
602
|
+
// Statement-list folding: env threading (constant `let`/`const` -> inlined at
|
|
603
|
+
// every later reference), `if`/`while(false)` dead-branch splicing.
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
function blockToStmtArray(node) {
|
|
606
|
+
if (node == null) return []
|
|
607
|
+
if (Array.isArray(node) && node[0] === '{}') {
|
|
608
|
+
const inner = node.length > 1 ? node[1] : null
|
|
609
|
+
if (inner == null) return []
|
|
610
|
+
return Array.isArray(inner) && inner[0] === ';' ? inner.slice(1) : [inner]
|
|
611
|
+
}
|
|
612
|
+
if (Array.isArray(node) && node[0] === ';') return node.slice(1)
|
|
613
|
+
return [node]
|
|
614
|
+
}
|
|
615
|
+
function wrapBlockLike(stmts, wasBraced) {
|
|
616
|
+
if (!stmts.length) return wasBraced ? ['{}'] : [';']
|
|
617
|
+
if (stmts.length === 1) return wasBraced ? ['{}', stmts[0]] : stmts[0]
|
|
618
|
+
return wasBraced ? ['{}', [';', ...stmts]] : [';', ...stmts]
|
|
619
|
+
}
|
|
620
|
+
const sameStmts = (a, b) => a.length === b.length && a.every((s, i) => s === b[i])
|
|
621
|
+
|
|
622
|
+
// IMPORTANT — identity preservation: prepare() forward-seeds compile-stage fact stores
|
|
623
|
+
// (program-facts.js's WeakMap caches, compile/infer.js recordGlobalRep, ...) keyed by
|
|
624
|
+
// the SPECIFIC node objects it walked. Rebuilding a statement/block node whose content
|
|
625
|
+
// didn't actually change would silently orphan any per-node fact recorded against the
|
|
626
|
+
// original object. `foldStmts` therefore returns the exact input array (and
|
|
627
|
+
// `foldBlockLike` the exact input node) whenever nothing in it changed, all the way up.
|
|
628
|
+
function foldBlockLike(node, env, state) {
|
|
629
|
+
const wasBraced = Array.isArray(node) && node[0] === '{}'
|
|
630
|
+
const original = blockToStmtArray(node)
|
|
631
|
+
const folded = foldStmts(original, env, state)
|
|
632
|
+
return folded === original ? node : wrapBlockLike(folded, wasBraced)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function foldStmts(stmts, env, state) {
|
|
636
|
+
const out = []
|
|
637
|
+
for (const s0 of stmts) {
|
|
638
|
+
const op = Array.isArray(s0) ? s0[0] : null
|
|
639
|
+
|
|
640
|
+
if (op === 'let' || op === 'const') {
|
|
641
|
+
// Fold each initializer's OWN expression (pure literal arithmetic/string/bool/
|
|
642
|
+
// Math chains need no outside binding to reduce). Deliberately NOT propagated any
|
|
643
|
+
// further: `env` here is never populated from a declaration, so a LATER reference
|
|
644
|
+
// to `name` is never rewritten to its value. Earlier revisions did that (and it's
|
|
645
|
+
// sound in isolation — see evalFunctionBodyConst's OWN, separately-scoped env,
|
|
646
|
+
// which still does this safely for a zero-arg call's self-contained body) but
|
|
647
|
+
// several existing passes downstream pattern-match a NAMED loop bound/index
|
|
648
|
+
// expression structurally rather than by value — clamp-peel's & the multi-pixel
|
|
649
|
+
// SIMD blur's loop-shape match, unrollSmallConstFor's trip-count shape, watr
|
|
650
|
+
// LICM's post-inline invariant recognition all fire (or don't) off the SYMBOLIC
|
|
651
|
+
// shape. Replacing `row = y*ww` with `row = y*64` is value-identical but silently
|
|
652
|
+
// swaps which of those passes engages. Tier 1 stays inside the proven-safe
|
|
653
|
+
// boundary: fold the expression tree, never rewrite a bare-name reference.
|
|
654
|
+
const decls = []
|
|
655
|
+
for (let i = 1; i < s0.length; i++) {
|
|
656
|
+
const d = s0[i]
|
|
657
|
+
if (!Array.isArray(d) || d[0] !== '=') { decls.push(d); continue }
|
|
658
|
+
const name = d[1], init = d[2]
|
|
659
|
+
const foldedInit = init !== undefined ? foldNode(init, env, state) : init
|
|
660
|
+
decls.push(foldedInit === init ? d : ['=', name, foldedInit])
|
|
661
|
+
}
|
|
662
|
+
const declsChanged = decls.some((d, i) => d !== s0[i + 1])
|
|
663
|
+
out.push(declsChanged ? [s0[0], ...decls] : s0)
|
|
664
|
+
continue
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (op === 'if') {
|
|
668
|
+
const condVal = evalConst(s0[1], env, state)
|
|
669
|
+
if (condVal) {
|
|
670
|
+
const takeThen = toBoolean(condVal)
|
|
671
|
+
const branch = takeThen ? s0[2] : s0[3]
|
|
672
|
+
if (branch != null) out.push(...foldStmts(blockToStmtArray(branch), new Map(env), state))
|
|
673
|
+
continue
|
|
674
|
+
}
|
|
675
|
+
const cond = foldNode(s0[1], env, state)
|
|
676
|
+
const thenF = s0[2] != null ? foldBlockLike(s0[2], new Map(env), state) : s0[2]
|
|
677
|
+
const hasElse = s0.length > 3
|
|
678
|
+
const elseF = hasElse ? (s0[3] != null ? foldBlockLike(s0[3], new Map(env), state) : s0[3]) : undefined
|
|
679
|
+
const changed = cond !== s0[1] || thenF !== s0[2] || (hasElse && elseF !== s0[3])
|
|
680
|
+
out.push(changed ? (hasElse ? ['if', cond, thenF, elseF] : ['if', cond, thenF]) : s0)
|
|
681
|
+
continue
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (op === 'while') {
|
|
685
|
+
const condVal = evalConst(s0[1], env, state)
|
|
686
|
+
if (condVal && !toBoolean(condVal)) continue
|
|
687
|
+
const cond = foldNode(s0[1], env, state)
|
|
688
|
+
const bodyF = s0[2] != null ? foldBlockLike(s0[2], new Map(env), state) : s0[2]
|
|
689
|
+
out.push(cond === s0[1] && bodyF === s0[2] ? s0 : ['while', cond, bodyF])
|
|
690
|
+
continue
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
out.push(foldNode(s0, env, state))
|
|
694
|
+
}
|
|
695
|
+
return sameStmts(out, stmts) ? stmts : out
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function foldFunctionBody(body, state) {
|
|
699
|
+
if (body == null || isLiteralNode(body)) return body
|
|
700
|
+
if (!Array.isArray(body) || (body[0] !== '{}' && body[0] !== ';')) return foldNode(body, new Map(), state)
|
|
701
|
+
return foldBlockLike(body, new Map(), state)
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** Run preEval over the prepared module AST + every ctx.func.list body (mutated in place —
|
|
705
|
+
* the same funcInfo objects compile() reads). Single top-to-bottom pass; see module doc for
|
|
706
|
+
* why that's already a full fixpoint. */
|
|
707
|
+
export function preEval(ast) {
|
|
708
|
+
const rationalOn = ctx.transform.optimize?.rationalConst !== false
|
|
709
|
+
const funcByName = new Map(ctx.func.list.map(f => [f.name, f]))
|
|
710
|
+
const state = { rationalOn, funcByName, evaluating: new Set() }
|
|
711
|
+
for (const f of ctx.func.list) f.body = foldFunctionBody(f.body, state)
|
|
712
|
+
if (ast == null) return ast
|
|
713
|
+
return foldBlockLike(ast, new Map(), state)
|
|
714
|
+
}
|