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
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* which mutate the AST as their final step):
|
|
9
9
|
*
|
|
10
10
|
* - `inferModuleLetTypes` — module-level `let` typed-array union
|
|
11
|
+
* - `inferModuleGlobalValTypes` — module-global VAL-kind from an all-writers scan
|
|
11
12
|
* - `unboxConstTypedGlobals` — const typed-array → unboxed i32 offset
|
|
12
13
|
* - `inferModuleIntGlobals` — purpose-focused f64→i32 numeric demotion
|
|
13
14
|
* - `flattenFuncNamespaces` — `f.prop` slot SROA + dead-write drop
|
|
@@ -20,12 +21,14 @@
|
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
23
|
import { ctx, warn, declGlobal } from '../../ctx.js'
|
|
23
|
-
import { ASSIGN_OPS, T, refsAny } from '../../ast.js'
|
|
24
|
+
import { ASSIGN_OPS, T, refsAny, extractParams, classifyParam, collectParamNames } from '../../ast.js'
|
|
24
25
|
import { VAL, updateGlobalRep } from '../../reps.js'
|
|
25
26
|
import { typedElemCtor, ternaryCtorOfRhs, MIXED_CTORS } from '../../type.js'
|
|
27
|
+
import { inferSchemaId } from '../infer.js'
|
|
28
|
+
import { valTypeOf } from '../../kind.js'
|
|
26
29
|
import { typedElemAux } from '../../../layout.js'
|
|
27
30
|
import { MAX_CLOSURE_ARITY, UNDEF_NAN } from '../../ir.js'
|
|
28
|
-
import { analyzeFuncNamespaces } from '../analyze.js'
|
|
31
|
+
import { analyzeFuncNamespaces, analyzeBody } from '../analyze.js'
|
|
29
32
|
import { invalidateProgramFactsCache } from '../program-facts.js'
|
|
30
33
|
|
|
31
34
|
// `scanGlobalValueFacts` was deleted — prepare's depth-0 catch (calling
|
|
@@ -102,6 +105,16 @@ export const inferModuleLetTypes = (ast) => {
|
|
|
102
105
|
if (ctor === MIXED) { d.bad = true; return }
|
|
103
106
|
if (ctor) { d.ctors.add(ctor); return }
|
|
104
107
|
if (typeof rhs === 'string') { d.refs.add(key(rhs, sid)); return }
|
|
108
|
+
// Field provenance: a schema slot holding ONE typed kind program-wide (and a
|
|
109
|
+
// prop never written — both gates inside slotTypedCtorAt, which also fails
|
|
110
|
+
// closed before the slot census exists) contributes that kind as evidence.
|
|
111
|
+
if (Array.isArray(rhs) && (rhs[0] === '.' || rhs[0] === '?.') &&
|
|
112
|
+
typeof rhs[1] === 'string' && typeof rhs[2] === 'string') {
|
|
113
|
+
const fc = ctx.schema?.slotTypedCtorAt?.(rhs[1], rhs[2])
|
|
114
|
+
if (fc) { d.ctors.add(fc); return }
|
|
115
|
+
d.bad = true
|
|
116
|
+
return
|
|
117
|
+
}
|
|
105
118
|
if (Array.isArray(rhs) && rhs[0] === '()') {
|
|
106
119
|
const callee = rhs[1]
|
|
107
120
|
// `recv.subarray(...)` / `recv.slice(...)` / `recv.map(...)` → inherit recv's ctor.
|
|
@@ -182,6 +195,253 @@ export const inferModuleLetTypes = (ast) => {
|
|
|
182
195
|
}
|
|
183
196
|
}
|
|
184
197
|
|
|
198
|
+
/** LATE field-provenance refinement — after narrow's return inference, module
|
|
199
|
+
* consts bound to returned objects resolve their SCHEMA: `const P = mk(n)`
|
|
200
|
+
* binds P's sid through mk's inferred return sid (`valResult`/`ptrAux`). A
|
|
201
|
+
* const's single init is its value on every read, so the binding is exactly as
|
|
202
|
+
* trustworthy as the return inference itself. The ctor side (a following
|
|
203
|
+
* `const T = P.wre`, memo globals, Map-cached plans) is the late
|
|
204
|
+
* inferModuleLetTypes re-run, whose field/`@map:` evidence needs these sids
|
|
205
|
+
* bound first (bench: provenance, fftplan). */
|
|
206
|
+
export const refineFieldProvenance = (ast) => {
|
|
207
|
+
if (!ctx.scope.userGlobals || !ctx.schema?.vars) return
|
|
208
|
+
const visitDecl = (node) => {
|
|
209
|
+
if (!Array.isArray(node)) return
|
|
210
|
+
if (node[0] === 'let' || node[0] === 'const') {
|
|
211
|
+
for (const d of node.slice(1)) {
|
|
212
|
+
if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
|
|
213
|
+
const name = d[1], rhs = d[2]
|
|
214
|
+
if (!ctx.scope.userGlobals.has(name) || !ctx.scope.consts?.has(name)) continue
|
|
215
|
+
if (!ctx.schema.vars.has(name) && !ctx.schema.poisoned?.has(name)) {
|
|
216
|
+
const sid = inferSchemaId(rhs, null)
|
|
217
|
+
if (sid != null) ctx.schema.vars.set(name, sid)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
if (node[0] === ';' || node[0] === 'export') for (let i = 1; i < node.length; i++) visitDecl(node[i])
|
|
223
|
+
}
|
|
224
|
+
if (Array.isArray(ast)) {
|
|
225
|
+
if (ast[0] === ';') for (let i = 1; i < ast.length; i++) visitDecl(ast[i])
|
|
226
|
+
else visitDecl(ast)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// A module global whose every write anywhere in the program (any function body,
|
|
231
|
+
// any nesting/closure depth) agrees on one VAL.* kind — not just its depth-0
|
|
232
|
+
// initializer (recordGlobalRep's territory). The subscript/jessie shape this
|
|
233
|
+
// unblocks: `export let idx, cur, parse = s => (idx = 0, cur = s, …)` — `cur`
|
|
234
|
+
// is a parse-state global assigned ONLY inside `parse`'s body, so recordGlobalRep
|
|
235
|
+
// (depth-0 only) never proves it, every `cur.charCodeAt(i)` read in the scan
|
|
236
|
+
// loop stays on the durable-receiver override probe (sidecarOverride, ir.js —
|
|
237
|
+
// gated on `valTypeOf(receiver)`, which this pass feeds via `ctx.scope.
|
|
238
|
+
// globalValTypes`), and the probe's own call_indirect fail-closes the loop
|
|
239
|
+
// hoist. Proving `cur` STRING here removes the probe (emit.js's tryStaticDispatch
|
|
240
|
+
// fires once `vt` is known, ahead of the runtime-dispatch/sidecar strategies).
|
|
241
|
+
//
|
|
242
|
+
// Soundness is fail-closed and layered:
|
|
243
|
+
// - Host-writable escape: an exported MUTABLE global's wasm export lets the
|
|
244
|
+
// host `.value =` it with ANY bit pattern, invisible to this (or any) AST
|
|
245
|
+
// scan — excluded from candidacy outright (isHostWritableGlobal).
|
|
246
|
+
// - Shadowing: a write only counts when `name` is free (not a param/let/const/
|
|
247
|
+
// catch binding) at that point in ITS OWN enclosing function/closure scope —
|
|
248
|
+
// computed per scope via a dedicated bound-name collector, not reused from
|
|
249
|
+
// emit-time `ctx.func.locals` (not populated yet at plan time).
|
|
250
|
+
// - Unrecognized write shapes (compound-assign, `++`/`--`, a destructuring
|
|
251
|
+
// target — in practice already desugared to plain assigns by prepare, but
|
|
252
|
+
// defended anyway) poison the candidate outright: correctness-relevant
|
|
253
|
+
// mutations this scan can't classify must not be silently dropped.
|
|
254
|
+
// - Every other write's RHS is resolved via `valTypeOf` under a per-scope
|
|
255
|
+
// overlay (that scope's `let/const` locals from `analyzeBody`, plus — when
|
|
256
|
+
// `paramReps` is supplied — its OWN resolved param facts), so a plain
|
|
257
|
+
// parameter alias (`cur = s`) or a method result (`cur = s.slice(i)`)
|
|
258
|
+
// resolves the same way a body-local's does. A bare reference to ANOTHER
|
|
259
|
+
// candidate global, or a call to a user function, defers to a small
|
|
260
|
+
// alias-graph fixpoint (module globals + `@ret:fn` result nodes only —
|
|
261
|
+
// unlike inferModuleLetTypes's ctor lattice, ordinary locals need no
|
|
262
|
+
// fixpoint slot since the overlay already resolves them in one pass).
|
|
263
|
+
//
|
|
264
|
+
// Two call sites (mirrors recordGlobalRep's early landing spot, then widens
|
|
265
|
+
// once call-site param facts exist):
|
|
266
|
+
// 1. Here in plan(), alongside inferModuleLetTypes — before narrowSignatures
|
|
267
|
+
// reads `ctx.scope.globalValTypes` for its own callerValTypes seed, and
|
|
268
|
+
// early enough that a freshly-proven NUMBER global still reaches
|
|
269
|
+
// inferModuleIntGlobals's i32 candidacy check.
|
|
270
|
+
// 2. Again after narrowSignatures (plan/index.js) — `cur = s` needs `s`'s
|
|
271
|
+
// resolved param fact (`programFacts.paramReps`), which narrowSignatures'
|
|
272
|
+
// call-site fixpoint hasn't produced yet on pass 1. Idempotent: candidates
|
|
273
|
+
// already claimed (by recordGlobalRep, inferModuleLetTypes, or pass 1 of
|
|
274
|
+
// this same function) are skipped, so the rerun only picks up new proofs.
|
|
275
|
+
const GLOBAL_VT_CONFLICT = Symbol('global-vt-conflict')
|
|
276
|
+
|
|
277
|
+
// Exported mutable global — the wasm export lets the host assign it any value
|
|
278
|
+
// through `instance.exports.name.value = …`, a write no AST scan can see.
|
|
279
|
+
// `mut: false` (const) globals export as immutable wasm globals — the JS API
|
|
280
|
+
// throws on `.value =`, so a const export is safe regardless of `ctx.func.exports`.
|
|
281
|
+
const isHostWritableGlobal = (name) => {
|
|
282
|
+
const decl = ctx.scope.globals.get(name)
|
|
283
|
+
if (!decl?.mut) return false
|
|
284
|
+
for (const [exportName, val] of Object.entries(ctx.func.exports || {}))
|
|
285
|
+
if (val === name || (val === true && exportName === name)) return true
|
|
286
|
+
return false
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const isNullishLit = (e) => e == null || e === 'undefined' || e === 'null'
|
|
290
|
+
|| (Array.isArray(e) && e[0] == null && (e[1] === undefined || e[1] === null))
|
|
291
|
+
|
|
292
|
+
// A destructuring-ASSIGNMENT target (`[a,b] = …` / `({a} = …)`) — prepare
|
|
293
|
+
// desugars these to temp-based plain assigns before this pass ever runs (a
|
|
294
|
+
// module-global candidate never actually reaches this shape in practice), but
|
|
295
|
+
// a leaf write through one is a real mutation this scan doesn't classify, so
|
|
296
|
+
// it must poison rather than silently pass through. `'[]'` here is the
|
|
297
|
+
// pre-prepare pattern-or-index overload — length ≠ 3 rules out `recv[idx]`.
|
|
298
|
+
const isAssignPatternNode = (n) =>
|
|
299
|
+
Array.isArray(n) && (n[0] === '[' || n[0] === '{}' || (n[0] === '[]' && n.length !== 3))
|
|
300
|
+
|
|
301
|
+
export const inferModuleGlobalValTypes = (ast, paramReps) => {
|
|
302
|
+
if (!ctx.scope.userGlobals?.size) return
|
|
303
|
+
|
|
304
|
+
const candidates = new Set()
|
|
305
|
+
for (const name of ctx.scope.userGlobals) {
|
|
306
|
+
if (ctx.scope.globalValTypes?.get(name)) continue // already proven (recordGlobalRep / inferModuleLetTypes / a prior call)
|
|
307
|
+
if (ctx.func.names?.has(name)) continue // a function binding, not a data global
|
|
308
|
+
if (isHostWritableGlobal(name)) continue // host can write any bit pattern — no claim possible
|
|
309
|
+
candidates.add(name)
|
|
310
|
+
}
|
|
311
|
+
if (!candidates.size) return
|
|
312
|
+
|
|
313
|
+
const fnames = ctx.func.names || new Set()
|
|
314
|
+
// defs keys: bare candidate names (module-wide — globally unique) and
|
|
315
|
+
// `@ret:<fn>` virtual nodes (also globally unique). No scope qualification
|
|
316
|
+
// needed — everything that ISN'T a candidate-to-candidate or fn-return alias
|
|
317
|
+
// resolves synchronously via valTypeOf under the per-scope overlay below.
|
|
318
|
+
const defs = new Map()
|
|
319
|
+
const getDef = (k) => { let d = defs.get(k); if (!d) defs.set(k, d = { vals: new Set(), refs: new Set(), bad: false }); return d }
|
|
320
|
+
|
|
321
|
+
const observe = (name, rhs) => {
|
|
322
|
+
const d = getDef(name)
|
|
323
|
+
if (d.bad) return
|
|
324
|
+
if (isNullishLit(rhs)) return // no evidence either way
|
|
325
|
+
if (typeof rhs === 'string' && candidates.has(rhs)) { d.refs.add(rhs); return }
|
|
326
|
+
if (Array.isArray(rhs) && rhs[0] === '()' && typeof rhs[1] === 'string' && fnames.has(rhs[1])) {
|
|
327
|
+
d.refs.add('@ret:' + rhs[1]); return
|
|
328
|
+
}
|
|
329
|
+
const vt = valTypeOf(rhs)
|
|
330
|
+
if (vt) d.vals.add(vt)
|
|
331
|
+
else d.bad = true // unrecognized/computed shape — fail closed
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Positional (index-keyed) param names for a `=>` params node or a func.list
|
|
335
|
+
// signature — `null` slots (rest/destructured params) simply never resolve
|
|
336
|
+
// via paramReps, which is fine: they fall to the overlay's ordinary "unknown".
|
|
337
|
+
const paramNamesOf = (paramsNode) => extractParams(paramsNode).map(r => {
|
|
338
|
+
const c = classifyParam(r)
|
|
339
|
+
return (c.kind === 'plain' || c.kind === 'default') ? c.name : null
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
// Enter one function/arrow scope: `body` is walked for writes to `candidates`,
|
|
343
|
+
// with `paramNames` (positional) seeding the shadow set and — for a NAMED
|
|
344
|
+
// function with a resolved paramReps entry — the valType overlay too.
|
|
345
|
+
const walkFn = (body, paramNames, funcName) => {
|
|
346
|
+
// Shadow set: every name locally bound anywhere in THIS scope (params +
|
|
347
|
+
// every let/const/catch binding, at any nesting depth short of a nested
|
|
348
|
+
// `=>` — jz's own body-local analyses (findFreeVars, boxedCaptures) use
|
|
349
|
+
// the same "hoist let to function scope" convention, matching how prepare
|
|
350
|
+
// resolves same-name block shadowing). A write to a shadowed name is a
|
|
351
|
+
// local mutation, not a global one — it must not pollute the global's kind.
|
|
352
|
+
const bound = new Set()
|
|
353
|
+
for (const p of paramNames) if (p) bound.add(p)
|
|
354
|
+
const collectDecls = (node) => {
|
|
355
|
+
if (!Array.isArray(node)) return
|
|
356
|
+
const op = node[0]
|
|
357
|
+
if (op === '=>') return
|
|
358
|
+
if (op === 'let' || op === 'const') collectParamNames(node.slice(1), bound)
|
|
359
|
+
if (op === 'catch' && typeof node[2] === 'string') bound.add(node[2])
|
|
360
|
+
for (let i = 1; i < node.length; i++) collectDecls(node[i])
|
|
361
|
+
}
|
|
362
|
+
collectDecls(body)
|
|
363
|
+
|
|
364
|
+
// Overlay: this scope's own let/const locals (analyzeBody — the same
|
|
365
|
+
// per-function local analysis emit.js seeds from) plus, for a named
|
|
366
|
+
// function once paramReps is populated (pass 2, post-narrowSignatures),
|
|
367
|
+
// its resolved param facts — so `cur = s` resolves `s` exactly like a
|
|
368
|
+
// local alias would, via the same valTypeOf call sites use everywhere else.
|
|
369
|
+
const overlay = new Map(analyzeBody(body).valTypes)
|
|
370
|
+
if (funcName && paramReps) {
|
|
371
|
+
const reps = paramReps.get(funcName)
|
|
372
|
+
if (reps) for (const [idx, r] of reps)
|
|
373
|
+
if (r.val && paramNames[idx] != null && !overlay.has(paramNames[idx])) overlay.set(paramNames[idx], r.val)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const prevOverlay = ctx.func.localValTypesOverlay
|
|
377
|
+
ctx.func.localValTypesOverlay = overlay
|
|
378
|
+
try { walkStmts(body, bound, funcName) }
|
|
379
|
+
finally { ctx.func.localValTypesOverlay = prevOverlay }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const walkStmts = (node, bound, retFn) => {
|
|
383
|
+
if (!Array.isArray(node)) return
|
|
384
|
+
const op = node[0]
|
|
385
|
+
if (op === '=>') { walkFn(node[2], paramNamesOf(node[1]), null); return }
|
|
386
|
+
if (op === 'return' && retFn && node[1] !== undefined) observe('@ret:' + retFn, node[1])
|
|
387
|
+
if (op === '=' && node.length >= 3) {
|
|
388
|
+
const t = node[1]
|
|
389
|
+
if (typeof t === 'string') { if (candidates.has(t) && !bound.has(t)) observe(t, node[2]) }
|
|
390
|
+
else if (isAssignPatternNode(t)) {
|
|
391
|
+
for (const n of collectParamNames([t])) if (candidates.has(n) && !bound.has(n)) getDef(n).bad = true
|
|
392
|
+
}
|
|
393
|
+
} else if (ASSIGN_OPS.has(op) && typeof node[1] === 'string') {
|
|
394
|
+
if (candidates.has(node[1]) && !bound.has(node[1])) getDef(node[1]).bad = true // compound-assign: can't classify the merged value — poison
|
|
395
|
+
} else if ((op === '++' || op === '--') && typeof node[1] === 'string') {
|
|
396
|
+
if (candidates.has(node[1]) && !bound.has(node[1])) getDef(node[1]).bad = true // ToNumeric mutation — poison (recordGlobalRep/inferModuleIntGlobals own the numeric-counter case)
|
|
397
|
+
}
|
|
398
|
+
for (let i = 1; i < node.length; i++) walkStmts(node[i], bound, retFn)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Module-init-time code (ast + every bundled dependency's top-level init)
|
|
402
|
+
// is recordGlobalRep's territory ALREADY — including control-flow-nested
|
|
403
|
+
// assignments, verified empirically (an `if`-nested depth-0 `cur = 5` DOES
|
|
404
|
+
// land in `ctx.scope.globalValTypes` via prepare's own depth-0 walk). This
|
|
405
|
+
// walk exists only to reach closures DEFINED at module-init time (an inline
|
|
406
|
+
// `.forEach(x => { g = x })` at top level) whose BODIES don't run until
|
|
407
|
+
// called — invisible to the depth-0 walk, visible to this one.
|
|
408
|
+
const findArrows = (node) => {
|
|
409
|
+
if (!Array.isArray(node)) return
|
|
410
|
+
if (node[0] === '=>') { walkFn(node[2], paramNamesOf(node[1]), null); return }
|
|
411
|
+
for (let i = 1; i < node.length; i++) findArrows(node[i])
|
|
412
|
+
}
|
|
413
|
+
findArrows(ast)
|
|
414
|
+
if (ctx.module.moduleInits) for (const init of ctx.module.moduleInits) findArrows(init)
|
|
415
|
+
for (const f of ctx.func.list) {
|
|
416
|
+
if (!f.body || f.raw) continue
|
|
417
|
+
walkFn(f.body, (f.sig?.params || []).map(p => p.name), f.name)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Least-fixed-point over the alias graph (candidate↔candidate refs + `@ret:`
|
|
421
|
+
// fn-result refs). bottom = null (no evidence), top = CONFLICT; a concrete
|
|
422
|
+
// VAL.* is a fixed point once every def and ref agree.
|
|
423
|
+
const join = (a, b) => a === GLOBAL_VT_CONFLICT || b === GLOBAL_VT_CONFLICT ? GLOBAL_VT_CONFLICT
|
|
424
|
+
: a == null ? b : b == null ? a : a === b ? a : GLOBAL_VT_CONFLICT
|
|
425
|
+
const state = new Map()
|
|
426
|
+
const refState = (r) => defs.has(r) ? (state.get(r) ?? null) : (ctx.scope.globalValTypes?.get(r) ?? null)
|
|
427
|
+
let changed = true
|
|
428
|
+
while (changed) {
|
|
429
|
+
changed = false
|
|
430
|
+
for (const [k, d] of defs) {
|
|
431
|
+
let cur = d.bad ? GLOBAL_VT_CONFLICT : null
|
|
432
|
+
if (cur !== GLOBAL_VT_CONFLICT) for (const v of d.vals) cur = join(cur, v)
|
|
433
|
+
if (cur !== GLOBAL_VT_CONFLICT) for (const r of d.refs) cur = join(cur, refState(r))
|
|
434
|
+
if (cur !== (state.get(k) ?? null)) { state.set(k, cur); changed = true }
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const name of candidates) {
|
|
439
|
+
const vt = state.get(name)
|
|
440
|
+
if (!vt || vt === GLOBAL_VT_CONFLICT) continue
|
|
441
|
+
;(ctx.scope.globalValTypes ||= new Map()).set(name, vt)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
185
445
|
export const unboxConstTypedGlobals = () => {
|
|
186
446
|
if (!ctx.scope.globalTypedElem || !ctx.scope.consts) return
|
|
187
447
|
for (const [name, ctor] of ctx.scope.globalTypedElem) {
|
|
@@ -252,10 +512,18 @@ export const inferModuleIntGlobals = (ast) => {
|
|
|
252
512
|
// Does `e` provably evaluate to a non-integer? Integer-coercing ops (bitwise,
|
|
253
513
|
// shifts) and comparisons launder any fraction; only the *value*-bearing
|
|
254
514
|
// branches of ternary/logical ops carry it.
|
|
515
|
+
// Post-prepare, `Math.PI` / `Math.sqrt(x)` arrive as FLAT math keys — the bare
|
|
516
|
+
// string 'math.PI' in value position, 'math.sqrt' as a string callee — not the
|
|
517
|
+
// raw ['.','Math','sqrt'] shape. Missing them assumed INTEGER and i32-demoted
|
|
518
|
+
// module consts like `export const TWO_PI = Math.PI * 2` in DEP modules (their
|
|
519
|
+
// inits prep before this scan), truncating 6.283… → 6 at init.
|
|
520
|
+
const FRACTIONAL_MATH_CONSTS = new Set(['PI', 'E', 'LN2', 'LN10', 'LOG2E', 'LOG10E', 'SQRT2', 'SQRT1_2'])
|
|
521
|
+
const fractionalMathKey = (k) => typeof k === 'string' && k.startsWith('math.')
|
|
522
|
+
&& (FRACTIONAL_MATH.has(k.slice(5)) || FRACTIONAL_MATH_CONSTS.has(k.slice(5)))
|
|
255
523
|
const producesFraction = (e) => {
|
|
256
524
|
if (e == null) return false
|
|
257
525
|
if (typeof e === 'number') return !Number.isInteger(e)
|
|
258
|
-
if (typeof e === 'string') return refIsFractional(e)
|
|
526
|
+
if (typeof e === 'string') return refIsFractional(e) || fractionalMathKey(e)
|
|
259
527
|
if (!Array.isArray(e)) return false
|
|
260
528
|
const op = e[0]
|
|
261
529
|
if (op == null) return typeof e[1] === 'number' && !Number.isInteger(e[1])
|
|
@@ -267,6 +535,7 @@ export const inferModuleIntGlobals = (ast) => {
|
|
|
267
535
|
const callee = e[1]
|
|
268
536
|
if (Array.isArray(callee) && callee[0] === '?') return producesFraction(callee[2]) || producesFraction(callee[3])
|
|
269
537
|
if (Array.isArray(callee) && callee[0] === '.' && callee[1] === 'Math' && FRACTIONAL_MATH.has(callee[2])) return true
|
|
538
|
+
if (fractionalMathKey(callee)) return true
|
|
270
539
|
return false // unknown call → assume integer
|
|
271
540
|
}
|
|
272
541
|
for (let i = 1; i < e.length; i++) if (producesFraction(e[i])) return true
|
|
@@ -316,6 +585,10 @@ export const inferModuleIntGlobals = (ast) => {
|
|
|
316
585
|
for (let i = 1; i < node.length; i++) walk(node[i], scope)
|
|
317
586
|
}
|
|
318
587
|
walk(ast, null)
|
|
588
|
+
// DEP-module top-level inits live in ctx.module.moduleInits, NOT the entry ast —
|
|
589
|
+
// without walking them a dep's `export const TWO_PI = Math.PI * 2` records no
|
|
590
|
+
// RHS at all and the integer default i32-demotes it (init truncated 6.283 → 6).
|
|
591
|
+
if (ctx.module.moduleInits) for (const init of ctx.module.moduleInits) walk(init, null)
|
|
319
592
|
for (const f of ctx.func.list) {
|
|
320
593
|
if (!f.body || f.raw) continue
|
|
321
594
|
const params = new Set((f.sig?.params || []).map(p => p.name))
|
|
@@ -394,9 +667,22 @@ export const flattenFuncNamespaces = (ast) => {
|
|
|
394
667
|
for (const prop of info.props) {
|
|
395
668
|
if (ctx.func.multiProp.has(`${f}.${prop}`)) { plan(prop, { global: `${f}${T}${prop}` }); continue }
|
|
396
669
|
const w = info.writes.get(prop)
|
|
397
|
-
// Single write of the lifted `$f$prop
|
|
398
|
-
|
|
399
|
-
|
|
670
|
+
// Single top-level write of the lifted `$f$prop` (the `f.prop = arrow`
|
|
671
|
+
// definition shape): calls to it already lower to a direct `call $f$prop`,
|
|
672
|
+
// which a global would demote to call_indirect — leave it alone; when it's
|
|
673
|
+
// additionally never read as a value, the write itself is dead → drop it.
|
|
674
|
+
if (w && w.length === 1 && w[0].atInit && w[0].rhs === `${f}$${prop}`) {
|
|
675
|
+
if (!info.valRead.has(prop)) plan(prop, { drop: true })
|
|
676
|
+
continue
|
|
677
|
+
}
|
|
678
|
+
// Everything else dissolves into a module global — the namespace is
|
|
679
|
+
// non-escaping, so every site is visible and the slot is a closed cell.
|
|
680
|
+
// This covers slots multiProp can't see: props reassigned only INSIDE
|
|
681
|
+
// function bodies (prepare's registry counts top-level lifts), and
|
|
682
|
+
// single-write non-function values. The layered-parser state pattern
|
|
683
|
+
// (subscript's parse.comment/newline/semi — read/written per token) was
|
|
684
|
+
// paying a __dyn_get/__dyn_set probe chain per site without this.
|
|
685
|
+
plan(prop, { global: `${f}${T}${prop}` })
|
|
400
686
|
}
|
|
401
687
|
}
|
|
402
688
|
if (!flat.size) return false
|
|
@@ -409,7 +695,11 @@ export const flattenFuncNamespaces = (ast) => {
|
|
|
409
695
|
typeof obj === 'string' && typeof prop === 'string' && flat.has(obj)
|
|
410
696
|
? flat.get(obj).get(prop) : undefined
|
|
411
697
|
const isEmptySeq = (n) => Array.isArray(n) && n.length === 1 && n[0] === ';'
|
|
412
|
-
|
|
698
|
+
// `stmt` — node sits in statement position (a `;` sequence child). A dropped
|
|
699
|
+
// write there emits nothing; in EXPRESSION position (comma chain, init value,
|
|
700
|
+
// arrow body) `(f.p = v)` must still yield v — the lifted-closure reference —
|
|
701
|
+
// or the surrounding arity breaks (invalid wasm: values left on the stack).
|
|
702
|
+
const rewrite = (node, stmt = false) => {
|
|
413
703
|
if (!Array.isArray(node)) return node
|
|
414
704
|
const op = node[0]
|
|
415
705
|
if (op === '.' || op === '?.') {
|
|
@@ -419,13 +709,13 @@ export const flattenFuncNamespaces = (ast) => {
|
|
|
419
709
|
if (op === '=' && Array.isArray(node[1]) && (node[1][0] === '.' || node[1][0] === '?.')) {
|
|
420
710
|
const d = decisionFor(node[1][1], node[1][2])
|
|
421
711
|
if (d?.global) return ['=', d.global, rewrite(node[2])]
|
|
422
|
-
if (d?.drop) return [';'] // dead write —
|
|
712
|
+
if (d?.drop) return stmt ? [';'] : rewrite(node[2]) // dead write — statement: nothing; value: the rhs
|
|
423
713
|
}
|
|
424
714
|
const out = [op]
|
|
425
715
|
// Filter dropped writes out of statement sequences (an empty `[';']` left in
|
|
426
716
|
// a body would lower to an unrenderable node).
|
|
427
717
|
for (let i = 1; i < node.length; i++) {
|
|
428
|
-
const c = rewrite(node[i])
|
|
718
|
+
const c = rewrite(node[i], op === ';')
|
|
429
719
|
if (op === ';' && isEmptySeq(c)) continue
|
|
430
720
|
out.push(c)
|
|
431
721
|
}
|
|
@@ -435,8 +725,16 @@ export const flattenFuncNamespaces = (ast) => {
|
|
|
435
725
|
ast.length = 0
|
|
436
726
|
for (let i = 0; i < newAst.length; i++) ast.push(newAst[i])
|
|
437
727
|
invalidateProgramFactsCache(ast)
|
|
438
|
-
for (const fn of ctx.func.list)
|
|
728
|
+
for (const fn of ctx.func.list) {
|
|
439
729
|
if (fn.body && !fn.raw) fn.body = rewrite(fn.body)
|
|
730
|
+
// Default-param values are AST stored OUTSIDE fn.body (fn.defaults) — a
|
|
731
|
+
// closure default like subscript's `dispatch = (ops, tail, fn = (…) => {…
|
|
732
|
+
// parse.id(…) …})` reads func-props too. Missing them left the read on the
|
|
733
|
+
// dynamic __dyn_get path while every write had dissolved into the global —
|
|
734
|
+
// disjoint stores, so the read yielded undefined (the tokenizer's word-guard
|
|
735
|
+
// collapsed and `init` lexed as `in`+`it`).
|
|
736
|
+
if (fn.defaults) for (const k of Object.keys(fn.defaults)) fn.defaults[k] = rewrite(fn.defaults[k])
|
|
737
|
+
}
|
|
440
738
|
// The defining `f.prop = …` writes live in moduleInits for bundled programs —
|
|
441
739
|
// rewrite them too, or reads would resolve to an unwritten global.
|
|
442
740
|
if (ctx.module.moduleInits)
|
|
@@ -482,10 +780,25 @@ export const devirtGlobalCalls = (ast) => {
|
|
|
482
780
|
flatten(ast)
|
|
483
781
|
|
|
484
782
|
const isGlobal = (s) => typeof s === 'string' && ctx.scope.globals.has(s)
|
|
485
|
-
|
|
783
|
+
|
|
784
|
+
// A node that is ITSELF a compound-assign (`=`/`??=`/`||=`/…) directly
|
|
785
|
+
// targeting a global — the one-hop-deep shape a declarator's or a bare
|
|
786
|
+
// statement's own value position can be (chained assignment: `const x = (G
|
|
787
|
+
// = arrow)`; `const x = (G ??= Y)`, subscript asi.js's `parse._baseSpace ??=
|
|
788
|
+
// parse.space`). Both the poison scan and the env resolver special-case
|
|
789
|
+
// exactly this nesting; anything buried deeper (behind a comma-expression,
|
|
790
|
+
// ternary, call, …) stays outside what either tracks — the same shallow-
|
|
791
|
+
// recognition boundary this pass always had for plain `=`.
|
|
792
|
+
const chainedWrite = (n) => Array.isArray(n) && ASSIGN_OPS.has(n[0]) && typeof n[1] === 'string'
|
|
793
|
+
|
|
794
|
+
// `[target, valueNode]` pairs for a `=`/compound-assign / `let` / `const`
|
|
795
|
+
// node assigning a global. `valueNode` is either a plain value expression
|
|
796
|
+
// (declarator RHS) or — for a bare compound-assign statement — the whole
|
|
797
|
+
// write node, so resolveValue's chainedWrite branch can see the operator.
|
|
486
798
|
const writesOf = (node) => {
|
|
487
799
|
if (!Array.isArray(node)) return []
|
|
488
|
-
if (node
|
|
800
|
+
if (chainedWrite(node) && isGlobal(node[1])) return [[node[1], node]]
|
|
801
|
+
if ((node[0] === '++' || node[0] === '--') && isGlobal(node[1])) return [[node[1], null]]
|
|
489
802
|
if (node[0] === 'let' || node[0] === 'const') {
|
|
490
803
|
const out = []
|
|
491
804
|
for (let i = 1; i < node.length; i++) {
|
|
@@ -499,7 +812,11 @@ export const devirtGlobalCalls = (ast) => {
|
|
|
499
812
|
|
|
500
813
|
// Poison a global assigned anywhere but an unconditional init statement — in a
|
|
501
814
|
// function body, or nested in init control flow. Its value is then not a
|
|
502
|
-
// fixed post-init constant.
|
|
815
|
+
// fixed post-init constant. A RHS that is itself a direct chainedWrite (one
|
|
816
|
+
// hop) inherits the OUTER statement's topInit instead of being forced
|
|
817
|
+
// conservative — `const asi = parse.asi = arrow` is ONE unconditional
|
|
818
|
+
// top-level statement; poisoning its inner target merely for being nested
|
|
819
|
+
// one assignment deep would be over-conservative, not sound-required.
|
|
503
820
|
const poison = new Set()
|
|
504
821
|
const scanWrites = (node, topInit) => {
|
|
505
822
|
if (!Array.isArray(node)) return
|
|
@@ -511,15 +828,18 @@ export const devirtGlobalCalls = (ast) => {
|
|
|
511
828
|
const d = node[i]
|
|
512
829
|
if (Array.isArray(d) && d[0] === '=') {
|
|
513
830
|
if (!topInit && isGlobal(d[1])) poison.add(d[1])
|
|
514
|
-
scanWrites(d[2], false)
|
|
831
|
+
scanWrites(d[2], chainedWrite(d[2]) ? topInit : false)
|
|
515
832
|
} else scanWrites(d, false)
|
|
516
833
|
}
|
|
517
834
|
return
|
|
518
835
|
}
|
|
519
|
-
|
|
836
|
+
// Any assign-op (`=`, `??=`, `||=`, `+=`, …) or `++`/`--` on a global outside
|
|
837
|
+
// unconditional init poisons it — the house write predicate used throughout
|
|
838
|
+
// prepare/narrow/emit (`ASSIGN_OPS.has(op) || op==='++' || op==='--'`).
|
|
839
|
+
if (ASSIGN_OPS.has(op) || op === '++' || op === '--') {
|
|
520
840
|
if (!topInit && isGlobal(node[1])) poison.add(node[1])
|
|
521
841
|
scanWrites(node[1], false)
|
|
522
|
-
scanWrites(node[2], false)
|
|
842
|
+
scanWrites(node[2], chainedWrite(node[2]) ? topInit : false)
|
|
523
843
|
return
|
|
524
844
|
}
|
|
525
845
|
for (let i = 1; i < node.length; i++) scanWrites(node[i], false)
|
|
@@ -530,13 +850,94 @@ export const devirtGlobalCalls = (ast) => {
|
|
|
530
850
|
|
|
531
851
|
// Resolve each global's value by a linear pass over init in execution order.
|
|
532
852
|
const env = new Map()
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
853
|
+
|
|
854
|
+
// Free identifiers referenced by `node`, skipping property-name / literal-
|
|
855
|
+
// key positions (mirrors ast.js's REFS_IN_EXPR convention: `.`/`?.` only
|
|
856
|
+
// recurse the receiver, `:` only the value) and skipping anything bound by a
|
|
857
|
+
// `=>` param, `let`/`const`, or `catch` clause anywhere within — the same
|
|
858
|
+
// "hoist every local to function scope" approximation the other body-local
|
|
859
|
+
// scans in this file use (over-inclusive `bound` only makes lifting MORE
|
|
860
|
+
// conservative, never less sound).
|
|
861
|
+
const collectFreeIdents = (node, bound, out) => {
|
|
862
|
+
if (!Array.isArray(node)) return
|
|
863
|
+
const op = node[0]
|
|
864
|
+
if (op === '=>') { collectParamNames(extractParams(node[1]), bound); collectFreeIdents(node[2], bound, out); return }
|
|
865
|
+
if (op === 'str') return
|
|
866
|
+
if (op === '.' || op === '?.') { collectFreeIdents(node[1], bound, out); return }
|
|
867
|
+
if (op === ':') { collectFreeIdents(node[2], bound, out); return }
|
|
868
|
+
if (op === 'catch' && typeof node[2] === 'string') bound.add(node[2])
|
|
869
|
+
if (op === 'let' || op === 'const') collectParamNames(node.slice(1), bound)
|
|
870
|
+
for (let i = 1; i < node.length; i++) {
|
|
871
|
+
const c = node[i]
|
|
872
|
+
if (typeof c === 'string') { if (!bound.has(c)) out.add(c) }
|
|
873
|
+
else collectFreeIdents(c, bound, out)
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Lift a module-init-time arrow literal into a standalone top-level function
|
|
878
|
+
// so its resolved VALUE (the function name) can flow through env like any
|
|
879
|
+
// other devirt candidate — subscript's `parse.space = (r,e) => {…}` shape
|
|
880
|
+
// once flattenFuncNamespaces turns the write into a plain global assign, or
|
|
881
|
+
// an arrow reached one hop through a chainedWrite. Sound only when the arrow
|
|
882
|
+
// captures nothing from an enclosing function's locals: at true module-init
|
|
883
|
+
// depth that's automatic (no enclosing function exists at all — initStmts
|
|
884
|
+
// only holds top-level statements, and this walk never enters a `=>` body
|
|
885
|
+
// except the candidate's own), but an arrow nested inside another init
|
|
886
|
+
// expression is still checked, fail-closed — any free identifier that isn't
|
|
887
|
+
// a module global or a top-level function name aborts the lift, as does any
|
|
888
|
+
// non-plain (rest/default/destructured) param. The ORIGINAL arrow node is
|
|
889
|
+
// left untouched in place: this only ADDS a new function; whatever produced
|
|
890
|
+
// the arrow as a value (closure.make at its own AST position) keeps working
|
|
891
|
+
// unchanged for any other, non-call use of the same global.
|
|
892
|
+
const liftArrow = (node) => {
|
|
893
|
+
const params = []
|
|
894
|
+
for (const p of extractParams(node[1])) {
|
|
895
|
+
const c = classifyParam(p)
|
|
896
|
+
if (c.kind !== 'plain') return null
|
|
897
|
+
params.push(c.name)
|
|
898
|
+
}
|
|
899
|
+
const bound = new Set(params)
|
|
900
|
+
const free = new Set()
|
|
901
|
+
collectFreeIdents(node[2], bound, free)
|
|
902
|
+
for (const name of free) if (!ctx.scope.globals.has(name) && !fnNames.has(name)) return null
|
|
903
|
+
|
|
904
|
+
const name = `${T}devirt${ctx.func.uniq++}`
|
|
905
|
+
const funcInfo = { name, body: node[2], exported: false, sig: { params: params.map(n => ({ name: n, type: 'f64' })), results: ['f64'] } }
|
|
906
|
+
ctx.func.list.push(funcInfo)
|
|
907
|
+
ctx.func.map.set(name, funcInfo)
|
|
908
|
+
fnNames.add(name)
|
|
909
|
+
return name
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// A write node's resolved value: `=` always takes the RHS; `??=`/`||=` keep
|
|
913
|
+
// G's prior env value when one was already recorded (sound because a prior
|
|
914
|
+
// value present in env is always a function — non-nullish AND truthy, so
|
|
915
|
+
// both operators short-circuit without evaluating the RHS at all) and fall
|
|
916
|
+
// back to resolving the RHS only when no prior write was seen (sound because
|
|
917
|
+
// an as-yet-unwritten SROA/module global's declared init is the shared
|
|
918
|
+
// undefined atom — nullish and falsy). Any OTHER compound op (`+=`, `&&=`, …)
|
|
919
|
+
// can't be interpreted here — recorded as a write with an unknown value
|
|
920
|
+
// rather than left silently unset, so a LATER `??=`/`||=` on the same global
|
|
921
|
+
// can't mistake "written but uninterpretable" for "never written".
|
|
922
|
+
const resolveWriteNode = (n) => {
|
|
923
|
+
const [op, g, v] = n
|
|
924
|
+
const val = op === '='
|
|
925
|
+
? resolveValue(v)
|
|
926
|
+
: (op === '??=' || op === '||=')
|
|
927
|
+
? (env.has(g) ? env.get(g) : resolveValue(v))
|
|
928
|
+
: null
|
|
929
|
+
env.set(g, val)
|
|
930
|
+
return val
|
|
931
|
+
}
|
|
932
|
+
const resolveValue = (v) => {
|
|
933
|
+
if (typeof v === 'string') return fnNames.has(v) ? v : env.has(v) ? env.get(v) : null
|
|
934
|
+
if (!Array.isArray(v)) return null
|
|
935
|
+
if (v[0] === '=>') return liftArrow(v)
|
|
936
|
+
if (chainedWrite(v) && isGlobal(v[1])) return resolveWriteNode(v)
|
|
937
|
+
return null
|
|
938
|
+
}
|
|
538
939
|
for (const stmt of initStmts)
|
|
539
|
-
for (const [g,
|
|
940
|
+
for (const [g, valueNode] of writesOf(stmt)) env.set(g, resolveValue(valueNode))
|
|
540
941
|
|
|
541
942
|
const devirt = new Map()
|
|
542
943
|
for (const [g, fn] of env)
|
|
@@ -646,4 +1047,10 @@ export const canSkipWholeProgramNarrowing = (programFacts) =>
|
|
|
646
1047
|
!programFacts.anyDyn &&
|
|
647
1048
|
programFacts.propMap.size === 0 &&
|
|
648
1049
|
!programFacts.hasSchemaLiterals &&
|
|
649
|
-
!ctx.closure.make
|
|
1050
|
+
!ctx.closure.make &&
|
|
1051
|
+
// Typed default-arg annotations (`arr = new Int32Array(0)`) feed the param
|
|
1052
|
+
// lattice even with zero call sites — a host-called SPMD kernel (Workers v1)
|
|
1053
|
+
// gets its pointer-ABI lane and Atomics receiver proof from exactly this.
|
|
1054
|
+
!ctx.func.list.some(f => f.defaults && Object.values(f.defaults).some(d =>
|
|
1055
|
+
Array.isArray(d) && d[0] === '()' && typeof d[1] === 'string' &&
|
|
1056
|
+
d[1].startsWith('new.') && d[1].endsWith('Array')))
|