jz 0.5.1 → 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 +315 -318
- package/bench/README.md +369 -0
- package/bench/bench.svg +102 -0
- package/cli.js +104 -30
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +362 -84
- package/interop.js +159 -186
- 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 +184 -0
- package/module/array.js +377 -176
- package/module/collection.js +639 -145
- package/module/console.js +55 -43
- package/module/core.js +264 -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 +551 -187
- package/module/number.js +327 -60
- package/module/object.js +474 -184
- package/module/regex.js +255 -25
- package/module/schema.js +24 -6
- package/module/simd.js +117 -0
- package/module/string.js +621 -226
- package/module/symbol.js +1 -1
- package/module/timer.js +9 -14
- package/module/typedarray.js +459 -73
- package/package.json +58 -14
- package/src/abi/index.js +39 -23
- package/src/abi/string.js +38 -41
- package/src/ast.js +460 -0
- package/src/autoload.js +29 -24
- package/src/bridge.js +111 -0
- package/src/compile/analyze-scans.js +824 -0
- package/src/compile/analyze.js +1600 -0
- package/src/compile/emit-assign.js +411 -0
- package/src/compile/emit.js +3512 -0
- package/src/compile/flow-types.js +103 -0
- package/src/{compile.js → compile/index.js} +778 -128
- package/src/{infer.js → compile/infer.js} +62 -98
- package/src/compile/loop-divmod.js +155 -0
- package/src/{narrow.js → compile/narrow.js} +409 -106
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +370 -0
- package/src/compile/plan/common.js +150 -0
- package/src/compile/plan/index.js +122 -0
- package/src/compile/plan/inline.js +682 -0
- package/src/compile/plan/literals.js +1199 -0
- package/src/compile/plan/loops.js +472 -0
- package/src/compile/plan/scope.js +649 -0
- package/src/compile/program-facts.js +423 -0
- package/src/ctx.js +220 -64
- package/src/ir.js +589 -172
- package/src/kind-traits.js +132 -0
- package/src/kind.js +524 -0
- package/src/op-policy.js +57 -0
- package/src/{optimize.js → optimize/index.js} +1432 -473
- package/src/optimize/vectorize.js +4660 -0
- package/src/param-reps.js +65 -0
- package/src/parse.js +44 -0
- package/src/{prepare.js → prepare/index.js} +649 -205
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +116 -0
- package/src/resolve.js +12 -3
- package/src/static.js +208 -0
- package/src/type.js +651 -0
- package/src/{assemble.js → wat/assemble.js} +275 -55
- package/src/wat/optimize.js +3938 -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
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-scope planning — type-narrowing and structural rewrites of
|
|
3
|
+
* module-level bindings. Runs once per program (post fact-collection,
|
|
4
|
+
* pre whole-program narrowing) under `plan()`.
|
|
5
|
+
*
|
|
6
|
+
* Concerns owned here (each operates on `ctx.scope` / `ctx.func` / `ctx.schema`,
|
|
7
|
+
* not on AST shape — except `flattenFuncNamespaces` and `devirtGlobalCalls`,
|
|
8
|
+
* which mutate the AST as their final step):
|
|
9
|
+
*
|
|
10
|
+
* - `inferModuleLetTypes` — module-level `let` typed-array union
|
|
11
|
+
* - `unboxConstTypedGlobals` — const typed-array → unboxed i32 offset
|
|
12
|
+
* - `inferModuleIntGlobals` — purpose-focused f64→i32 numeric demotion
|
|
13
|
+
* - `flattenFuncNamespaces` — `f.prop` slot SROA + dead-write drop
|
|
14
|
+
* - `devirtGlobalCalls` — `call_indirect $global` → direct `call`
|
|
15
|
+
* - `materializeAutoBoxSchemas` — schema registration for object propMap
|
|
16
|
+
* - `resolveClosureWidth` — uniform closure ABI width
|
|
17
|
+
* - `canSkipWholeProgramNarrowing` — fast-path gate for monomorphic programs
|
|
18
|
+
*
|
|
19
|
+
* @module compile/plan/scope
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { ctx, warn, declGlobal } from '../../ctx.js'
|
|
23
|
+
import { ASSIGN_OPS, T, refsAny } from '../../ast.js'
|
|
24
|
+
import { VAL, updateGlobalRep } from '../../reps.js'
|
|
25
|
+
import { typedElemCtor, ternaryCtorOfRhs, MIXED_CTORS } from '../../type.js'
|
|
26
|
+
import { typedElemAux } from '../../../layout.js'
|
|
27
|
+
import { MAX_CLOSURE_ARITY, UNDEF_NAN } from '../../ir.js'
|
|
28
|
+
import { analyzeFuncNamespaces } from '../analyze.js'
|
|
29
|
+
import { invalidateProgramFactsCache } from '../program-facts.js'
|
|
30
|
+
|
|
31
|
+
// `scanGlobalValueFacts` was deleted — prepare's depth-0 catch (calling
|
|
32
|
+
// `recordGlobalRep` from src/infer.js) is the authoritative pass and a
|
|
33
|
+
// strict superset of what this top-level walker observed.
|
|
34
|
+
|
|
35
|
+
// Flow-insensitive type inference for module-level `let` bindings whose
|
|
36
|
+
// initial RHS doesn't pin a type (most often `let mem;` followed later by
|
|
37
|
+
// `mem = new TypedArray(...)` inside an init function). Without this the
|
|
38
|
+
// read site has to runtime-check the NaN-box tag on every access — game-of-life's
|
|
39
|
+
// inner step does that 9× per cell, blowing up the hot loop. We union RHS types
|
|
40
|
+
// across every assignment (initial decl + every `name = …` in any function);
|
|
41
|
+
// if every observed RHS is either a typed-array ctor of the same kind, a known
|
|
42
|
+
// VAL.TYPED binding of the same ctor, or null/undefined, the binding is
|
|
43
|
+
// monomorphically VAL.TYPED. Anything else (literal number, non-typed call,
|
|
44
|
+
// mixed ctors) clears the candidacy, keeping the read site polymorphic.
|
|
45
|
+
export const inferModuleLetTypes = (ast) => {
|
|
46
|
+
if (!ctx.scope.userGlobals) return
|
|
47
|
+
// Build an assignment/alias graph over EVERY `=`/`let`/`const` binding in the
|
|
48
|
+
// program (globals and locals alike), then resolve each global's typed-array
|
|
49
|
+
// ctor by least-fixed-point. A single forward pass can't see the double-buffer
|
|
50
|
+
// swap idiom — `let tmp = a; a = b; b = tmp` assigns `a` from `b` and `b` from
|
|
51
|
+
// a local `tmp` that aliases `a`, so neither ref resolves until its sibling is
|
|
52
|
+
// already known. The fixpoint closes that cycle: `a`/`b` each anchor on their
|
|
53
|
+
// `new Float64Array(...)` decl, the alias edges carry the ctor around the loop,
|
|
54
|
+
// and they promote to VAL.TYPED. Without it the swap poisoned both globals and
|
|
55
|
+
// every `a[i]` read forked __str_idx/__typed_idx, every `+` forked __str_concat.
|
|
56
|
+
//
|
|
57
|
+
// Lattice (per name): null (no evidence) < ctor < MIXED. `bad` evidence (a non-
|
|
58
|
+
// typed, non-alias RHS — number, string, call, arithmetic, compound-assign) jumps
|
|
59
|
+
// straight to MIXED; conflicting ctors join to MIXED. We promote a global only
|
|
60
|
+
// when its fixed point is a single concrete ctor — sound: every assignment then
|
|
61
|
+
// provably yields that typed-array kind or nullish.
|
|
62
|
+
const MIXED = MIXED_CTORS
|
|
63
|
+
const defs = new Map() // name → { ctors:Set<string>, refs:Set<string>, bad:bool }
|
|
64
|
+
const getDef = (name) => {
|
|
65
|
+
let d = defs.get(name)
|
|
66
|
+
if (!d) defs.set(name, d = { ctors: new Set(), refs: new Set(), bad: false })
|
|
67
|
+
return d
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const isNullishLit = (e) => e == null || e === 'undefined' || e === 'null'
|
|
71
|
+
|| (Array.isArray(e) && e[0] == null && (e[1] === undefined || e[1] === null))
|
|
72
|
+
|
|
73
|
+
// User-function names — a call to one is an alias edge to its return value
|
|
74
|
+
// (virtual node `@ret:<fn>`, populated from each `return`). Lets a global
|
|
75
|
+
// assigned `a = makeBuffer(n)` inherit makeBuffer's typed-array ctor without
|
|
76
|
+
// relying on the call being inlined (locals get it via inlining; globals,
|
|
77
|
+
// typed before inlining runs, did not). `@`/`:` can't occur in a JS identifier,
|
|
78
|
+
// so the virtual key never collides with a real binding.
|
|
79
|
+
const fnames = new Set()
|
|
80
|
+
for (const f of ctx.func.list) if (f.body && !f.raw && typeof f.name === 'string') fnames.add(f.name)
|
|
81
|
+
// Typed-array methods that preserve the receiver's element ctor: `.subarray`
|
|
82
|
+
// and `.slice` (same-kind view/copy), `.map` (same-kind, per propagateTyped).
|
|
83
|
+
const CTOR_PRESERVING = new Set(['subarray', 'slice', 'map'])
|
|
84
|
+
|
|
85
|
+
// Record one assignment `name = rhs` as evidence. Nullish contributes nothing
|
|
86
|
+
// (consistent with any typed-array value); a bare identifier, a ctor-preserving
|
|
87
|
+
// method on a name, or a call to a user function are alias edges; anything else
|
|
88
|
+
// that isn't a typed ctor poisons the name.
|
|
89
|
+
// Scope-qualified binding key. A module global is ONE node program-wide (bare
|
|
90
|
+
// name); a function-local is unique to its scope `sid`. Keying locals by bare
|
|
91
|
+
// name made a numeric counter `let s = 0` in one function poison a typed
|
|
92
|
+
// swap-temp `let s = a` in another — cascading MIXED into the double-buffer
|
|
93
|
+
// globals so every `a[i]` fell back to runtime __str_idx/__typed_idx dispatch
|
|
94
|
+
// (lbm: 3.9× slower than JS). `@ret:` virtual nodes stay bare (module-wide
|
|
95
|
+
// return-value anchors).
|
|
96
|
+
const key = (name, sid) => name[0] === '@' || ctx.scope.userGlobals.has(name) ? name : sid + '\x00' + name
|
|
97
|
+
|
|
98
|
+
const observe = (name, rhs, sid) => {
|
|
99
|
+
const d = getDef(key(name, sid))
|
|
100
|
+
if (isNullishLit(rhs)) return
|
|
101
|
+
const ctor = typedElemCtor(rhs) ?? ternaryCtorOfRhs(rhs)
|
|
102
|
+
if (ctor === MIXED) { d.bad = true; return }
|
|
103
|
+
if (ctor) { d.ctors.add(ctor); return }
|
|
104
|
+
if (typeof rhs === 'string') { d.refs.add(key(rhs, sid)); return }
|
|
105
|
+
if (Array.isArray(rhs) && rhs[0] === '()') {
|
|
106
|
+
const callee = rhs[1]
|
|
107
|
+
// `recv.subarray(...)` / `recv.slice(...)` / `recv.map(...)` → inherit recv's ctor.
|
|
108
|
+
if (Array.isArray(callee) && callee[0] === '.' && typeof callee[1] === 'string'
|
|
109
|
+
&& CTOR_PRESERVING.has(callee[2])) { d.refs.add(key(callee[1], sid)); return }
|
|
110
|
+
// `fn(...)` to a user function → inherit its return ctor.
|
|
111
|
+
if (typeof callee === 'string' && fnames.has(callee)) { d.refs.add('@ret:' + callee); return }
|
|
112
|
+
}
|
|
113
|
+
d.bad = true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Scope-aware walk. Every `=>` opens a fresh scope so same-named locals across
|
|
117
|
+
// functions (and sibling closures) stay distinct. A function bound to a name
|
|
118
|
+
// descends in a name-stable scope (`fn\0name`) so the ast descent and the
|
|
119
|
+
// func.list sweep below visit it identically (idempotent), and so `return`
|
|
120
|
+
// exprs anchor on `@ret:name`.
|
|
121
|
+
let sidc = 0
|
|
122
|
+
const walk = (node, sid, retFn) => {
|
|
123
|
+
if (!Array.isArray(node)) return
|
|
124
|
+
const op = node[0]
|
|
125
|
+
if (op === '=>') { walk(node[2], 's' + (++sidc), null); return }
|
|
126
|
+
if (op === 'return' && retFn != null) observe('@ret:' + retFn, node[1], sid)
|
|
127
|
+
const assign = (name, rhs) => {
|
|
128
|
+
if (Array.isArray(rhs) && rhs[0] === '=>') { observe(name, rhs, sid); enterFn(rhs[2], name); return }
|
|
129
|
+
observe(name, rhs, sid); walk(rhs, sid, retFn)
|
|
130
|
+
}
|
|
131
|
+
if (op === '=' && typeof node[1] === 'string') return assign(node[1], node[2])
|
|
132
|
+
if ((op === 'let' || op === 'const') && node.length > 1) {
|
|
133
|
+
for (let i = 1; i < node.length; i++) {
|
|
134
|
+
const d = node[i]
|
|
135
|
+
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') assign(d[1], d[2])
|
|
136
|
+
else walk(d, sid, retFn)
|
|
137
|
+
}
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
// Compound-assigns (`+=`, `++`, …) can't preserve a typed-array kind — poison.
|
|
141
|
+
if (ASSIGN_OPS.has(op) && typeof node[1] === 'string') { getDef(key(node[1], sid)).bad = true; walk(node[2], sid, retFn); return }
|
|
142
|
+
for (let i = 1; i < node.length; i++) walk(node[i], sid, retFn)
|
|
143
|
+
}
|
|
144
|
+
// Descend into a function body anchored on `@ret:fn`. An arrow expr-body IS the
|
|
145
|
+
// implicit return (`(n) => new Float64Array(n)`); a `{}` block uses explicit
|
|
146
|
+
// `return` nodes (captured in walk). Without the implicit-return capture, a
|
|
147
|
+
// global assigned `a = mk(n)` from an expr-body fn never inherited mk's ctor.
|
|
148
|
+
const enterFn = (body, fn) => {
|
|
149
|
+
if (Array.isArray(body) && body[0] !== '{}') observe('@ret:' + fn, body, 'fn\x00' + fn)
|
|
150
|
+
walk(body, 'fn\x00' + fn, fn)
|
|
151
|
+
}
|
|
152
|
+
walk(ast, 'mod', null)
|
|
153
|
+
// Defensive sweep: cover any func.list body not reachable by descent from `ast`
|
|
154
|
+
// (hoisted / submodule). Name-stable scope keeps it idempotent with the descent.
|
|
155
|
+
for (const f of ctx.func.list) if (f.body && !f.raw) enterFn(f.body, f.name)
|
|
156
|
+
|
|
157
|
+
// Least-fixed-point over the alias graph. join: null is bottom, MIXED is top.
|
|
158
|
+
const join = (a, b) => a === MIXED || b === MIXED ? MIXED : a == null ? b : b == null ? a : a === b ? a : MIXED
|
|
159
|
+
const state = new Map() // name → null | ctor | MIXED
|
|
160
|
+
// A ref to a name with no tracked defs resolves via an already-known typed
|
|
161
|
+
// global (const typed array / earlier-recorded rep); otherwise it's opaque → MIXED.
|
|
162
|
+
const refState = (r) => defs.has(r) ? (state.get(r) ?? null)
|
|
163
|
+
: ctx.scope.globalValTypes?.get(r) === VAL.TYPED ? (ctx.scope.globalTypedElem?.get(r) ?? MIXED)
|
|
164
|
+
: MIXED
|
|
165
|
+
let changed = true
|
|
166
|
+
while (changed) {
|
|
167
|
+
changed = false
|
|
168
|
+
for (const [name, d] of defs) {
|
|
169
|
+
let cur = d.bad ? MIXED : null
|
|
170
|
+
if (cur !== MIXED) for (const c of d.ctors) cur = join(cur, c)
|
|
171
|
+
if (cur !== MIXED) for (const r of d.refs) cur = join(cur, refState(r))
|
|
172
|
+
if (cur !== (state.get(name) ?? null)) { state.set(name, cur); changed = true }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const name of ctx.scope.userGlobals) {
|
|
177
|
+
const ctor = state.get(name)
|
|
178
|
+
if (!ctor || ctor === MIXED) continue
|
|
179
|
+
if (ctx.scope.globalValTypes?.get(name) === VAL.TYPED) continue
|
|
180
|
+
;(ctx.scope.globalValTypes ||= new Map()).set(name, VAL.TYPED)
|
|
181
|
+
;(ctx.scope.globalTypedElem ||= new Map()).set(name, ctor)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export const unboxConstTypedGlobals = () => {
|
|
186
|
+
if (!ctx.scope.globalTypedElem || !ctx.scope.consts) return
|
|
187
|
+
for (const [name, ctor] of ctx.scope.globalTypedElem) {
|
|
188
|
+
if (!ctx.scope.consts.has(name)) continue
|
|
189
|
+
if (ctx.scope.globalValTypes?.get(name) !== VAL.TYPED) continue
|
|
190
|
+
const aux = typedElemAux(ctor)
|
|
191
|
+
if (aux == null) continue
|
|
192
|
+
const decl = ctx.scope.globals.get(name)
|
|
193
|
+
if (!(decl?.mut && decl.type === 'f64')) continue
|
|
194
|
+
declGlobal(name, 'i32')
|
|
195
|
+
updateGlobalRep(name, { ptrKind: VAL.TYPED, ptrAux: aux })
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Integer-global type inference — narrow purpose-focused numeric module globals
|
|
200
|
+
// (counters, sizes, strides, indices: `N`, `width`, `offset`, …) from f64 to i32.
|
|
201
|
+
//
|
|
202
|
+
// Principle: in purpose-focused code an integer-initialized numeric global is an
|
|
203
|
+
// integer unless an assignment *proves* it fractional. Sizes/strides/indices are
|
|
204
|
+
// the overwhelming majority; demanding the user annotate them (asm.js `x | 0`)
|
|
205
|
+
// defeats clean code. So we assume i32 and demote only on positive proof of a
|
|
206
|
+
// fraction — a non-integer literal, `/` or `**`, a float-valued `Math.*`, or a
|
|
207
|
+
// reference to an already-fractional value. (jz already truncates fractional
|
|
208
|
+
// array indices, so a stray fraction in an integer slot is a pre-existing bug,
|
|
209
|
+
// not one this introduces; a future advisory can flag it.)
|
|
210
|
+
//
|
|
211
|
+
// The payoff cascades: an i32 `width` makes `mem[y*width+x]` a fully-i32 index
|
|
212
|
+
// (the per-access `trunc_sat` and the index-counter widen both vanish), and an
|
|
213
|
+
// i32 `N` makes the loop guard `i < N` pure-i32 (no per-iteration convert),
|
|
214
|
+
// unlocking SIMD — all from idiomatic source, no hints.
|
|
215
|
+
const FRACTIONAL_MATH = new Set([
|
|
216
|
+
'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2',
|
|
217
|
+
'sinh', 'cosh', 'tanh', 'asinh', 'acosh', 'atanh',
|
|
218
|
+
'sqrt', 'cbrt', 'exp', 'expm1', 'log', 'log2', 'log10', 'log1p',
|
|
219
|
+
'pow', 'hypot', 'random', 'fround',
|
|
220
|
+
])
|
|
221
|
+
const INT_COERCE_OPS = new Set(['&', '|', '^', '<<', '>>', '>>>', '~'])
|
|
222
|
+
const COMPARE_OPS = new Set(['<', '>', '<=', '>=', '==', '===', '!=', '!==', '!', 'in', 'instanceof'])
|
|
223
|
+
const FRAC_COMPOUND = new Set(['/=', '**='])
|
|
224
|
+
const INT_COMPOUND = new Set(['&=', '|=', '^=', '<<=', '>>=', '>>>='])
|
|
225
|
+
|
|
226
|
+
export const inferModuleIntGlobals = (ast) => {
|
|
227
|
+
if (!ctx.scope.userGlobals?.size) return
|
|
228
|
+
// Candidates: mutable f64 scalar globals with positive numeric-initializer
|
|
229
|
+
// evidence and not a function. (const-folded / typed-pointer globals already
|
|
230
|
+
// carry a non-`(mut f64)` decl, so they're excluded.)
|
|
231
|
+
const candidates = new Set()
|
|
232
|
+
for (const name of ctx.scope.userGlobals) {
|
|
233
|
+
const decl = ctx.scope.globals.get(name)
|
|
234
|
+
if (!(decl?.mut && decl.type === 'f64')) continue
|
|
235
|
+
if (ctx.scope.globalValTypes?.get(name) !== VAL.NUMBER) continue
|
|
236
|
+
if (ctx.func.names?.has(name)) continue
|
|
237
|
+
candidates.add(name)
|
|
238
|
+
}
|
|
239
|
+
if (!candidates.size) return
|
|
240
|
+
|
|
241
|
+
const fractional = new Set()
|
|
242
|
+
const refIsFractional = (ref) => {
|
|
243
|
+
if (candidates.has(ref)) return fractional.has(ref)
|
|
244
|
+
const gt = ctx.scope.globalTypes?.get(ref)
|
|
245
|
+
if (gt === 'i32') return false
|
|
246
|
+
if (gt === 'f64') {
|
|
247
|
+
const vt = ctx.scope.globalValTypes?.get(ref)
|
|
248
|
+
return vt === VAL.NUMBER || vt == null // a fractional f64 number; pointers aren't
|
|
249
|
+
}
|
|
250
|
+
return false // param / local / unknown numeric → assume integer
|
|
251
|
+
}
|
|
252
|
+
// Does `e` provably evaluate to a non-integer? Integer-coercing ops (bitwise,
|
|
253
|
+
// shifts) and comparisons launder any fraction; only the *value*-bearing
|
|
254
|
+
// branches of ternary/logical ops carry it.
|
|
255
|
+
const producesFraction = (e) => {
|
|
256
|
+
if (e == null) return false
|
|
257
|
+
if (typeof e === 'number') return !Number.isInteger(e)
|
|
258
|
+
if (typeof e === 'string') return refIsFractional(e)
|
|
259
|
+
if (!Array.isArray(e)) return false
|
|
260
|
+
const op = e[0]
|
|
261
|
+
if (op == null) return typeof e[1] === 'number' && !Number.isInteger(e[1])
|
|
262
|
+
if (op === '/' || op === '**') return true
|
|
263
|
+
if (INT_COERCE_OPS.has(op) || COMPARE_OPS.has(op)) return false
|
|
264
|
+
if (op === '?:') return producesFraction(e[2]) || producesFraction(e[3])
|
|
265
|
+
if (op === '&&' || op === '||' || op === '??') return producesFraction(e[1]) || producesFraction(e[2])
|
|
266
|
+
if (op === '()') {
|
|
267
|
+
const callee = e[1]
|
|
268
|
+
if (Array.isArray(callee) && callee[0] === '?') return producesFraction(callee[2]) || producesFraction(callee[3])
|
|
269
|
+
if (Array.isArray(callee) && callee[0] === '.' && callee[1] === 'Math' && FRACTIONAL_MATH.has(callee[2])) return true
|
|
270
|
+
return false // unknown call → assume integer
|
|
271
|
+
}
|
|
272
|
+
for (let i = 1; i < e.length; i++) if (producesFraction(e[i])) return true
|
|
273
|
+
return false
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// A numeric-initialized global later assigned a provably non-numeric value
|
|
277
|
+
// (string/object/array/arrow/`new`/boolean literal) must stay the f64 NaN-box
|
|
278
|
+
// carrier — narrowing it to i32 would corrupt the boxed value. Disqualify it.
|
|
279
|
+
const looksNonNumeric = (e) => {
|
|
280
|
+
if (!Array.isArray(e)) return false
|
|
281
|
+
const op = e[0]
|
|
282
|
+
if (op == null) { const v = e[1]; return typeof v === 'string' || typeof v === 'boolean' }
|
|
283
|
+
// `[` is prepare's array-literal form; `[]` length-2 is the raw (pre-prepare) one.
|
|
284
|
+
return op === '{}' || op === '[' || (op === '[]' && e.length === 2) || op === '=>' || op === 'new' || op === 'str' || op === '`'
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Collect every assignment RHS (init + reassignments, program-wide). `fromParam`
|
|
288
|
+
// records (global → the function it was assigned a parameter-derived value in) —
|
|
289
|
+
// a parameter is an f64 of unknown integrality, so an i32-narrowed global fed from
|
|
290
|
+
// one may silently truncate a fractional Number (DSP/filter state). We do NOT
|
|
291
|
+
// demote on this (the integer default is the load-bearing index/size perf win);
|
|
292
|
+
// we surface it on the opt-in warn channel below.
|
|
293
|
+
const rhsByName = new Map()
|
|
294
|
+
const fromParam = new Map()
|
|
295
|
+
for (const name of candidates) rhsByName.set(name, [])
|
|
296
|
+
const record = (name, rhs, scope) => {
|
|
297
|
+
if (!candidates.has(name)) return
|
|
298
|
+
if (looksNonNumeric(rhs)) { candidates.delete(name); rhsByName.delete(name); return }
|
|
299
|
+
rhsByName.get(name)?.push(rhs)
|
|
300
|
+
if (scope && !fromParam.has(name) && refsAny(rhs, scope.params, { skipBindingPositions: true }))
|
|
301
|
+
fromParam.set(name, scope.fn)
|
|
302
|
+
}
|
|
303
|
+
const walk = (node, scope) => {
|
|
304
|
+
if (!Array.isArray(node)) return
|
|
305
|
+
const op = node[0]
|
|
306
|
+
if (op === '=' && typeof node[1] === 'string') record(node[1], node[2], scope)
|
|
307
|
+
else if ((op === 'let' || op === 'const') && node.length > 1) {
|
|
308
|
+
for (let i = 1; i < node.length; i++) {
|
|
309
|
+
const d = node[i]
|
|
310
|
+
if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') record(d[1], d[2], scope)
|
|
311
|
+
}
|
|
312
|
+
} else if (ASSIGN_OPS.has(op) && op !== '=' && typeof node[1] === 'string' && candidates.has(node[1])) {
|
|
313
|
+
if (FRAC_COMPOUND.has(op)) fractional.add(node[1]) // `/=`, `**=` → fractional outright
|
|
314
|
+
else if (!INT_COMPOUND.has(op)) record(node[1], node[2], scope) // `+= -= *= %= ||= &&= ??=` → as their rhs
|
|
315
|
+
}
|
|
316
|
+
for (let i = 1; i < node.length; i++) walk(node[i], scope)
|
|
317
|
+
}
|
|
318
|
+
walk(ast, null)
|
|
319
|
+
for (const f of ctx.func.list) {
|
|
320
|
+
if (!f.body || f.raw) continue
|
|
321
|
+
const params = new Set((f.sig?.params || []).map(p => p.name))
|
|
322
|
+
walk(f.body, params.size ? { params, fn: f.name } : null)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Fixpoint: demote any candidate with a provably-fractional assignment; repeat
|
|
326
|
+
// so fractionality propagates through globals that reference each other.
|
|
327
|
+
let changed = true
|
|
328
|
+
while (changed) {
|
|
329
|
+
changed = false
|
|
330
|
+
for (const name of candidates) {
|
|
331
|
+
if (fractional.has(name)) continue
|
|
332
|
+
if (rhsByName.get(name).some(producesFraction)) { fractional.add(name); changed = true }
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (const name of candidates) {
|
|
337
|
+
if (fractional.has(name)) continue
|
|
338
|
+
declGlobal(name, 'i32')
|
|
339
|
+
// Advisory only (off unless opts.warnings): the value flows in from a parameter,
|
|
340
|
+
// which may be a fractional Number that the i32 carrier truncates.
|
|
341
|
+
if (ctx.warnings && fromParam.has(name))
|
|
342
|
+
warn('int-global-truncation',
|
|
343
|
+
`module global '${name}' is inferred i32 (integer) but is assigned from a parameter — if it can hold a fractional Number (e.g. DSP/filter state), the fraction is truncated; store fractional state in a Float64Array instead`,
|
|
344
|
+
{ fn: fromParam.get(name) })
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Function-namespace scalar replacement + devirtualization.
|
|
350
|
+
*
|
|
351
|
+
* A property of a user function compiles, by default, as a dynamic object: each
|
|
352
|
+
* `f.prop` write is a `__dyn_set` into a closure-keyed hash side-table, each
|
|
353
|
+
* read a `__dyn_get`. But a function's property table can never be observed by
|
|
354
|
+
* the host (the host receives only the callable; the table lives in jz linear
|
|
355
|
+
* memory), so jz sees every `f.prop` site — the slot is a closed, fully-known
|
|
356
|
+
* cell. Per property of a non-escaping namespace:
|
|
357
|
+
*
|
|
358
|
+
* - reassigned (`multiProp`) slot → dissolve into a plain f64 module global:
|
|
359
|
+
* `__dyn_get/__dyn_set` → `global.get/global.set`. The indirect call stays
|
|
360
|
+
* (a genuinely reassigned function pointer needs `call_indirect`). Pure
|
|
361
|
+
* storage relocation: the global inits to the undefined NaN atom, exactly mirroring
|
|
362
|
+
* "key never set → __dyn_get yields undefined".
|
|
363
|
+
* - written once to its lifted `$f$prop` function and only ever *called*
|
|
364
|
+
* (never read as a value) → the `__dyn_set` is dead: emit already lowers
|
|
365
|
+
* `f.prop()` to a direct `call $f$prop`. Drop the write entirely.
|
|
366
|
+
*
|
|
367
|
+
* Disqualified namespaces (`f` escapes as a bare value / is computed-indexed —
|
|
368
|
+
* an alias could reach the table) keep the dynamic path. Together these can
|
|
369
|
+
* eliminate the `__dyn_*` machinery from a namespace-only program outright.
|
|
370
|
+
*/
|
|
371
|
+
export const flattenFuncNamespaces = (ast) => {
|
|
372
|
+
const names = ctx.func.names
|
|
373
|
+
if (!names?.size) return false
|
|
374
|
+
// Cheap structural gate: a flattenable namespace exists only if some lifted
|
|
375
|
+
// `f$prop` name's `f` is itself a function (prepare lifts every `f.prop =
|
|
376
|
+
// arrow` — multiProp slots included). The base `f` may itself carry a module
|
|
377
|
+
// prefix (`mod$f`), so scan every `$` boundary, not just the first; a
|
|
378
|
+
// populated `multiProp` registry is itself a direct namespace witness.
|
|
379
|
+
let hasNs = ctx.func.multiProp.size > 0
|
|
380
|
+
if (!hasNs) outer: for (const n of names) {
|
|
381
|
+
for (let i = n.indexOf('$'); i > 0; i = n.indexOf('$', i + 1))
|
|
382
|
+
if (names.has(n.slice(0, i))) { hasNs = true; break outer }
|
|
383
|
+
}
|
|
384
|
+
if (!hasNs) return false
|
|
385
|
+
const ns = analyzeFuncNamespaces(ast)
|
|
386
|
+
if (!ns.size) return false
|
|
387
|
+
// f → Map<prop, decision>; decision is { global } (SROA) or { drop } (dead
|
|
388
|
+
// write to an only-called single-write slot).
|
|
389
|
+
const flat = new Map()
|
|
390
|
+
for (const [f, info] of ns) {
|
|
391
|
+
if (info.disq) continue
|
|
392
|
+
let decide
|
|
393
|
+
const plan = (prop, d) => { if (!decide) flat.set(f, decide = new Map()); decide.set(prop, d) }
|
|
394
|
+
for (const prop of info.props) {
|
|
395
|
+
if (ctx.func.multiProp.has(`${f}.${prop}`)) { plan(prop, { global: `${f}${T}${prop}` }); continue }
|
|
396
|
+
const w = info.writes.get(prop)
|
|
397
|
+
// Single write of the lifted `$f$prop`, never read as a value → drop it.
|
|
398
|
+
if (w && w.length === 1 && w[0].atInit && w[0].rhs === `${f}$${prop}` && !info.valRead.has(prop))
|
|
399
|
+
plan(prop, { drop: true })
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (!flat.size) return false
|
|
403
|
+
for (const decide of flat.values())
|
|
404
|
+
for (const d of decide.values())
|
|
405
|
+
if (d.global && !ctx.scope.globals.has(d.global)) {
|
|
406
|
+
declGlobal(d.global, 'f64', `nan:${UNDEF_NAN}`)
|
|
407
|
+
}
|
|
408
|
+
const decisionFor = (obj, prop) =>
|
|
409
|
+
typeof obj === 'string' && typeof prop === 'string' && flat.has(obj)
|
|
410
|
+
? flat.get(obj).get(prop) : undefined
|
|
411
|
+
const isEmptySeq = (n) => Array.isArray(n) && n.length === 1 && n[0] === ';'
|
|
412
|
+
const rewrite = (node) => {
|
|
413
|
+
if (!Array.isArray(node)) return node
|
|
414
|
+
const op = node[0]
|
|
415
|
+
if (op === '.' || op === '?.') {
|
|
416
|
+
const d = decisionFor(node[1], node[2])
|
|
417
|
+
if (d?.global) return d.global // drop-decisions leave reads/calls alone
|
|
418
|
+
}
|
|
419
|
+
if (op === '=' && Array.isArray(node[1]) && (node[1][0] === '.' || node[1][0] === '?.')) {
|
|
420
|
+
const d = decisionFor(node[1][1], node[1][2])
|
|
421
|
+
if (d?.global) return ['=', d.global, rewrite(node[2])]
|
|
422
|
+
if (d?.drop) return [';'] // dead write — emit nothing
|
|
423
|
+
}
|
|
424
|
+
const out = [op]
|
|
425
|
+
// Filter dropped writes out of statement sequences (an empty `[';']` left in
|
|
426
|
+
// a body would lower to an unrenderable node).
|
|
427
|
+
for (let i = 1; i < node.length; i++) {
|
|
428
|
+
const c = rewrite(node[i])
|
|
429
|
+
if (op === ';' && isEmptySeq(c)) continue
|
|
430
|
+
out.push(c)
|
|
431
|
+
}
|
|
432
|
+
return out
|
|
433
|
+
}
|
|
434
|
+
const newAst = rewrite(ast)
|
|
435
|
+
ast.length = 0
|
|
436
|
+
for (let i = 0; i < newAst.length; i++) ast.push(newAst[i])
|
|
437
|
+
invalidateProgramFactsCache(ast)
|
|
438
|
+
for (const fn of ctx.func.list)
|
|
439
|
+
if (fn.body && !fn.raw) fn.body = rewrite(fn.body)
|
|
440
|
+
// The defining `f.prop = …` writes live in moduleInits for bundled programs —
|
|
441
|
+
// rewrite them too, or reads would resolve to an unwritten global.
|
|
442
|
+
if (ctx.module.moduleInits)
|
|
443
|
+
for (let i = 0; i < ctx.module.moduleInits.length; i++)
|
|
444
|
+
ctx.module.moduleInits[i] = rewrite(ctx.module.moduleInits[i])
|
|
445
|
+
return true
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Closure devirtualization.
|
|
450
|
+
*
|
|
451
|
+
* `flattenFuncNamespaces` dissolves a reassigned `f.prop` function slot into a
|
|
452
|
+
* module global, but the call through it stays a `call_indirect` on a
|
|
453
|
+
* `global.get`, dispatched via an ABI-adapting trampoline. When that global is
|
|
454
|
+
* written *only* by unconditional module-init assignments it holds, for the
|
|
455
|
+
* entire post-init program, one statically-known function — so every call
|
|
456
|
+
* through it collapses to a direct `call`: no table lookup, no trampoline, no
|
|
457
|
+
* 8-wide padding ABI, no closure type guard.
|
|
458
|
+
*
|
|
459
|
+
* A global G qualifies iff:
|
|
460
|
+
* 1. every assignment to G is an unconditional module-init statement — none in
|
|
461
|
+
* a function body, none nested inside init control flow;
|
|
462
|
+
* 2. G's final init value resolves (through global aliases) to a top-level
|
|
463
|
+
* function F;
|
|
464
|
+
* 3. G is never *called* by module-init code, nor by any function reachable
|
|
465
|
+
* from it — so every call site runs strictly post-init, where G ≡ F.
|
|
466
|
+
* Devirt then only swaps an indirect call for a direct call to the very same
|
|
467
|
+
* callee: it cannot change behavior, only drop dispatch overhead. The result is
|
|
468
|
+
* recorded in `ctx.func.globalDevirt` (`Map<global, fn>`) and consumed by emit.
|
|
469
|
+
*/
|
|
470
|
+
export const devirtGlobalCalls = (ast) => {
|
|
471
|
+
const fnNames = ctx.func.names
|
|
472
|
+
if (!fnNames?.size || !ctx.scope.globals?.size) return
|
|
473
|
+
|
|
474
|
+
// Module-init statement stream, in execution order: moduleInits run first in
|
|
475
|
+
// `$__start`, then the main module's top-level.
|
|
476
|
+
const initStmts = []
|
|
477
|
+
const flatten = (n) => {
|
|
478
|
+
if (Array.isArray(n) && n[0] === ';') for (let i = 1; i < n.length; i++) flatten(n[i])
|
|
479
|
+
else if (n != null) initStmts.push(n)
|
|
480
|
+
}
|
|
481
|
+
for (const mi of ctx.module.moduleInits || []) flatten(mi)
|
|
482
|
+
flatten(ast)
|
|
483
|
+
|
|
484
|
+
const isGlobal = (s) => typeof s === 'string' && ctx.scope.globals.has(s)
|
|
485
|
+
// `[target, rhs]` pairs for a `=` / `let` / `const` node assigning a global.
|
|
486
|
+
const writesOf = (node) => {
|
|
487
|
+
if (!Array.isArray(node)) return []
|
|
488
|
+
if (node[0] === '=' && isGlobal(node[1])) return [[node[1], node[2]]]
|
|
489
|
+
if (node[0] === 'let' || node[0] === 'const') {
|
|
490
|
+
const out = []
|
|
491
|
+
for (let i = 1; i < node.length; i++) {
|
|
492
|
+
const d = node[i]
|
|
493
|
+
if (Array.isArray(d) && d[0] === '=' && isGlobal(d[1])) out.push([d[1], d[2]])
|
|
494
|
+
}
|
|
495
|
+
return out
|
|
496
|
+
}
|
|
497
|
+
return []
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Poison a global assigned anywhere but an unconditional init statement — in a
|
|
501
|
+
// function body, or nested in init control flow. Its value is then not a
|
|
502
|
+
// fixed post-init constant.
|
|
503
|
+
const poison = new Set()
|
|
504
|
+
const scanWrites = (node, topInit) => {
|
|
505
|
+
if (!Array.isArray(node)) return
|
|
506
|
+
const op = node[0]
|
|
507
|
+
if (op === 'let' || op === 'const') {
|
|
508
|
+
// A declarator `=` is part of the declaration, not a nested assignment —
|
|
509
|
+
// poison only when the declaration itself is non-top-level.
|
|
510
|
+
for (let i = 1; i < node.length; i++) {
|
|
511
|
+
const d = node[i]
|
|
512
|
+
if (Array.isArray(d) && d[0] === '=') {
|
|
513
|
+
if (!topInit && isGlobal(d[1])) poison.add(d[1])
|
|
514
|
+
scanWrites(d[2], false)
|
|
515
|
+
} else scanWrites(d, false)
|
|
516
|
+
}
|
|
517
|
+
return
|
|
518
|
+
}
|
|
519
|
+
if (op === '=') {
|
|
520
|
+
if (!topInit && isGlobal(node[1])) poison.add(node[1])
|
|
521
|
+
scanWrites(node[1], false)
|
|
522
|
+
scanWrites(node[2], false)
|
|
523
|
+
return
|
|
524
|
+
}
|
|
525
|
+
for (let i = 1; i < node.length; i++) scanWrites(node[i], false)
|
|
526
|
+
}
|
|
527
|
+
for (const stmt of initStmts) scanWrites(stmt, true)
|
|
528
|
+
for (const fn of ctx.func.map.values())
|
|
529
|
+
if (fn.body && !fn.raw) scanWrites(fn.body, false)
|
|
530
|
+
|
|
531
|
+
// Resolve each global's value by a linear pass over init in execution order.
|
|
532
|
+
const env = new Map()
|
|
533
|
+
const evalFn = (rhs) =>
|
|
534
|
+
typeof rhs !== 'string' ? null
|
|
535
|
+
: fnNames.has(rhs) ? rhs
|
|
536
|
+
: env.has(rhs) ? env.get(rhs)
|
|
537
|
+
: null
|
|
538
|
+
for (const stmt of initStmts)
|
|
539
|
+
for (const [g, rhs] of writesOf(stmt)) env.set(g, evalFn(rhs))
|
|
540
|
+
|
|
541
|
+
const devirt = new Map()
|
|
542
|
+
for (const [g, fn] of env)
|
|
543
|
+
if (fn && fnNames.has(fn) && !poison.has(g)) devirt.set(g, fn)
|
|
544
|
+
if (!devirt.size) return
|
|
545
|
+
|
|
546
|
+
// Condition 3: a call through G that runs *during* init would see an
|
|
547
|
+
// intermediate value. Drop any candidate G called by init code, or by a
|
|
548
|
+
// function reachable from it.
|
|
549
|
+
//
|
|
550
|
+
// `walkStraightLine` follows only straight-line execution: a nested `=>`
|
|
551
|
+
// literal is a closure *constructed* here, not run here, so its body is
|
|
552
|
+
// skipped — an IIFE callee `(=> …)()` is the one exception, its body does
|
|
553
|
+
// run. This is what keeps operator-registration init (`binary('+', 11)`
|
|
554
|
+
// builds, but does not invoke, a parselet closure) from dragging the parser
|
|
555
|
+
// into the init-reachable set, and keeps a wrapper body's `space()` call —
|
|
556
|
+
// which fires at parse time — from counting as an init call. (Soundness
|
|
557
|
+
// rests on a closure constructed during init not also being invoked during
|
|
558
|
+
// init: true of function-slot wrappers, which are registered then called at
|
|
559
|
+
// use time.)
|
|
560
|
+
const walkStraightLine = (node, onCall) => {
|
|
561
|
+
if (!Array.isArray(node)) return
|
|
562
|
+
const op = node[0]
|
|
563
|
+
if (op === '()') {
|
|
564
|
+
onCall(node[1])
|
|
565
|
+
if (Array.isArray(node[1]) && node[1][0] === '=>') walkStraightLine(node[1][2], onCall)
|
|
566
|
+
for (let i = 2; i < node.length; i++) walkStraightLine(node[i], onCall)
|
|
567
|
+
return
|
|
568
|
+
}
|
|
569
|
+
if (op === '=>' || op === 'function') return
|
|
570
|
+
for (let i = 1; i < node.length; i++) walkStraightLine(node[i], onCall)
|
|
571
|
+
}
|
|
572
|
+
const reachable = new Set()
|
|
573
|
+
const queue = []
|
|
574
|
+
const seedCalls = (node) => walkStraightLine(node, (c) => {
|
|
575
|
+
if (typeof c === 'string' && fnNames.has(c)) queue.push(c)
|
|
576
|
+
})
|
|
577
|
+
for (const s of initStmts) seedCalls(s)
|
|
578
|
+
while (queue.length) {
|
|
579
|
+
const f = queue.pop()
|
|
580
|
+
if (reachable.has(f)) continue
|
|
581
|
+
reachable.add(f)
|
|
582
|
+
const fn = ctx.func.map.get(f)
|
|
583
|
+
if (fn?.body && !fn.raw) seedCalls(fn.body)
|
|
584
|
+
}
|
|
585
|
+
const calledInInit = new Set()
|
|
586
|
+
const collectCalled = (node) => walkStraightLine(node, (c) => {
|
|
587
|
+
if (devirt.has(c)) calledInInit.add(c)
|
|
588
|
+
})
|
|
589
|
+
for (const s of initStmts) collectCalled(s)
|
|
590
|
+
for (const f of reachable) { const fn = ctx.func.map.get(f); if (fn?.body) collectCalled(fn.body) }
|
|
591
|
+
for (const g of calledInInit) devirt.delete(g)
|
|
592
|
+
|
|
593
|
+
if (devirt.size) ctx.func.globalDevirt = devirt
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export const materializeAutoBoxSchemas = (programFacts) => {
|
|
597
|
+
if (!ctx.schema.register) return
|
|
598
|
+
for (const [name, props] of programFacts.propMap) {
|
|
599
|
+
if (ctx.schema.vars.has(name)) {
|
|
600
|
+
const existing = ctx.schema.resolve(name)
|
|
601
|
+
const newProps = [...props].filter(prop => !existing.includes(prop))
|
|
602
|
+
if (newProps.length) {
|
|
603
|
+
const merged = [...existing, ...newProps]
|
|
604
|
+
const mergedId = ctx.schema.register(merged)
|
|
605
|
+
ctx.schema.vars.set(name, mergedId)
|
|
606
|
+
}
|
|
607
|
+
continue
|
|
608
|
+
}
|
|
609
|
+
const valueProps = [...props].filter(prop => !ctx.func.names.has(`${name}$${prop}`))
|
|
610
|
+
if (!valueProps.length) continue
|
|
611
|
+
const allProps = [...props]
|
|
612
|
+
const schema = ['__inner__', ...allProps]
|
|
613
|
+
const schemaId = ctx.schema.register(schema)
|
|
614
|
+
ctx.schema.vars.set(name, schemaId)
|
|
615
|
+
if (ctx.func.names.has(name) && !ctx.scope.globals.has(name))
|
|
616
|
+
declGlobal(name, 'f64')
|
|
617
|
+
if (!ctx.schema.autoBox) ctx.schema.autoBox = new Map()
|
|
618
|
+
ctx.schema.autoBox.set(name, { schemaId, schema })
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export const resolveClosureWidth = (programFacts) => {
|
|
623
|
+
if (!ctx.closure.make) return
|
|
624
|
+
const { hasSpread, hasRest, maxCall, maxDef, valueUsed } = programFacts
|
|
625
|
+
const floor = ctx.closure.floor ?? 0
|
|
626
|
+
// A top-level function used as a first-class value gets a boundary trampoline
|
|
627
|
+
// that forwards $__a0..$__a{arity-1} into it (emit.js). The uniform closure
|
|
628
|
+
// ABI must therefore be at least as wide as any table-resident function's
|
|
629
|
+
// fixed arity — maxDef only counts surviving `=>` literals, so lifted/hoisted
|
|
630
|
+
// function definitions slip past it (their bodies are walked, their param
|
|
631
|
+
// lists aren't). Without this, e.g. an arity-3 function used only via a
|
|
632
|
+
// 1-arg indirect call emits `(local.get $__a2)` against a 2-param trampoline.
|
|
633
|
+
let maxValueArity = 0
|
|
634
|
+
if (valueUsed) for (const name of valueUsed) {
|
|
635
|
+
const n = ctx.func.map.get(name)?.sig?.params?.length ?? 0
|
|
636
|
+
if (n > maxValueArity) maxValueArity = n
|
|
637
|
+
}
|
|
638
|
+
ctx.closure.width = (hasSpread && hasRest)
|
|
639
|
+
? MAX_CLOSURE_ARITY
|
|
640
|
+
: Math.min(MAX_CLOSURE_ARITY, Math.max(maxCall, maxDef + (hasRest ? 1 : 0), maxValueArity, floor))
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export const canSkipWholeProgramNarrowing = (programFacts) =>
|
|
644
|
+
programFacts.callSites.length === 0 &&
|
|
645
|
+
programFacts.valueUsed.size === 0 &&
|
|
646
|
+
!programFacts.anyDyn &&
|
|
647
|
+
programFacts.propMap.size === 0 &&
|
|
648
|
+
!programFacts.hasSchemaLiterals &&
|
|
649
|
+
!ctx.closure.make
|