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
|
@@ -25,18 +25,22 @@
|
|
|
25
25
|
* @module compile
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import
|
|
29
|
-
import { ctx, err, inc, resolveIncludes, PTR, LAYOUT } from '
|
|
28
|
+
import parseWat from 'watr/parse'
|
|
29
|
+
import { ctx, err, inc, resolveIncludes, PTR, LAYOUT, declGlobal } from '../ctx.js'
|
|
30
|
+
import { T, isBlockBody, isReassigned, refsName, REFS_IN_EXPR } from '../ast.js'
|
|
31
|
+
import { intLiteralValue } from '../static.js'
|
|
30
32
|
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
boxedCaptures, updateRep,
|
|
34
|
-
isBlockBody, analyzeStructInline,
|
|
33
|
+
analyzeBody, unboxablePtrs, cseSafeLoadBases, boxedCaptures,
|
|
34
|
+
analyzeStructInline, invalidateLocalsCache,
|
|
35
35
|
} from './analyze.js'
|
|
36
|
+
import { typedElemAux } from '../../layout.js'
|
|
37
|
+
import { VAL, updateRep, REP_FIELDS } from '../reps.js'
|
|
36
38
|
import { inferLocals } from './infer.js'
|
|
37
|
-
import { optimizeFunc, treeshake } from '
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
39
|
+
import { optimizeFunc, treeshake } from '../optimize/index.js'
|
|
40
|
+
import { strengthReduceLoopDivMod } from './loop-divmod.js'
|
|
41
|
+
import { peelClampedStencil } from './peel-stencil.js'
|
|
42
|
+
import { emit, emitter, emitVoid, emitBlockBody } from './emit.js'
|
|
43
|
+
import { emitCharDecompPrologue, JSS_IMPORT_SIGS } from '../abi/string.js'
|
|
40
44
|
import {
|
|
41
45
|
typed, asF64, asI32, asPtrOffset, asParamType, toI32, asI64, fromI64,
|
|
42
46
|
NULL_NAN, UNDEF_NAN, NULL_WAT, UNDEF_WAT, NULL_IR, UNDEF_IR, nullExpr, undefExpr,
|
|
@@ -49,13 +53,14 @@ import {
|
|
|
49
53
|
multiCount, loopTop, flat, reconstructArgsWithSpreads,
|
|
50
54
|
valKindToPtr, findBodyStart, tcoTailRewrite,
|
|
51
55
|
boolBoxIR,
|
|
52
|
-
I32_MIN, I32_MAX,
|
|
53
|
-
} from '
|
|
54
|
-
import plan from './plan.js'
|
|
56
|
+
I32_MIN, I32_MAX, dollar,
|
|
57
|
+
} from '../ir.js'
|
|
58
|
+
import plan from './plan/index.js'
|
|
59
|
+
import { foldStaticConstAggregates } from './plan/literals.js'
|
|
55
60
|
import {
|
|
56
61
|
buildStartFn, dedupClosureBodies, finalizeClosureTable,
|
|
57
|
-
pullStdlib, syncImports, optimizeModule, stripStaticDataPrefix,
|
|
58
|
-
} from '
|
|
62
|
+
pullStdlib, syncImports, optimizeModule, stripStaticDataPrefix, hoistConstGlobalInits,
|
|
63
|
+
} from '../wat/assemble.js'
|
|
59
64
|
|
|
60
65
|
// =============================================================================
|
|
61
66
|
// Single-source export semantics
|
|
@@ -94,17 +99,18 @@ const isExported = f => {
|
|
|
94
99
|
return false
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
/**
|
|
98
|
-
* per-export ABI metadata in custom sections — one entry per
|
|
99
|
-
* since the host (interop.js wrap) keys by export name. */
|
|
100
|
-
function
|
|
102
|
+
/** Collect JS-visible export names that resolve to `funcName` (as an array).
|
|
103
|
+
* Used to emit per-export ABI metadata in custom sections — one entry per
|
|
104
|
+
* JS-visible name, since the host (interop.js wrap) keys by export name. */
|
|
105
|
+
function exportNamesOf(funcName) {
|
|
106
|
+
const names = []
|
|
101
107
|
for (const [key, val] of Object.entries(ctx.func.exports)) {
|
|
102
|
-
if (val === true && key === funcName)
|
|
103
|
-
else if (val === funcName) yield key
|
|
108
|
+
if ((val === true && key === funcName) || val === funcName) names.push(key)
|
|
104
109
|
}
|
|
110
|
+
return names
|
|
105
111
|
}
|
|
106
112
|
|
|
107
|
-
const timePhase = (profiler, name, fn) => profiler ? profiler.time(name, fn) : fn()
|
|
113
|
+
const timePhase = (profiler, name, fn) => profiler?.time ? profiler.time(name, fn) : fn()
|
|
108
114
|
|
|
109
115
|
// Per-compile func name set + map live on ctx.func.names / ctx.func.map,
|
|
110
116
|
// populated at compile() entry. Both reset by ctx.js reset() and re-filled here.
|
|
@@ -113,8 +119,7 @@ const timePhase = (profiler, name, fn) => profiler ? profiler.time(name, fn) : f
|
|
|
113
119
|
// emit-calling ones (toBool, emitTypeofCmp, emitDecl, materializeMulti,
|
|
114
120
|
// buildArrayWithSpreads) moved to src/emit.js.
|
|
115
121
|
|
|
116
|
-
// AST-analysis primitives
|
|
117
|
-
// infer* cross-call inference, collectProgramFacts) moved to src/analyze.js.
|
|
122
|
+
// AST-analysis primitives live in kind.js, type.js, static.js, program-facts.js.
|
|
118
123
|
|
|
119
124
|
/**
|
|
120
125
|
* Boundary-wrap predicate: exports whose body-driven result OR any param narrowed
|
|
@@ -133,9 +138,62 @@ const isBoundaryWrapped = (func) => {
|
|
|
133
138
|
// A boolean result rides the 0/1 number carrier internally; the export thunk
|
|
134
139
|
// boxes it to the TRUE_NAN/FALSE_NAN atom so the host sees a real boolean.
|
|
135
140
|
if (func.valResult === VAL.BOOL) return true
|
|
141
|
+
// A bigint result rides the i64-reinterpreted-f64 carrier internally; the export thunk converts
|
|
142
|
+
// it to a real Number so a JS host doesn't see raw i64 bits (`() => 100n` was returning 4.94e-322).
|
|
143
|
+
if (func.valResult === VAL.BIGINT) return true
|
|
136
144
|
return func.sig.params.some(p => p.type !== 'f64' || p.ptrKind != null)
|
|
137
145
|
}
|
|
138
146
|
|
|
147
|
+
// Static-string intern index (the `internStrings` pass). Open-addressing table
|
|
148
|
+
// over the deduped static string literals (5–32 bytes): [hash u32][ptr u32]
|
|
149
|
+
// pairs appended to the data segment, FNV-1a matching __str_hash's heap branch.
|
|
150
|
+
// __str_slice/__str_slice_view probe it so a runtime substring whose content
|
|
151
|
+
// equals any source literal returns the CANONICAL static pointer — string
|
|
152
|
+
// equality then short-circuits on bit-eq instead of walking bytes (a compiler
|
|
153
|
+
// or parser compares each token against tag literals many times; ~25% of
|
|
154
|
+
// self-host compile time was __str_eq/__eq/__str_hash volume). Built before
|
|
155
|
+
// pullStdlib (the slice thunks emit the probe only when `__internBase` exists);
|
|
156
|
+
// stripStaticDataPrefix shifts the stored ptr slots like every other static
|
|
157
|
+
// reference. Misses cost one FNV + one probe per slice; the table is read-only.
|
|
158
|
+
function buildInternTable() {
|
|
159
|
+
const cfg = ctx.transform.optimize
|
|
160
|
+
if (!cfg || cfg.internStrings === false) return
|
|
161
|
+
if (ctx.memory.shared || !ctx.runtime.dataDedup?.size) return
|
|
162
|
+
const enc = new TextEncoder()
|
|
163
|
+
const entries = []
|
|
164
|
+
for (const [str, off] of ctx.runtime.dataDedup) {
|
|
165
|
+
const b = enc.encode(str)
|
|
166
|
+
if (b.length < 5 || b.length > 32) continue
|
|
167
|
+
let h = 0x811c9dc5 | 0
|
|
168
|
+
for (let i = 0; i < b.length; i++) h = Math.imul(h ^ b[i], 0x01000193) | 0
|
|
169
|
+
if (h <= 1) h = (h + 2) | 0 // mirror __str_hash's empty/tombstone clamp
|
|
170
|
+
entries.push([h >>> 0, off + 8])
|
|
171
|
+
}
|
|
172
|
+
if (!entries.length) return
|
|
173
|
+
let size = 4
|
|
174
|
+
while (size < entries.length * 2) size = (size * 2) | 0
|
|
175
|
+
const mask = size - 1
|
|
176
|
+
const slots = new Uint32Array(size * 2)
|
|
177
|
+
for (let e = 0; e < entries.length; e++) {
|
|
178
|
+
const h = entries[e][0], off = entries[e][1]
|
|
179
|
+
let i = h & mask
|
|
180
|
+
while (slots[i * 2 + 1] !== 0) i = (i + 1) & mask
|
|
181
|
+
slots[i * 2] = h
|
|
182
|
+
slots[i * 2 + 1] = off
|
|
183
|
+
}
|
|
184
|
+
while (ctx.runtime.data.length % 8 !== 0) ctx.runtime.data += '\0'
|
|
185
|
+
const base = ctx.runtime.data.length
|
|
186
|
+
let s = ''
|
|
187
|
+
for (let i = 0; i < slots.length; i++) {
|
|
188
|
+
const v = slots[i]
|
|
189
|
+
s += String.fromCharCode(v & 0xFF, (v >>> 8) & 0xFF, (v >>> 16) & 0xFF, (v >>> 24) & 0xFF)
|
|
190
|
+
}
|
|
191
|
+
ctx.runtime.data += s
|
|
192
|
+
ctx.runtime.internTable = { base, size }
|
|
193
|
+
declGlobal('__internBase', 'i32', base, { mut: false })
|
|
194
|
+
declGlobal('__internMask', 'i32', mask, { mut: false })
|
|
195
|
+
}
|
|
196
|
+
|
|
139
197
|
const ensureThrowRuntime = (sec) => {
|
|
140
198
|
// A pulled stdlib helper may throw $__jz_err even when no user `throw` set the
|
|
141
199
|
// flag (e.g. __to_num on a Symbol). Detect it from the included stdlib bodies
|
|
@@ -147,7 +205,7 @@ const ensureThrowRuntime = (sec) => {
|
|
|
147
205
|
if (!ctx.runtime.throws) return
|
|
148
206
|
|
|
149
207
|
if (!ctx.scope.globals.has('__jz_last_err_bits'))
|
|
150
|
-
|
|
208
|
+
declGlobal('__jz_last_err_bits', 'i64')
|
|
151
209
|
if (!sec.tags.some(t => Array.isArray(t) && t[0] === 'tag' && t[1] === '$__jz_err'))
|
|
152
210
|
sec.tags.push(['tag', '$__jz_err', ['param', 'f64']])
|
|
153
211
|
if (!sec.tags.some(t => Array.isArray(t) && t[0] === 'export' && t[1] === '"__jz_last_err_bits"'))
|
|
@@ -181,13 +239,12 @@ const pruneUnusedThrowRuntime = (sec) => {
|
|
|
181
239
|
const cloneRepMap = map => map ? new Map([...map].map(([k, v]) => [k, { ...v }])) : null
|
|
182
240
|
|
|
183
241
|
/** Serialize a ValueRep entry into a plain object for inspect output.
|
|
184
|
-
* Omits undefined fields so consumers can JSON-stringify without noise.
|
|
242
|
+
* Omits undefined fields so consumers can JSON-stringify without noise.
|
|
243
|
+
* Iterates REP_FIELDS (the closed shape in reps.js) so it can't drift. */
|
|
185
244
|
const repView = (rep) => {
|
|
186
245
|
if (!rep) return null
|
|
187
246
|
const out = {}
|
|
188
|
-
for (const k of
|
|
189
|
-
if (rep[k] != null) out[k] = rep[k]
|
|
190
|
-
}
|
|
247
|
+
for (const k of REP_FIELDS) if (rep[k] != null) out[k] = rep[k]
|
|
191
248
|
return Object.keys(out).length ? out : null
|
|
192
249
|
}
|
|
193
250
|
|
|
@@ -230,18 +287,80 @@ function captureFuncInspect(func, facts, programFacts) {
|
|
|
230
287
|
}
|
|
231
288
|
}
|
|
232
289
|
|
|
290
|
+
function scanAndTagNonEscapingClosures(body) {
|
|
291
|
+
if (!body) return
|
|
292
|
+
const onlyCalledNotReferenced = (node, name) => {
|
|
293
|
+
if (typeof node === 'string') return node !== name
|
|
294
|
+
if (!Array.isArray(node)) return true
|
|
295
|
+
const op = node[0]
|
|
296
|
+
if (op === 'str') return true
|
|
297
|
+
if (op === '=>') {
|
|
298
|
+
return !refsName(node[1], name, REFS_IN_EXPR) && !refsName(node[2], name, REFS_IN_EXPR)
|
|
299
|
+
}
|
|
300
|
+
if (op === '=' && node[1] === name) {
|
|
301
|
+
return onlyCalledNotReferenced(node[2], name)
|
|
302
|
+
}
|
|
303
|
+
if (op === '()' && node[1] === name) {
|
|
304
|
+
for (let i = 2; i < node.length; i++) {
|
|
305
|
+
if (!onlyCalledNotReferenced(node[i], name)) return false
|
|
306
|
+
}
|
|
307
|
+
return true
|
|
308
|
+
}
|
|
309
|
+
if (op === '.' || op === '?.') return onlyCalledNotReferenced(node[1], name)
|
|
310
|
+
if (op === ':') return onlyCalledNotReferenced(node[2], name)
|
|
311
|
+
for (let i = 1; i < node.length; i++) {
|
|
312
|
+
if (!onlyCalledNotReferenced(node[i], name)) return false
|
|
313
|
+
}
|
|
314
|
+
return true
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const walk = (node) => {
|
|
318
|
+
if (!Array.isArray(node)) return
|
|
319
|
+
const op = node[0]
|
|
320
|
+
if (op === 'let' || op === 'const') {
|
|
321
|
+
for (const decl of node.slice(1)) {
|
|
322
|
+
if (Array.isArray(decl) && decl[0] === '=' && typeof decl[1] === 'string') {
|
|
323
|
+
const name = decl[1]
|
|
324
|
+
const init = decl[2]
|
|
325
|
+
if (Array.isArray(init) && init[0] === '=>') {
|
|
326
|
+
const arrow_body = init[2]
|
|
327
|
+
if (arrow_body && typeof arrow_body === 'object' && !ctx.func.boxed?.has(name) && !isGlobal(name) && !isReassigned(body, name) && onlyCalledNotReferenced(body, name)) {
|
|
328
|
+
arrow_body._nonEscaping = name
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
} else if (op === '=' && typeof node[1] === 'string' && Array.isArray(node[2]) && node[2][0] === '=>') {
|
|
334
|
+
const name = node[1]
|
|
335
|
+
const init = node[2]
|
|
336
|
+
const arrow_body = init[2]
|
|
337
|
+
if (arrow_body && typeof arrow_body === 'object' && !ctx.func.boxed?.has(name) && !isGlobal(name) && !isReassigned(body, name) && onlyCalledNotReferenced(body, name)) {
|
|
338
|
+
arrow_body._nonEscaping = name
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
342
|
+
}
|
|
343
|
+
walk(body)
|
|
344
|
+
}
|
|
345
|
+
|
|
233
346
|
// Reset per-function emit-frame state — the single source of frame entry.
|
|
234
347
|
// `emitFunc`, `analyzeFuncForEmit`, and `emitClosureBody` all route through
|
|
235
348
|
// here. Top-level funcs start `uniq` at 0; closures pass a higher base so
|
|
236
349
|
// their synthetic labels can't collide with the parent frame's.
|
|
237
350
|
function enterFunc(sig, body, { uniq = 0, directClosures = null } = {}) {
|
|
238
351
|
ctx.func.stack = []
|
|
352
|
+
ctx.func.zeroInitSeen = new Set() // names whose `let x=0` zero-init was elided once; a 2nd is a real re-init (unrolled bodies)
|
|
353
|
+
ctx.func.maybeNullish = new Set() // bindings assigned a nullish literal → coerce in arithmetic (null-flow)
|
|
354
|
+
ctx.func.pendingLabel = null // label awaiting its loop, for `continue <label>`
|
|
239
355
|
ctx.func.uniq = uniq
|
|
240
356
|
ctx.func.current = sig
|
|
241
357
|
ctx.func.body = body
|
|
242
358
|
ctx.func.directClosures = directClosures
|
|
243
359
|
ctx.func.localProps = null
|
|
244
360
|
ctx.func.charDecomp = null
|
|
361
|
+
if (ctx.transform.optimize) {
|
|
362
|
+
scanAndTagNonEscapingClosures(body)
|
|
363
|
+
}
|
|
245
364
|
}
|
|
246
365
|
|
|
247
366
|
// Allocate + null-init a heap cell for every boxed local that isn't seeded
|
|
@@ -264,6 +383,15 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
264
383
|
const { paramReps } = programFacts
|
|
265
384
|
if (func.raw) return null
|
|
266
385
|
|
|
386
|
+
// Strength-reduce per-iteration `i % w` / `(i/w)|0` to incremental i32 counters
|
|
387
|
+
// (idempotent: a reduced loop has no modulo left to match). Before analyze so the
|
|
388
|
+
// counters are typed/narrowed like any i32 local. Off at L0 / `loopIVDivMod:false`.
|
|
389
|
+
const _o = ctx.transform.optimize
|
|
390
|
+
if (_o && _o.loopIVDivMod !== false && isBlockBody(func.body)) func.body = strengthReduceLoopDivMod(func.body)
|
|
391
|
+
// Edge-clamp peeling: split a clamped stencil loop into clamp-free interior + edges
|
|
392
|
+
// (the interior then lifts to SIMD). Before analyze so the new loops are analyzed.
|
|
393
|
+
if (_o && _o.clampPeel !== false && isBlockBody(func.body)) func.body = peelClampedStencil(func.body)
|
|
394
|
+
|
|
267
395
|
const { name, body, sig } = func
|
|
268
396
|
enterFunc(sig, body)
|
|
269
397
|
|
|
@@ -288,6 +416,31 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
288
416
|
if (r.intConst != null) updateRep(pname, { intConst: r.intConst })
|
|
289
417
|
}
|
|
290
418
|
}
|
|
419
|
+
// Trust numeric export params. An exported f64 param used only in numeric
|
|
420
|
+
// positions is marked VAL.NUMBER so its uses skip the `__to_num` coercion
|
|
421
|
+
// entirely (not just hoist it). External callers reach jz through interop's
|
|
422
|
+
// `mem.wrapVal`, which passes a JS number straight to f64 — so the coercion
|
|
423
|
+
// only ever fired for a *string* arg to a numeric param (a type misuse). When
|
|
424
|
+
// that lone coercion is the only `__to_num` consumer, dropping it lets the whole
|
|
425
|
+
// ToNumber string-parse dep tree (`__to_str`→`__itoa`/`__toExp`/`__mkstr`/…)
|
|
426
|
+
// treeshake away — a ~4× module shrink that, decisively, lets V8 tier the hot
|
|
427
|
+
// fill loop up properly (the bloated module JITs the *identical* loop ~2× slower).
|
|
428
|
+
if (func.exported && block) {
|
|
429
|
+
for (const p of sig.params) {
|
|
430
|
+
if (p.type === 'f64' && p.ptrKind == null && !p.jsstring
|
|
431
|
+
&& !func.defaults?.[p.name] && !ctx.func.boxed?.has(p.name)
|
|
432
|
+
&& !ctx.func.localReps?.get(p.name)?.val
|
|
433
|
+
// Numeric either by PROOF (ToNumber-forcing uses) or by the export
|
|
434
|
+
// boundary contract (never used as a string → wrapVal guarantees a
|
|
435
|
+
// number). The latter catches `acc + cre` float kernels whose `+` would
|
|
436
|
+
// otherwise pull a per-iteration string-concat fork (julia, floatbeats).
|
|
437
|
+
&& (paramAllUsesNumeric(body, p.name) || paramNeverString(body, p.name)))
|
|
438
|
+
updateRep(p.name, { val: VAL.NUMBER })
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (block) {
|
|
442
|
+
seedLocalIntConsts(body)
|
|
443
|
+
}
|
|
291
444
|
// Drop any earlier-cached analyzeBody.locals slice for this body —
|
|
292
445
|
// narrowSignatures called it before our pre-seed, when params still had no
|
|
293
446
|
// inferred VAL.TYPED, so the cached widths reflect the pre-narrow state.
|
|
@@ -295,6 +448,11 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
295
448
|
invalidateLocalsCache(body)
|
|
296
449
|
const bodyFacts = block ? analyzeBody(body) : null
|
|
297
450
|
ctx.func.locals = bodyFacts ? bodyFacts.locals : new Map()
|
|
451
|
+
if (bodyFacts?.valTypes) {
|
|
452
|
+
for (const [name, vt] of bodyFacts.valTypes) updateRep(name, { val: vt })
|
|
453
|
+
}
|
|
454
|
+
// Never-relocated array bindings — the `[]` reader skips the forwarding follow.
|
|
455
|
+
if (bodyFacts?.neverGrown) for (const name of bodyFacts.neverGrown) updateRep(name, { neverGrown: true })
|
|
298
456
|
// Proven uint32 accumulator locals — readVar tags reads `.unsigned` so the
|
|
299
457
|
// f64 round-trip widens with convert_i32_u (not _s).
|
|
300
458
|
if (bodyFacts?.unsignedLocals) for (const n of bodyFacts.unsignedLocals) updateRep(n, { unsigned: true })
|
|
@@ -351,6 +509,9 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
351
509
|
if (p.ptrAux != null) fields.ptrAux = p.ptrAux
|
|
352
510
|
updateRep(p.name, fields)
|
|
353
511
|
}
|
|
512
|
+
for (const p of sig.params) {
|
|
513
|
+
if (p.jsstring) updateRep(p.name, { carrier: 'jsstring', val: VAL.STRING })
|
|
514
|
+
}
|
|
354
515
|
|
|
355
516
|
// CSE-safe load bases — pointer locals whose memory reads `cseScalarLoad`
|
|
356
517
|
// may scalar-replace. Computed last: needs every `let`/param ptrKind in place.
|
|
@@ -358,10 +519,23 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
358
519
|
? cseSafeLoadBases(body, ctx.func.locals, ctx.func.localReps)
|
|
359
520
|
: new Set()
|
|
360
521
|
|
|
522
|
+
// Closure-capture narrowing: a boxed var whose every defining RHS — owner
|
|
523
|
+
// body AND nested arrows, the narrower's intCertain contract — is integer-
|
|
524
|
+
// valued keeps its CELL in i32, so readVar/writeVar skip the f64↔i32
|
|
525
|
+
// round-trip per access. Params are excluded: their cell is seeded from the
|
|
526
|
+
// raw f64 param value, which would desync an i32-read cell. Same asm.js-style
|
|
527
|
+
// range contract as plain intCertain locals.
|
|
528
|
+
const cellTypes = new Set()
|
|
529
|
+
for (const name of ctx.func.boxed.keys()) {
|
|
530
|
+
if (sig.params.some(p => p.name === name)) continue
|
|
531
|
+
if (ctx.func.localReps?.get(name)?.intCertain === true) cellTypes.add(name)
|
|
532
|
+
}
|
|
533
|
+
|
|
361
534
|
return {
|
|
362
535
|
block,
|
|
363
536
|
locals: new Map(ctx.func.locals),
|
|
364
537
|
boxed: new Map(ctx.func.boxed),
|
|
538
|
+
cellTypes,
|
|
365
539
|
flatObjects: new Map(ctx.func.flatObjects),
|
|
366
540
|
sliceViews: new Set(ctx.func.sliceViews),
|
|
367
541
|
cseLoadBases,
|
|
@@ -370,6 +544,367 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
370
544
|
}
|
|
371
545
|
}
|
|
372
546
|
|
|
547
|
+
function seedLocalIntConsts(body) {
|
|
548
|
+
// Fold each never-reassigned local `const`/`let NAME = EXPR` to a known i32, so a
|
|
549
|
+
// divisor / bound / size built from earlier consts (`rr = R|0; win = 2*rr+1`) becomes
|
|
550
|
+
// a compile-time literal — which lets the int-divide lowering hand the wasm backend a
|
|
551
|
+
// constant divisor to magic-multiply (no runtime sdiv), array bounds resolve, etc.
|
|
552
|
+
// Mirrors the module-scope fold (evalConst above); a string ref resolves through the
|
|
553
|
+
// intConst already recorded on its rep, and the fixpoint lets a later const see an
|
|
554
|
+
// earlier one regardless of declaration order. Skips nested functions (own scope).
|
|
555
|
+
const evalC = (n) => {
|
|
556
|
+
if (typeof n === 'number') return Number.isInteger(n) ? n : null
|
|
557
|
+
if (Array.isArray(n) && n[0] == null && typeof n[1] === 'number') return Number.isInteger(n[1]) ? n[1] : null
|
|
558
|
+
if (typeof n === 'string') return intLiteralValue(n) // a seeded intConst / literal local
|
|
559
|
+
if (!Array.isArray(n)) return null
|
|
560
|
+
const [op, a, b] = n
|
|
561
|
+
const va = evalC(a); if (va == null) return null
|
|
562
|
+
if (op === 'u-' || (op === '-' && b === undefined)) return -va
|
|
563
|
+
const vb = evalC(b); if (vb == null) return null
|
|
564
|
+
switch (op) {
|
|
565
|
+
case '+': return va + vb; case '-': return va - vb; case '*': return va * vb
|
|
566
|
+
case '&': return va & vb; case '|': return va | vb; case '^': return va ^ vb
|
|
567
|
+
case '<<': return va << vb; case '>>': return va >> vb; case '>>>': return va >>> vb
|
|
568
|
+
default: return null
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const decls = []
|
|
572
|
+
const walk = (node) => {
|
|
573
|
+
if (!Array.isArray(node)) return
|
|
574
|
+
const [op, ...args] = node
|
|
575
|
+
if (op === '=>') return
|
|
576
|
+
if (op === 'let' || op === 'const') {
|
|
577
|
+
for (const decl of args)
|
|
578
|
+
if (Array.isArray(decl) && decl[0] === '=' && typeof decl[1] === 'string' && !isReassigned(body, decl[1])) decls.push(decl)
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
for (const arg of args) walk(arg)
|
|
582
|
+
}
|
|
583
|
+
walk(body)
|
|
584
|
+
const seeded = new Set()
|
|
585
|
+
let changed = true
|
|
586
|
+
while (changed) {
|
|
587
|
+
changed = false
|
|
588
|
+
for (const decl of decls) {
|
|
589
|
+
if (seeded.has(decl[1])) continue
|
|
590
|
+
const value = evalC(decl[2])
|
|
591
|
+
if (value != null && Number.isInteger(value) && value >= I32_MIN && value <= I32_MAX) {
|
|
592
|
+
updateRep(decl[1], { intConst: value }); seeded.add(decl[1]); changed = true
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ── Loop-invariant exported-param coercion hoist ────────────────────────────
|
|
599
|
+
//
|
|
600
|
+
// An exported numeric param arrives as a NaN-box (jz's value ABI), so each use
|
|
601
|
+
// in an arithmetic context emits `__to_num(p)`. When the param is never
|
|
602
|
+
// reassigned and *every* use is an unconditional-ToNumber arithmetic operand,
|
|
603
|
+
// the coercion is loop-invariant: do it once at entry and let every use read the
|
|
604
|
+
// already-unboxed f64. This flips a serial recurrence like the de Jong attractor
|
|
605
|
+
// (4 `__to_num`/iter × millions) from ~parity to a clear win over V8.
|
|
606
|
+
//
|
|
607
|
+
// Self-gating: the rewrite only fires when the emitted body ALREADY contains
|
|
608
|
+
// `__to_num(p)` calls — meaning the helper is loaded for other reasons (global
|
|
609
|
+
// typed-array assigns, strings, …). A provably-numeric program (`(a,b)=>a*b`)
|
|
610
|
+
// never loads the helper, has no pattern to match, and is left byte-for-byte
|
|
611
|
+
// alone, preserving the minimal-bundle / golden-size guarantee.
|
|
612
|
+
|
|
613
|
+
// `=`/`+=`/`++`/… targets — reassigning the param breaks the coerce-once premise.
|
|
614
|
+
const PARAM_REASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=',
|
|
615
|
+
'^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??=', '++', '--'])
|
|
616
|
+
// Binary ops that unconditionally ToNumber BOTH operands, so a bare param operand
|
|
617
|
+
// is a pure numeric use. `+` is excluded (may concatenate); `===`/`==` are excluded
|
|
618
|
+
// (they branch on type, never coerce a string operand to number).
|
|
619
|
+
const NUM_BIN_OPS = new Set(['*', '/', '%', '**', '&', '|', '^', '<<', '>>', '>>>'])
|
|
620
|
+
// Relational ops: jz has no lexicographic compare for an untyped operand — `<`
|
|
621
|
+
// lowers to `f64.lt`, taking the string path only when a *known-string* operand
|
|
622
|
+
// is present (emit.js cmpOp). So a bare param compared against a non-string is a
|
|
623
|
+
// pure numeric use, same as NUM_BIN_OPS. A string-literal counterpart (`x < "m"`)
|
|
624
|
+
// signals string intent and is rejected (handled in the walk below).
|
|
625
|
+
const REL_OPS = new Set(['<', '<=', '>', '>='])
|
|
626
|
+
// A string literal/template operand poisons relational numeric inference.
|
|
627
|
+
const isStrLiteral = (n) => Array.isArray(n) && (n[0] === 'str' || n[0] === 'template')
|
|
628
|
+
|
|
629
|
+
/** True iff every use of param `name` in `body` is numeric-COMPATIBLE *and* at
|
|
630
|
+
* least one use is numeric-PROVING — so coercing it to a number once at entry is
|
|
631
|
+
* observationally exact. Two verdict levels guard against a polymorphic slot
|
|
632
|
+
* passing on absence of evidence:
|
|
633
|
+
* - PROVING (`proven=true`): arithmetic / relational / bitwise / unary operand —
|
|
634
|
+
* JS ToNumbers these, and a string/array value would have shown a disqualifying
|
|
635
|
+
* use elsewhere.
|
|
636
|
+
* - COMPATIBLE-ONLY: the length slot of `new TypedArray(x)` / `new ArrayBuffer(x)`.
|
|
637
|
+
* A number sizes the buffer, but an array is COPIED and a buffer VIEWED — so a
|
|
638
|
+
* bare param here proves nothing. A param used *solely* as `new Float64Array(arr)`
|
|
639
|
+
* stays unproven and keeps the polymorphic ctor dispatch (else array-copy is lost).
|
|
640
|
+
* Any other appearance (member/call-arg/return/concat/`===`/reassignment) rejects.
|
|
641
|
+
* Two transparencies:
|
|
642
|
+
* - copy aliases: `let x = name` makes `x` carry the same value, so `x`'s uses
|
|
643
|
+
* must be numeric too (fixpoint-collected). Catches `let T = t` then `…T…`.
|
|
644
|
+
* - captured closures: a non-shadowing inner arrow captures the binding by
|
|
645
|
+
* reference, so its body's uses count — we recurse instead of rejecting.
|
|
646
|
+
* Catches floatbeat helpers `let s=(f)=>…t…` that read the param numerically. */
|
|
647
|
+
// requireProof=true (default): the param has a ToNumber-FORCING use (PROVES numeric).
|
|
648
|
+
// requireProof=false: the param merely has NO string-requiring use (numeric-COMPATIBLE).
|
|
649
|
+
// Forwarding recursions use the latter — a callee receiving the param need only be
|
|
650
|
+
// string-free (e.g. fbm's `ph`, used additively inside Math.sin), since the OUTER
|
|
651
|
+
// param earns its own proof from its own uses; requiring the callee be self-proven
|
|
652
|
+
// wrongly rejected forwards into additive-only params.
|
|
653
|
+
function paramAllUsesNumeric(body, name, _seen = new Set(), requireProof = true) {
|
|
654
|
+
if (body == null) return false
|
|
655
|
+
// Local closure defs (`let f = (p,…) => …`) so a call `f(name)` can be judged by
|
|
656
|
+
// f's own param numericity (see the call-arg handler in the walk).
|
|
657
|
+
const closures = new Map() // name → { params:[string], body }
|
|
658
|
+
// Fixpoint-collect copy aliases: `let/const x = <name-or-alias>`.
|
|
659
|
+
const names = new Set([name])
|
|
660
|
+
for (let grew = true; grew;) {
|
|
661
|
+
grew = false
|
|
662
|
+
const collect = (node) => {
|
|
663
|
+
if (!Array.isArray(node)) return
|
|
664
|
+
if ((node[0] === 'let' || node[0] === 'const') && node.length === 2
|
|
665
|
+
&& Array.isArray(node[1]) && node[1][0] === '=' && typeof node[1][1] === 'string') {
|
|
666
|
+
const init = node[1][2]
|
|
667
|
+
if (typeof init === 'string' && names.has(init) && !names.has(node[1][1])) { names.add(node[1][1]); grew = true }
|
|
668
|
+
else if (Array.isArray(init) && init[0] === '=>' && !closures.has(node[1][1])) {
|
|
669
|
+
const ps = Array.isArray(init[1]) ? init[1].slice(1) : [init[1]] // ['()', p0, p1] → [p0,p1]
|
|
670
|
+
if (ps.every(p => typeof p === 'string')) closures.set(node[1][1], { params: ps, body: init[2] })
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
for (let i = 1; i < node.length; i++) collect(node[i])
|
|
674
|
+
}
|
|
675
|
+
collect(body)
|
|
676
|
+
}
|
|
677
|
+
let ok = true, proven = false
|
|
678
|
+
// A param in a numeric-operand slot is a PROVING use; recurse into a non-param sub-expr.
|
|
679
|
+
const numOperand = (n) => { if (names.has(n)) proven = true; else walk(n) }
|
|
680
|
+
// Positional call args, flattening the `(, a b c)` node multi-arg calls parse to —
|
|
681
|
+
// without this a forward like `fbm(x, y, t, …)` never matched its param positions.
|
|
682
|
+
const flat1 = (a) => Array.isArray(a) && a[0] === ',' ? a.slice(1).flatMap(flat1) : [a]
|
|
683
|
+
const callArgList = (n) => n.slice(2).flatMap(flat1)
|
|
684
|
+
const walk = (node) => {
|
|
685
|
+
if (!ok) return
|
|
686
|
+
if (typeof node === 'string') { if (names.has(node)) ok = false; return } // bare use → reject
|
|
687
|
+
if (!Array.isArray(node)) return
|
|
688
|
+
const op = node[0]
|
|
689
|
+
// single `let/const x = init`: x is a binding (not a use). A pure copy of an
|
|
690
|
+
// alias is consumed (already in `names`); otherwise the init must be numeric.
|
|
691
|
+
if ((op === 'let' || op === 'const') && node.length === 2
|
|
692
|
+
&& Array.isArray(node[1]) && node[1][0] === '=' && typeof node[1][1] === 'string') {
|
|
693
|
+
const init = node[1][2]
|
|
694
|
+
if (typeof init === 'string' && names.has(init)) return
|
|
695
|
+
walk(init)
|
|
696
|
+
return
|
|
697
|
+
}
|
|
698
|
+
if (op === '=>') { // closure capture: recurse unless shadowed
|
|
699
|
+
const ps = node[1]
|
|
700
|
+
const shadowed = Array.isArray(ps)
|
|
701
|
+
? ps.some(p => names.has(p) || (Array.isArray(p) && names.has(p[1])))
|
|
702
|
+
: names.has(ps)
|
|
703
|
+
if (!shadowed) { walk(node[1]); walk(node[2]) } // defaults + body; param names aren't in `names`
|
|
704
|
+
return
|
|
705
|
+
}
|
|
706
|
+
if (PARAM_REASSIGN_OPS.has(op) && names.has(node[1])) { ok = false; return }
|
|
707
|
+
if (NUM_BIN_OPS.has(op) && node.length === 3) { // numeric binary: operands are ToNumber'd
|
|
708
|
+
numOperand(node[1]); numOperand(node[2])
|
|
709
|
+
return
|
|
710
|
+
}
|
|
711
|
+
if (REL_OPS.has(op) && node.length === 3) { // relational: numeric unless a known string is present
|
|
712
|
+
if (isStrLiteral(node[1]) || isStrLiteral(node[2])) { ok = false; return }
|
|
713
|
+
numOperand(node[1]); numOperand(node[2])
|
|
714
|
+
return
|
|
715
|
+
}
|
|
716
|
+
// `new TypedArray(x)` / `new ArrayBuffer(x)`: the length argument is ToNumber'd
|
|
717
|
+
// on the alloc path, but a pointer arg is copied (array) or viewed (buffer).
|
|
718
|
+
// A bare param in the length slot is numeric-COMPATIBLE but not PROVING — skip it
|
|
719
|
+
// (no reject, no proof); other args walk normally. A param used *solely* as
|
|
720
|
+
// `new Float64Array(param)` thus stays unproven → keeps the polymorphic ctor (so
|
|
721
|
+
// `f(arr)` copies the array instead of mis-sizing a zero buffer).
|
|
722
|
+
if (op === '()' && typeof node[1] === 'string' && node[1].startsWith('new.')
|
|
723
|
+
&& (node[1].endsWith('Array') || node[1] === 'new.ArrayBuffer')) {
|
|
724
|
+
for (let i = 2; i < node.length; i++) if (!names.has(node[i])) walk(node[i])
|
|
725
|
+
return
|
|
726
|
+
}
|
|
727
|
+
// Call of a LOCAL closure `f(…name…)`: forwarding the param flows its value into
|
|
728
|
+
// f's positional param. If that param is itself all-numeric (recursively, with a
|
|
729
|
+
// cycle guard), `name` in that slot is numeric-COMPATIBLE — neither rejected nor
|
|
730
|
+
// proving (so a param used *only* as a forwarded arg stays unproven, like the ctor
|
|
731
|
+
// length slot). Unknown / non-numeric callees fall through and reject (a string
|
|
732
|
+
// could flow in). Covers heapsort's `heapify(n)` and crc32's `crc32(buf)`.
|
|
733
|
+
if (op === '()' && typeof node[1] === 'string' && closures.has(node[1]) && !_seen.has(node[1])) {
|
|
734
|
+
const cl = closures.get(node[1])
|
|
735
|
+
const args = callArgList(node)
|
|
736
|
+
for (let i = 0; i < args.length; i++) {
|
|
737
|
+
if (!names.has(args[i])) { walk(args[i]); continue }
|
|
738
|
+
const param = cl.params[i]
|
|
739
|
+
if (param == null || !paramAllUsesNumeric(cl.body, param, new Set([..._seen, node[1]]), false)) { ok = false; return }
|
|
740
|
+
}
|
|
741
|
+
return
|
|
742
|
+
}
|
|
743
|
+
// Same forwarding judgement for a call to a MODULE-LEVEL user function (sibling,
|
|
744
|
+
// not a body-local closure): `frame` passing its param into a helper `fbm(x,y,t,…)`.
|
|
745
|
+
// Without this the bare arg fell through and rejected, leaving an exported numeric
|
|
746
|
+
// param (plasma/raymarcher's `t`) unproven → per-pixel `__to_num` + polymorphic-`+`
|
|
747
|
+
// string forks. Judge by the callee param's own numericity (recursive, cycle-guarded).
|
|
748
|
+
if (op === '()' && typeof node[1] === 'string' && !_seen.has(node[1])) {
|
|
749
|
+
const fn = ctx.func.map?.get(node[1])
|
|
750
|
+
if (fn && fn.body && !fn.raw && Array.isArray(fn.sig?.params) && !fn.rest) {
|
|
751
|
+
const args = callArgList(node)
|
|
752
|
+
for (let i = 0; i < args.length; i++) {
|
|
753
|
+
if (!names.has(args[i])) { walk(args[i]); continue }
|
|
754
|
+
const p = fn.sig.params[i]
|
|
755
|
+
if (!p || !paramAllUsesNumeric(fn.body, p.name, new Set([..._seen, node[1]]), false)) { ok = false; return }
|
|
756
|
+
}
|
|
757
|
+
return
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// `Math.f(...)` ToNumbers every argument (Math operates on numbers), so a bare
|
|
761
|
+
// param in any arg slot is a PROVING numeric use — same contract as `*`/`-`.
|
|
762
|
+
// Without this, `Math.sin(t)` rejected the param via the generic-call fallthrough,
|
|
763
|
+
// so a numeric kernel like `Math.sin(tick) + …` lost its NUMBER proof and paid a
|
|
764
|
+
// per-use `__to_num` + a polymorphic-`+` string-concat fork (interference example).
|
|
765
|
+
// The callee is the lowered `math.sin` string at emit time (post-autoload), or the
|
|
766
|
+
// raw `(. Math sin)` member pre-lowering — match both.
|
|
767
|
+
const isMathCall = op === '()' && (
|
|
768
|
+
(typeof node[1] === 'string' && node[1].startsWith('math.')) ||
|
|
769
|
+
(Array.isArray(node[1]) && node[1][0] === '.' && node[1][1] === 'Math'))
|
|
770
|
+
if (isMathCall) {
|
|
771
|
+
const numArg = (a) => { if (Array.isArray(a) && a[0] === ',') { numArg(a[1]); numArg(a[2]) } else numOperand(a) }
|
|
772
|
+
for (let i = 2; i < node.length; i++) numArg(node[i])
|
|
773
|
+
return
|
|
774
|
+
}
|
|
775
|
+
// Binary `+` is overloaded (numeric add | string concat). A string-literal
|
|
776
|
+
// operand means concat intent → reject. Otherwise it is numeric-COMPATIBLE but
|
|
777
|
+
// not self-PROVING (a string param would concat) — recurse the non-param operand
|
|
778
|
+
// and treat a bare param as compatible (neither prove nor reject), exactly like
|
|
779
|
+
// paramNeverString. The numeric proof must still come from a ToNumber-forcing use
|
|
780
|
+
// (`*`, `Math.*`, …); a param used ONLY in `+` stays unproven (sound).
|
|
781
|
+
if (op === '+' && node.length === 3) {
|
|
782
|
+
if (isStrLiteral(node[1]) || isStrLiteral(node[2])) { ok = false; return }
|
|
783
|
+
if (!names.has(node[1])) walk(node[1])
|
|
784
|
+
if (!names.has(node[2])) walk(node[2])
|
|
785
|
+
return
|
|
786
|
+
}
|
|
787
|
+
if (op === '-' && node.length === 2) { numOperand(node[1]); return } // unary negate
|
|
788
|
+
if (op === '-' && node.length === 3) { numOperand(node[1]); numOperand(node[2]); return }
|
|
789
|
+
// `u-`/`u+` are the normalized unary minus/plus (prepare rewrites `-x`/`+x`); both ToNumber.
|
|
790
|
+
if ((op === 'u-' || op === 'u+') && node.length === 2) { numOperand(node[1]); return }
|
|
791
|
+
if (op === '+' && node.length === 2) { numOperand(node[1]); return } // unary + = ToNumber
|
|
792
|
+
if (op === '~' && node.length === 2) { numOperand(node[1]); return }
|
|
793
|
+
for (let i = 1; i < node.length; i++) walk(node[i]) // bare param reaching here → rejected above
|
|
794
|
+
}
|
|
795
|
+
walk(body)
|
|
796
|
+
return requireProof ? (ok && proven) : ok
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// String methods whose receiver MUST be a string — their presence proves the
|
|
800
|
+
// param is (sometimes) string and disqualifies the boundary-numeric trust.
|
|
801
|
+
const STRING_RECV_METHODS = new Set([
|
|
802
|
+
'charCodeAt', 'charAt', 'codePointAt', 'startsWith', 'endsWith', 'toUpperCase',
|
|
803
|
+
'toLowerCase', 'normalize', 'localeCompare', 'padStart', 'padEnd', 'repeat',
|
|
804
|
+
'trim', 'trimStart', 'trimEnd', 'split', 'match', 'matchAll', 'replace',
|
|
805
|
+
'replaceAll', 'substring', 'substr', 'concat', 'indexOf', 'lastIndexOf',
|
|
806
|
+
'includes', 'slice',
|
|
807
|
+
])
|
|
808
|
+
|
|
809
|
+
/** True iff no use of exported f64 param `name` REQUIRES it to be a string — so
|
|
810
|
+
* the interop boundary contract (`wrapVal` passes a JS number straight to an f64
|
|
811
|
+
* param; a string arg is a type misuse already unsupported, returning NaN) makes
|
|
812
|
+
* it provably numeric. Weaker than `paramAllUsesNumeric`: that PROVES numericity
|
|
813
|
+
* from ToNumber-forcing ops, this DISPROVES stringness so binary `+` (the common
|
|
814
|
+
* `accumulator + cre` shape) no longer pessimistically pulls the string-concat
|
|
815
|
+
* fork into a pure float kernel. Only sound under the export boundary — never use
|
|
816
|
+
* for locals/closures, whose values can genuinely be strings.
|
|
817
|
+
*
|
|
818
|
+
* Disqualifying (string-requiring) uses:
|
|
819
|
+
* - `+` with a string-literal/template operand (`"px" + name`) — concat intent
|
|
820
|
+
* - a string-receiver method call (`name.charCodeAt(…)`, `name.split(…)`)
|
|
821
|
+
* - `name[k]` / `name.length` is NOT disqualifying (works on arrays/typed too,
|
|
822
|
+
* but an f64 param is neither — so a member access means the caller passed a
|
|
823
|
+
* pointer, out of the f64-number contract; conservatively we reject it)
|
|
824
|
+
* - passing `name` to a call / returning it / storing into an aggregate: the
|
|
825
|
+
* value escapes where it could be ToString'd; reject conservatively. */
|
|
826
|
+
function paramNeverString(body, name) {
|
|
827
|
+
if (body == null) return false
|
|
828
|
+
let ok = true
|
|
829
|
+
const walk = (node) => {
|
|
830
|
+
if (!ok || node == null) return
|
|
831
|
+
if (typeof node === 'string') { if (node === name) ok = false; return } // bare escape → reject
|
|
832
|
+
if (!Array.isArray(node)) return
|
|
833
|
+
const op = node[0]
|
|
834
|
+
if (op === '=>') return // shadowing-safe: closure handled conservatively (escape)
|
|
835
|
+
// `+` (binary): a string-literal/template operand makes it concat → reject.
|
|
836
|
+
// Otherwise the param is in an arithmetic add; recurse the non-name operand.
|
|
837
|
+
if (op === '+' && node.length === 3) {
|
|
838
|
+
if (isStrLiteral(node[1]) || isStrLiteral(node[2])) { ok = false; return }
|
|
839
|
+
for (let i = 1; i <= 2; i++) if (node[i] !== name) walk(node[i])
|
|
840
|
+
return
|
|
841
|
+
}
|
|
842
|
+
// Numeric/relational/bitwise binary + unary: param operand is fine, recurse rest.
|
|
843
|
+
if ((NUM_BIN_OPS.has(op) || REL_OPS.has(op)) && node.length === 3) {
|
|
844
|
+
for (let i = 1; i <= 2; i++) if (node[i] !== name) walk(node[i])
|
|
845
|
+
return
|
|
846
|
+
}
|
|
847
|
+
if ((op === 'u-' || op === 'u+' || op === '~') && node.length === 2) {
|
|
848
|
+
if (node[1] !== name) walk(node[1]); return
|
|
849
|
+
}
|
|
850
|
+
if (op === '-' && (node.length === 2 || node.length === 3)) {
|
|
851
|
+
for (let i = 1; i < node.length; i++) if (node[i] !== name) walk(node[i])
|
|
852
|
+
return
|
|
853
|
+
}
|
|
854
|
+
// Member access / method call on the param → it's a pointer, not an f64 number:
|
|
855
|
+
// reject (out of contract). `.`/`?.`/`[]` with the name as receiver.
|
|
856
|
+
if ((op === '.' || op === '?.' || op === '[]') && node[1] === name) { ok = false; return }
|
|
857
|
+
// `=`/compound reassignment of the param to a non-numeric value: reject if it
|
|
858
|
+
// could become a string. A reassignment makes the param mutable — conservatively
|
|
859
|
+
// require the RHS to be string-free too (recurse), and the target isn't a use.
|
|
860
|
+
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
861
|
+
}
|
|
862
|
+
walk(body)
|
|
863
|
+
return ok
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/** Hoist each eligible param's `__to_num` coercion to a single entry `local.set`,
|
|
867
|
+
* rewriting per-use calls in `stmts` to a bare typed `local.get`. Mutates
|
|
868
|
+
* `stmts` in place; returns the prologue inits to splice ahead of the body.
|
|
869
|
+
* Only fires for params whose coercion appears inside a loop (or ≥2×) — a lone
|
|
870
|
+
* straight-line coercion isn't worth the rebind. */
|
|
871
|
+
function hoistInvariantParamCoercions(stmts, func) {
|
|
872
|
+
const inits = []
|
|
873
|
+
const defaults = func.defaults || {}
|
|
874
|
+
for (const p of func.sig.params) {
|
|
875
|
+
if (p.type !== 'f64' || p.ptrKind != null || p.jsstring) continue
|
|
876
|
+
if (ctx.func.boxed?.has(p.name)) continue
|
|
877
|
+
if (p.name in defaults) continue
|
|
878
|
+
if (!paramAllUsesNumeric(func.body, p.name)) continue
|
|
879
|
+
const pat = (n) => Array.isArray(n) && n[0] === 'call' && n[1] === '$__to_num'
|
|
880
|
+
&& Array.isArray(n[2]) && n[2][0] === 'i64.reinterpret_f64'
|
|
881
|
+
&& Array.isArray(n[2][1]) && n[2][1][0] === 'local.get' && n[2][1][1] === `$${p.name}`
|
|
882
|
+
let total = 0, inLoop = 0
|
|
883
|
+
const count = (node, depth) => {
|
|
884
|
+
if (!Array.isArray(node)) return
|
|
885
|
+
const d = node[0] === 'loop' ? depth + 1 : depth
|
|
886
|
+
for (let i = 1; i < node.length; i++) {
|
|
887
|
+
if (pat(node[i])) { total++; if (d > 0) inLoop++ }
|
|
888
|
+
else count(node[i], d)
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
for (const s of stmts) count(s, 0)
|
|
892
|
+
if (total === 0 || (inLoop === 0 && total < 2)) continue
|
|
893
|
+
const strip = (node) => {
|
|
894
|
+
if (!Array.isArray(node)) return
|
|
895
|
+
for (let i = 1; i < node.length; i++) {
|
|
896
|
+
if (pat(node[i])) node[i] = typed(['local.get', `$${p.name}`], 'f64')
|
|
897
|
+
else strip(node[i])
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
for (const s of stmts) strip(s)
|
|
901
|
+
inits.push(['local.set', `$${p.name}`,
|
|
902
|
+
typed(['call', '$__to_num', ['i64.reinterpret_f64', typed(['local.get', `$${p.name}`], 'f64')]], 'f64')])
|
|
903
|
+
inc('__to_num')
|
|
904
|
+
}
|
|
905
|
+
return inits
|
|
906
|
+
}
|
|
907
|
+
|
|
373
908
|
/**
|
|
374
909
|
* Phase: emit one user function to WAT IR.
|
|
375
910
|
*
|
|
@@ -390,6 +925,7 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
390
925
|
const block = funcFacts.block
|
|
391
926
|
ctx.func.locals = new Map(funcFacts.locals)
|
|
392
927
|
ctx.func.boxed = new Map(funcFacts.boxed)
|
|
928
|
+
ctx.func.cellTypes = new Set(funcFacts.cellTypes)
|
|
393
929
|
ctx.func.flatObjects = new Map(funcFacts.flatObjects)
|
|
394
930
|
ctx.func.sliceViews = new Set(funcFacts.sliceViews)
|
|
395
931
|
ctx.func.localReps = cloneRepMap(funcFacts.localReps)
|
|
@@ -431,7 +967,7 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
431
967
|
// Boundary-wrapped exports also defer the attribute to the synthesized
|
|
432
968
|
// wrapper ($${name}$exp) that reboxes the narrowed result back to f64.
|
|
433
969
|
if (exported && !isBoundaryWrapped(func)) fn.push(['export', `"${name}"`])
|
|
434
|
-
fn.push(...sig.params.map(p => ['param',
|
|
970
|
+
fn.push(...sig.params.map(p => ['param', dollar(p.name), p.type]))
|
|
435
971
|
fn.push(...sig.results.map(t => ['result', t]))
|
|
436
972
|
|
|
437
973
|
// Default params: ES spec says default applies only when arg is `undefined`
|
|
@@ -497,9 +1033,11 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
497
1033
|
}
|
|
498
1034
|
|
|
499
1035
|
if (block) {
|
|
500
|
-
const stmts =
|
|
1036
|
+
const stmts = emitBlockBody(body)
|
|
1037
|
+
// Hoist loop-invariant `__to_num(param)` coercions to a single entry rebind.
|
|
1038
|
+
const numCoerceInits = hoistInvariantParamCoercions(stmts, func)
|
|
501
1039
|
const paramInits = collectParamInits()
|
|
502
|
-
for (const [l, t] of ctx.func.locals) fn.push(['local',
|
|
1040
|
+
for (const [l, t] of ctx.func.locals) fn.push(['local', dollar(l), t])
|
|
503
1041
|
// I: Skip trailing fallback when last statement is return (unreachable code)
|
|
504
1042
|
const lastStmt = stmts.at(-1)
|
|
505
1043
|
const endsWithReturn = lastStmt && (lastStmt[0] === 'return' || lastStmt[0] === 'return_call')
|
|
@@ -510,16 +1048,16 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
510
1048
|
const fallthrough = endsWithReturn ? []
|
|
511
1049
|
: sig.results.length === 1 && sig.results[0] === 'f64' ? [undefExpr()]
|
|
512
1050
|
: sig.results.map(t => [`${t}.const`, 0])
|
|
513
|
-
fn.push(...paramInits, ...boxedParamInits, ...preboxedLocalInits, ...stmts, ...fallthrough)
|
|
1051
|
+
fn.push(...paramInits, ...boxedParamInits, ...preboxedLocalInits, ...numCoerceInits, ...stmts, ...fallthrough)
|
|
514
1052
|
} else if (multi && body[0] === '[') {
|
|
515
1053
|
const values = body.slice(1).map(e => asF64(emit(e)))
|
|
516
1054
|
const paramInits = collectParamInits()
|
|
517
|
-
for (const [l, t] of ctx.func.locals) fn.push(['local',
|
|
1055
|
+
for (const [l, t] of ctx.func.locals) fn.push(['local', dollar(l), t])
|
|
518
1056
|
fn.push(...paramInits, ...boxedParamInits, ...preboxedLocalInits, ...values)
|
|
519
1057
|
} else {
|
|
520
1058
|
const ir = emit(body)
|
|
521
1059
|
const paramInits = collectParamInits()
|
|
522
|
-
for (const [l, t] of ctx.func.locals) fn.push(['local',
|
|
1060
|
+
for (const [l, t] of ctx.func.locals) fn.push(['local', dollar(l), t])
|
|
523
1061
|
const finalIR = sig.ptrKind != null ? asPtrOffset(ir, sig.ptrKind) : asParamType(ir, sig.results[0])
|
|
524
1062
|
fn.push(...paramInits, ...boxedParamInits, ...preboxedLocalInits, tcoTailRewrite(finalIR, sig.results[0]))
|
|
525
1063
|
}
|
|
@@ -560,17 +1098,17 @@ function synthesizeBoundaryWrappers() {
|
|
|
560
1098
|
for (const func of ctx.func.list) {
|
|
561
1099
|
if (!isBoundaryWrapped(func)) continue
|
|
562
1100
|
const { name, sig } = func
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
//
|
|
1101
|
+
// Quiet NaN-box ABI: every boundary value is f64. A number is a plain f64; a
|
|
1102
|
+
// tagged value (heap pointer, null/undef/bool atom) is an f64 whose quiet-NaN
|
|
1103
|
+
// (0x7FF8…) payload carries the tag. Quiet-NaN payloads are preserved across the
|
|
1104
|
+
// JS↔wasm call boundary by every real engine (and non-JS hosts don't canonicalize
|
|
1105
|
+
// at all), so no i64 carrier is needed — the wasm signature is self-describing
|
|
1106
|
+
// (f64 everywhere) and a consumer discriminates a tagged value by the NaN prefix.
|
|
1107
|
+
// Env requirement: a non-canonicalizing NaN boundary. To support a canonicalizing
|
|
1108
|
+
// engine, a per-position i64 carrier would re-enter here (param/result type i64 +
|
|
1109
|
+
// `i64.reinterpret_f64`) plus a `jz:i64exp` section for interop.js.
|
|
572
1110
|
const resultBool = func.valResult === VAL.BOOL && sig.ptrKind == null
|
|
573
|
-
const
|
|
1111
|
+
const resultBigint = func.valResult === VAL.BIGINT && sig.ptrKind == null
|
|
574
1112
|
// Inline `(export ...)` attribute only when the func decl carried the
|
|
575
1113
|
// inline-export keyword (`export function foo`). For re-exports
|
|
576
1114
|
// (`function foo; export { foo as bar }`) the `name` is the *internal*
|
|
@@ -580,19 +1118,17 @@ function synthesizeBoundaryWrappers() {
|
|
|
580
1118
|
const wrapNode = func.exported
|
|
581
1119
|
? ['func', `$${name}$exp`, ['export', `"${name}"`]]
|
|
582
1120
|
: ['func', `$${name}$exp`]
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
wrapNode.push(['param', `$${p.name}`,
|
|
1121
|
+
// jsstring params flow as externref end-to-end; every other boundary value is f64.
|
|
1122
|
+
sig.params.forEach((p) => {
|
|
1123
|
+
wrapNode.push(['param', `$${p.name}`, p.jsstring ? 'externref' : 'f64'])
|
|
586
1124
|
})
|
|
587
|
-
wrapNode.push(['result',
|
|
588
|
-
const args = sig.params.map((p
|
|
1125
|
+
wrapNode.push(['result', resultBigint ? 'i64' : 'f64'])
|
|
1126
|
+
const args = sig.params.map((p) => {
|
|
589
1127
|
const get = ['local.get', `$${p.name}`]
|
|
590
1128
|
// jsstring: externref flows through unchanged — inner func also takes externref.
|
|
591
1129
|
if (p.jsstring) return get
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
return ['i32.wrap_i64', get]
|
|
595
|
-
}
|
|
1130
|
+
// ptrKind param: the f64 NaN-box carries the pointer — extract the i32 offset.
|
|
1131
|
+
if (p.ptrKind != null) return ['i32.wrap_i64', ['i64.reinterpret_f64', get]]
|
|
596
1132
|
if (p.type === 'f64') return get
|
|
597
1133
|
// Numeric narrowing: f64 → i32 truncate
|
|
598
1134
|
return ['i32.trunc_sat_f64_s', get]
|
|
@@ -603,17 +1139,26 @@ function synthesizeBoundaryWrappers() {
|
|
|
603
1139
|
const ptrType = valKindToPtr(sig.ptrKind)
|
|
604
1140
|
body = mkPtrIR(ptrType, sig.ptrAux ?? 0, callIR)
|
|
605
1141
|
} else if (resultBool) {
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
|
|
1142
|
+
// The inner func returns a clean 0/1 boolean carrier — never NaN. The i32
|
|
1143
|
+
// carrier already takes truthyIR's identity path; the f64 carrier would
|
|
1144
|
+
// otherwise fall through to the full __is_truthy NaN-discrimination, every
|
|
1145
|
+
// arm of which is dead for a boolean. Pull the bit out with one f64.ne so
|
|
1146
|
+
// boolBoxIR boxes `4|bit` straight into the TRUE_NAN/FALSE_NAN atom.
|
|
1147
|
+
const carrier = sig.results[0] === 'i32'
|
|
1148
|
+
? typed(callIR, 'i32')
|
|
1149
|
+
: typed(['f64.ne', callIR, ['f64.const', 0]], 'i32')
|
|
1150
|
+
body = boolBoxIR(carrier)
|
|
1151
|
+
} else if (resultBigint) {
|
|
1152
|
+
// BigInt rides the i64-reinterpret-f64 carrier internally; expose the raw i64 at the JS
|
|
1153
|
+
// boundary so the host receives a real, lossless BigInt (wasm i64 <-> JS BigInt). Internal
|
|
1154
|
+
// callers use `$name` (the f64 carrier) untouched; only the `$exp` export result is i64.
|
|
1155
|
+
body = ['i64.reinterpret_f64', callIR]
|
|
609
1156
|
} else if (sig.results[0] === 'i32') {
|
|
610
1157
|
body = [sig.unsignedResult ? 'f64.convert_i32_u' : 'f64.convert_i32_s', callIR]
|
|
611
1158
|
} else {
|
|
612
1159
|
body = callIR
|
|
613
1160
|
}
|
|
614
|
-
wrapNode.push(
|
|
615
|
-
func._exportUsesI64 = resultI64 || paramI64.some(Boolean)
|
|
616
|
-
func._exportI64Sig = { params: paramI64, result: resultI64 }
|
|
1161
|
+
wrapNode.push(body)
|
|
617
1162
|
// Track externref param positions so interop.js can pass JS values
|
|
618
1163
|
// raw (skipping `mem.wrapVal`) at those slots. Today this only fires
|
|
619
1164
|
// for `jsstring`-tagged params; future externref carriers wire here too.
|
|
@@ -649,6 +1194,7 @@ function emitClosureBody(cb) {
|
|
|
649
1194
|
ctx.func.localReps = null
|
|
650
1195
|
if (cb.intConsts) for (const [name, v] of cb.intConsts) updateRep(name, { intConst: v })
|
|
651
1196
|
if (cb.intCertain) for (const name of cb.intCertain) updateRep(name, { intCertain: true })
|
|
1197
|
+
if (cb.nullables) for (const name of cb.nullables) updateRep(name, { nullable: true })
|
|
652
1198
|
if (cb.valTypes) for (const [name, vt] of cb.valTypes) updateRep(name, { val: vt })
|
|
653
1199
|
if (cb.schemaVars) {
|
|
654
1200
|
ctx.schema.vars = new Map([...prevSchemaVars, ...cb.schemaVars])
|
|
@@ -664,6 +1210,9 @@ function emitClosureBody(cb) {
|
|
|
664
1210
|
}
|
|
665
1211
|
// In closure bodies, boxed captures use the original name as both var and cell local
|
|
666
1212
|
ctx.func.boxed = cb.boxed ? new Map([...cb.boxed].map(v => [v, v])) : new Map()
|
|
1213
|
+
// i32-narrowed cells: the owner decided the cell width (funcFacts.cellTypes);
|
|
1214
|
+
// every body sharing the cell must read/write it at that width.
|
|
1215
|
+
ctx.func.cellTypes = new Set(cb.cellI32 || [])
|
|
667
1216
|
const parentBoxedCaptures = new Set(cb.boxed || [])
|
|
668
1217
|
ctx.func.preboxed = new Set()
|
|
669
1218
|
// Bare `;`-sequence bodies (no enclosing `{}`) reach us when callers built a
|
|
@@ -693,6 +1242,45 @@ function emitClosureBody(cb) {
|
|
|
693
1242
|
|
|
694
1243
|
// Params are locals, assigned directly from inline slots
|
|
695
1244
|
for (const p of cb.params) ctx.func.locals.set(p, 'f64')
|
|
1245
|
+
// Mark params that every direct call site passed a number (seeded by
|
|
1246
|
+
// tryDirectClosureCall) VAL.NUMBER — their body uses then skip the __to_num
|
|
1247
|
+
// coercion. All direct calls were emitted before this body (module end), so the
|
|
1248
|
+
// lattice is complete; a `false`/unobserved slot leaves the param boxed.
|
|
1249
|
+
const ptRow = ctx.closure.paramTypes?.get(cb.name)
|
|
1250
|
+
// A param numeric at every call site is typed NUMBER so its body uses skip __to_num. If some
|
|
1251
|
+
// call omits it (index ≥ minArgc) it can hold UNDEF_NAN, so also flag it nullable — that keeps
|
|
1252
|
+
// the boxing win yet stops `x === undefined` mis-folding to false (it bit-compares instead).
|
|
1253
|
+
const minArgc = ctx.closure.minArgc?.get(cb.name) ?? 0
|
|
1254
|
+
if (ptRow) for (let i = 0; i < cb.params.length; i++) {
|
|
1255
|
+
if (ptRow[i] === true && !ctx.func.localReps?.get(cb.params[i])?.val)
|
|
1256
|
+
updateRep(cb.params[i], i < minArgc ? { val: VAL.NUMBER } : { val: VAL.NUMBER, nullable: true })
|
|
1257
|
+
}
|
|
1258
|
+
// A param passed the same typed-array ctor at every direct call site is TYPED:
|
|
1259
|
+
// register its element ctor so `buf[i]` reads use the typed load (it stays an f64
|
|
1260
|
+
// NaN-box in the closure ABI, but knowing the kind avoids the dynamic `__typed_idx`
|
|
1261
|
+
// /`__len` dispatch that pulls the string runtime). Numeric trust (above) wins if it
|
|
1262
|
+
// already classified the slot — they're disjoint anyway (NUMBER vs TYPED arg).
|
|
1263
|
+
const tcRow = ctx.closure.paramTypedCtors?.get(cb.name)
|
|
1264
|
+
if (tcRow) for (let i = 0; i < cb.params.length; i++) {
|
|
1265
|
+
const ctor = tcRow[i]
|
|
1266
|
+
if (ctor && !ctx.func.localReps?.get(cb.params[i])?.val) {
|
|
1267
|
+
updateRep(cb.params[i], { val: VAL.TYPED })
|
|
1268
|
+
;(ctx.types.typedElem ||= new Map()).set(cb.params[i], ctor)
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
// Body-usage numeric trust for closure params — the same proof the export path
|
|
1272
|
+
// applies (paramAllUsesNumeric). A nested helper like heapsort's `heapify(n)` whose
|
|
1273
|
+
// param is used only in arithmetic/relational positions is VAL.NUMBER, so `(n>>1)-1`
|
|
1274
|
+
// skips the `__to_num` coercion that would otherwise drag the ToNumber string-parse
|
|
1275
|
+
// tree into a pure-integer kernel. paramAllUsesNumeric walks any AST node, so this
|
|
1276
|
+
// also covers expression-bodied arrows (`(m) => m | 0`) — the common closure shape
|
|
1277
|
+
// whose dynamic param would otherwise emit a polymorphic add/coerce that pulls the
|
|
1278
|
+
// whole string runtime in. Call-site evidence (ptRow) already covers the monomorphic
|
|
1279
|
+
// case; this also catches params the lattice left unobserved.
|
|
1280
|
+
for (const p of cb.params) {
|
|
1281
|
+
if (!ctx.func.localReps?.get(p)?.val && !cb.defaults?.[p] && paramAllUsesNumeric(cb.body, p))
|
|
1282
|
+
updateRep(p, { val: VAL.NUMBER })
|
|
1283
|
+
}
|
|
696
1284
|
|
|
697
1285
|
// Register captured variable locals: boxed = i32 cell pointer, otherwise f64 value
|
|
698
1286
|
for (let i = 0; i < cb.captures.length; i++) {
|
|
@@ -726,7 +1314,7 @@ function emitClosureBody(cb) {
|
|
|
726
1314
|
ctx.func.locals.set(name, 'i32')
|
|
727
1315
|
updateRep(name, fields)
|
|
728
1316
|
}
|
|
729
|
-
bodyIR =
|
|
1317
|
+
bodyIR = emitBlockBody(cb.body)
|
|
730
1318
|
} else {
|
|
731
1319
|
bodyIR = [asF64(emit(cb.body))]
|
|
732
1320
|
}
|
|
@@ -734,13 +1322,15 @@ function emitClosureBody(cb) {
|
|
|
734
1322
|
// Pre-allocate cache locals for env unpacking
|
|
735
1323
|
const envBase = cb.captures.length > 0 ? `${T}envBase${ctx.func.uniq++}` : null
|
|
736
1324
|
if (envBase) ctx.func.locals.set(envBase, 'i32')
|
|
737
|
-
// Rest param: allocate helper locals (len + offset) before emitting decls
|
|
738
|
-
let restOff, restLen
|
|
1325
|
+
// Rest param: allocate helper locals (len + offset + spill loop index) before emitting decls
|
|
1326
|
+
let restOff, restLen, restIdx
|
|
739
1327
|
if (cb.rest) {
|
|
740
1328
|
restOff = `${T}restOff${ctx.func.uniq++}`
|
|
741
1329
|
restLen = `${T}restLen${ctx.func.uniq++}`
|
|
1330
|
+
restIdx = `${T}restIdx${ctx.func.uniq++}`
|
|
742
1331
|
ctx.func.locals.set(restOff, 'i32')
|
|
743
1332
|
ctx.func.locals.set(restLen, 'i32')
|
|
1333
|
+
ctx.func.locals.set(restIdx, 'i32')
|
|
744
1334
|
inc('__alloc_hdr', '__mkptr')
|
|
745
1335
|
}
|
|
746
1336
|
|
|
@@ -777,7 +1367,7 @@ function emitClosureBody(cb) {
|
|
|
777
1367
|
}
|
|
778
1368
|
}
|
|
779
1369
|
|
|
780
|
-
for (const [l, t] of ctx.func.locals) fn.push(['local',
|
|
1370
|
+
for (const [l, t] of ctx.func.locals) fn.push(['local', dollar(l), t])
|
|
781
1371
|
|
|
782
1372
|
// Load captures from env: boxed → i32.load (raw cell pointer), immutable → f64.load value.
|
|
783
1373
|
// env is the CLOSURE pointer (PTR.CLOSURE) — never an ARRAY, no forwarding chain.
|
|
@@ -814,20 +1404,20 @@ function emitClosureBody(cb) {
|
|
|
814
1404
|
}
|
|
815
1405
|
}
|
|
816
1406
|
|
|
817
|
-
// Rest param: pack
|
|
818
|
-
// len =
|
|
819
|
-
//
|
|
820
|
-
//
|
|
1407
|
+
// Rest param: pack args a[fixedParams..argc-1] into a fresh array.
|
|
1408
|
+
// len = max(argc - fixedParams, 0). The first `restSlots = width - fixedParams`
|
|
1409
|
+
// come from the inline arg slots; any overflow (argc > width, only reachable via a
|
|
1410
|
+
// spread call) is read straight from the caller's full args array, whose offset the
|
|
1411
|
+
// spread path published in $__closure_spill. This gives unbounded variadic arity.
|
|
821
1412
|
if (cb.rest) {
|
|
822
1413
|
const fixedN = fixedParamN
|
|
823
1414
|
const restSlots = W - fixedN
|
|
1415
|
+
declGlobal('__closure_spill', 'i32')
|
|
824
1416
|
fn.push(['local.set', `$${restLen}`,
|
|
825
1417
|
['select',
|
|
826
1418
|
['i32.sub', ['local.get', '$__argc'], ['i32.const', fixedN]],
|
|
827
1419
|
['i32.const', 0],
|
|
828
1420
|
['i32.gt_s', ['local.get', '$__argc'], ['i32.const', fixedN]]]])
|
|
829
|
-
fn.push(['if', ['i32.gt_s', ['local.get', `$${restLen}`], ['i32.const', restSlots]],
|
|
830
|
-
['then', ['local.set', `$${restLen}`, ['i32.const', restSlots]]]])
|
|
831
1421
|
fn.push(['local.set', `$${restOff}`,
|
|
832
1422
|
['call', '$__alloc_hdr',
|
|
833
1423
|
['local.get', `$${restLen}`], ['local.get', `$${restLen}`]]])
|
|
@@ -837,6 +1427,21 @@ function emitClosureBody(cb) {
|
|
|
837
1427
|
['i32.add', ['local.get', `$${restOff}`], ['i32.const', i * 8]],
|
|
838
1428
|
['local.get', `$__a${fixedN + i}`]]]])
|
|
839
1429
|
}
|
|
1430
|
+
// Overflow beyond the inline slots: copy args[width..argc-1] from the spill array
|
|
1431
|
+
// (set by the spread-call site). rest[i] = spill[(fixedN+i)*8] for i in [restSlots, restLen).
|
|
1432
|
+
const rid = ctx.func.uniq++
|
|
1433
|
+
fn.push(['if', ['i32.gt_s', ['local.get', `$${restLen}`], ['i32.const', restSlots]],
|
|
1434
|
+
['then',
|
|
1435
|
+
['local.set', `$${restIdx}`, ['i32.const', restSlots]],
|
|
1436
|
+
['block', `$restEnd${rid}`,
|
|
1437
|
+
['loop', `$restLoop${rid}`,
|
|
1438
|
+
['br_if', `$restEnd${rid}`, ['i32.ge_s', ['local.get', `$${restIdx}`], ['local.get', `$${restLen}`]]],
|
|
1439
|
+
['f64.store',
|
|
1440
|
+
['i32.add', ['local.get', `$${restOff}`], ['i32.mul', ['local.get', `$${restIdx}`], ['i32.const', 8]]],
|
|
1441
|
+
['f64.load', ['i32.add', ['global.get', '$__closure_spill'],
|
|
1442
|
+
['i32.mul', ['i32.add', ['local.get', `$${restIdx}`], ['i32.const', fixedN]], ['i32.const', 8]]]]],
|
|
1443
|
+
['local.set', `$${restIdx}`, ['i32.add', ['local.get', `$${restIdx}`], ['i32.const', 1]]],
|
|
1444
|
+
['br', `$restLoop${rid}`]]]]])
|
|
840
1445
|
const restValue = ['call', '$__mkptr', ['i32.const', PTR.ARRAY], ['i32.const', 0], ['local.get', `$${restOff}`]]
|
|
841
1446
|
if (boxedParamNames.has(cb.rest)) {
|
|
842
1447
|
fn.push(
|
|
@@ -866,6 +1471,9 @@ function emitClosureBody(cb) {
|
|
|
866
1471
|
* @returns {Array} Complete WASM module as S-expression
|
|
867
1472
|
*/
|
|
868
1473
|
export default function compile(ast, profiler) {
|
|
1474
|
+
// Contract: callers (jzCompileInner / scripts/self.js compileSelf) must set
|
|
1475
|
+
// ctx.transform.optimize before reaching here — every optimize-gated pass below
|
|
1476
|
+
// reads `cfg && cfg.x === false`, so a null cfg silently runs every pass.
|
|
869
1477
|
// Populate known function names + lookup map on ctx.func for direct call detection
|
|
870
1478
|
ctx.func.names.clear()
|
|
871
1479
|
ctx.func.map.clear()
|
|
@@ -891,7 +1499,7 @@ export default function compile(ast, profiler) {
|
|
|
891
1499
|
|
|
892
1500
|
// Check user globals don't conflict with runtime globals (modules loaded after user decls)
|
|
893
1501
|
for (const name of ctx.scope.userGlobals)
|
|
894
|
-
if (!ctx.scope.globals.get(name)?.
|
|
1502
|
+
if (!(ctx.scope.globals.get(name)?.mut && ctx.scope.globals.get(name)?.type === 'f64'))
|
|
895
1503
|
err(`'${name}' conflicts with a compiler internal — choose a different name`)
|
|
896
1504
|
|
|
897
1505
|
// Pre-fold const globals: evaluate constant initializers before function compilation
|
|
@@ -903,6 +1511,12 @@ export default function compile(ast, profiler) {
|
|
|
903
1511
|
if (ast) {
|
|
904
1512
|
const evalConst = n => {
|
|
905
1513
|
if (typeof n === 'number') return n
|
|
1514
|
+
// A reference to an already-folded integer const (`const NEW = CALL + 1`):
|
|
1515
|
+
// resolve it from constInts so const-referencing-const initializers fold too.
|
|
1516
|
+
// Without this they stay unfolded → decl defaults to 0 AND emitDecl skips the
|
|
1517
|
+
// (const) runtime init → the binding reads 0 (e.g. subscript's NEW=CALL+1 → the
|
|
1518
|
+
// `new` keyword registers with precedence 0 and never dispatches).
|
|
1519
|
+
if (typeof n === 'string') return ctx.scope.constInts?.get(n) ?? null
|
|
906
1520
|
if (Array.isArray(n) && n[0] == null && typeof n[1] === 'number') return n[1]
|
|
907
1521
|
if (!Array.isArray(n)) return null
|
|
908
1522
|
const [op, a, b] = n
|
|
@@ -922,27 +1536,40 @@ export default function compile(ast, profiler) {
|
|
|
922
1536
|
: Array.isArray(n) && n[0] === 'const' ? [n] : []
|
|
923
1537
|
const stmts = [...topStmts(ast)]
|
|
924
1538
|
for (const mi of ctx.module.moduleInits || []) stmts.push(...topStmts(mi))
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1539
|
+
// Fixpoint: a const may reference one declared later or in another module
|
|
1540
|
+
// (`NEW = CALL + 1`). Each pass folds every now-resolvable initializer (its refs
|
|
1541
|
+
// already in constInts); repeat until none change so order/cross-module refs resolve.
|
|
1542
|
+
const foldedDecls = new Set()
|
|
1543
|
+
let changed = true
|
|
1544
|
+
while (changed) {
|
|
1545
|
+
changed = false
|
|
1546
|
+
for (const s of stmts) {
|
|
1547
|
+
if (!Array.isArray(s) || s[0] !== 'const') continue
|
|
1548
|
+
for (const decl of s.slice(1)) {
|
|
1549
|
+
if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
|
|
1550
|
+
const [, name, init] = decl
|
|
1551
|
+
if (foldedDecls.has(name)) continue
|
|
1552
|
+
if (!ctx.scope.globals.has(name) || !ctx.scope.consts?.has(name)) continue
|
|
1553
|
+
const v = evalConst(init)
|
|
1554
|
+
if (v == null || !isFinite(v)) continue
|
|
1555
|
+
foldedDecls.add(name)
|
|
1556
|
+
changed = true
|
|
1557
|
+
const isInt = Number.isInteger(v) && v >= I32_MIN && v <= I32_MAX
|
|
1558
|
+
declGlobal(name, isInt ? 'i32' : 'f64', v, { mut: false })
|
|
1559
|
+
// Cache integer values for cross-call const-arg propagation: `f(N)` where
|
|
1560
|
+
// `const N = 8` should observe the param as intConst=8.
|
|
1561
|
+
if (isInt) (ctx.scope.constInts ||= new Map()).set(name, v)
|
|
1562
|
+
}
|
|
941
1563
|
}
|
|
942
1564
|
}
|
|
943
1565
|
}
|
|
944
1566
|
|
|
945
|
-
|
|
1567
|
+
// Whole-program constant fold of module-scope aggregate literals — `var x=[1,2,3];
|
|
1568
|
+
// y=x[0]` → `y=1`, dropping the array (no data segment, no __arr_idx_known) when
|
|
1569
|
+
// every reference is a static read. The scalar analog of the constInts fold above.
|
|
1570
|
+
timePhase(profiler, 'foldAggregates', () => foldStaticConstAggregates(ast))
|
|
1571
|
+
|
|
1572
|
+
const programFacts = timePhase(profiler, 'plan', () => plan(ast, profiler))
|
|
946
1573
|
|
|
947
1574
|
// Inspect sink: editor hosts opt in via { inspect: true } to read inferred shapes.
|
|
948
1575
|
// Initialized here (post-plan) so paramReps and schema.list are stable, populated
|
|
@@ -950,28 +1577,30 @@ export default function compile(ast, profiler) {
|
|
|
950
1577
|
if (ctx.transform.inspect) ctx.inspect = { functions: {}, schemas: ctx.schema.list.map(s => s.slice()) }
|
|
951
1578
|
|
|
952
1579
|
const funcFacts = new Map()
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1580
|
+
timePhase(profiler, 'analyzeFuncs', () => {
|
|
1581
|
+
for (const func of ctx.func.list) {
|
|
1582
|
+
if (func.raw) continue
|
|
1583
|
+
const facts = analyzeFuncForEmit(func, programFacts)
|
|
1584
|
+
funcFacts.set(func, facts)
|
|
1585
|
+
captureFuncInspect(func, facts, programFacts)
|
|
1586
|
+
}
|
|
1587
|
+
})
|
|
959
1588
|
// Whole-program SRoA: pick the schemas whose `Array<S>` instances use the
|
|
960
1589
|
// `structInline` carrier. Runs once the per-function reps have settled (they
|
|
961
1590
|
// are codegen truth) and before any function is emitted.
|
|
962
|
-
analyzeStructInline(funcFacts, programFacts)
|
|
963
|
-
const funcs = ctx.func.list.map(func => emitFunc(func, funcFacts.get(func), programFacts))
|
|
1591
|
+
timePhase(profiler, 'structInline', () => analyzeStructInline(funcFacts, programFacts))
|
|
1592
|
+
const funcs = timePhase(profiler, 'emitFuncs', () => ctx.func.list.map(func => emitFunc(func, funcFacts.get(func), programFacts)))
|
|
964
1593
|
funcs.push(...synthesizeBoundaryWrappers())
|
|
965
1594
|
|
|
966
1595
|
const closureFuncs = []
|
|
967
1596
|
let compiledBodyCount = 0
|
|
968
|
-
const compilePendingClosures = () => {
|
|
1597
|
+
const compilePendingClosures = () => timePhase(profiler, 'emitClosures', () => {
|
|
969
1598
|
const bodies = ctx.closure.bodies || []
|
|
970
1599
|
for (let bodyIndex = compiledBodyCount; bodyIndex < bodies.length; bodyIndex++) {
|
|
971
1600
|
closureFuncs.push(emitClosureBody(bodies[bodyIndex]))
|
|
972
1601
|
}
|
|
973
1602
|
compiledBodyCount = bodies.length
|
|
974
|
-
}
|
|
1603
|
+
})
|
|
975
1604
|
compilePendingClosures()
|
|
976
1605
|
|
|
977
1606
|
// `wasm:js-string` imports — drained from `ctx.core.jsstring`, one
|
|
@@ -1055,7 +1684,15 @@ export default function compile(ast, profiler) {
|
|
|
1055
1684
|
if (ctx.closure.table?.length)
|
|
1056
1685
|
sec.elem.push(['elem', ['i32.const', 0], 'func', ...ctx.closure.table.map(n => `$${n}`)])
|
|
1057
1686
|
|
|
1058
|
-
buildStartFn(ast, sec, closureFuncs, compilePendingClosures)
|
|
1687
|
+
timePhase(profiler, 'buildStart', () => buildStartFn(ast, sec, closureFuncs, compilePendingClosures))
|
|
1688
|
+
|
|
1689
|
+
// Host globals (globalThis/process/WebAssembly/…) referenced as values are
|
|
1690
|
+
// recorded in ctx.core.hostGlobals during emit; register them as env imports
|
|
1691
|
+
// now (assembly owns ctx.module.imports). Drained after buildStartFn so a
|
|
1692
|
+
// host global first used in a top-level statement (emitted into __start) is
|
|
1693
|
+
// captured; syncImports below merges them into sec.imports.
|
|
1694
|
+
for (const name of ctx.core.hostGlobals)
|
|
1695
|
+
ctx.module.imports.push(['import', '"env"', `"${name}"`, ['global', `$${name}`, 'i64']])
|
|
1059
1696
|
|
|
1060
1697
|
syncImports(sec)
|
|
1061
1698
|
|
|
@@ -1063,16 +1700,38 @@ export default function compile(ast, profiler) {
|
|
|
1063
1700
|
|
|
1064
1701
|
finalizeClosureTable(sec)
|
|
1065
1702
|
|
|
1066
|
-
|
|
1703
|
+
buildInternTable()
|
|
1704
|
+
|
|
1705
|
+
timePhase(profiler, 'pullStdlib', () => pullStdlib(sec))
|
|
1067
1706
|
|
|
1068
1707
|
stripStaticDataPrefix(sec)
|
|
1069
1708
|
|
|
1070
1709
|
ensureThrowRuntime(sec)
|
|
1071
1710
|
|
|
1072
|
-
optimizeModule(sec)
|
|
1073
|
-
|
|
1074
|
-
//
|
|
1075
|
-
|
|
1711
|
+
timePhase(profiler, 'optimizeModule', () => optimizeModule(sec, profiler))
|
|
1712
|
+
|
|
1713
|
+
// Fold constant `__start` global inits into immutable inline decls (drops the
|
|
1714
|
+
// store, and `__start` with it when that empties it). Runs HERE — after
|
|
1715
|
+
// stripStaticDataPrefix and optimizeModule — so any data-segment offset a hoisted
|
|
1716
|
+
// pointer carries is already in its final, shifted form (hoisting earlier would
|
|
1717
|
+
// freeze a pre-strip offset the shift pass never revisits in the global decl).
|
|
1718
|
+
hoistConstGlobalInits(sec)
|
|
1719
|
+
|
|
1720
|
+
// Populate globals (after __start — const folding may update declarations).
|
|
1721
|
+
// Records build IR directly — no WAT-text parse-back.
|
|
1722
|
+
// The wasm type comes from globalTypes (the canonical name→type map declGlobal
|
|
1723
|
+
// maintains alongside the entry), falling back to the entry's own `.type`. They
|
|
1724
|
+
// are normally identical, but a global whose entry object is later rebuilt (e.g.
|
|
1725
|
+
// hoistConstGlobalInits' `{...g, …}` spread) must not depend on that rebuild
|
|
1726
|
+
// preserving `.type` — globalTypes is the stable source, so an entry that lost
|
|
1727
|
+
// its `.type` still emits a well-typed `(global …)` rather than `(undefined.const)`.
|
|
1728
|
+
sec.globals.push(...[...ctx.scope.globals].filter(([, g]) => g).map(([n, g]) => {
|
|
1729
|
+
const ty = ctx.scope.globalTypes.get(n) ?? g.type
|
|
1730
|
+
return ['global', `$${n}`,
|
|
1731
|
+
...(g.export ? [['export', `"${g.export}"`]] : []),
|
|
1732
|
+
g.mut ? ['mut', ty] : ty,
|
|
1733
|
+
[`${ty}.const`, g.init]]
|
|
1734
|
+
}))
|
|
1076
1735
|
|
|
1077
1736
|
// Data segments (after emit — string literals append to ctx.runtime.data / strPool during emit)
|
|
1078
1737
|
// Active segment at address 0 — skipped for shared memory (would collide across modules)
|
|
@@ -1124,26 +1783,6 @@ export default function compile(ast, profiler) {
|
|
|
1124
1783
|
if (restParamFuncs.length)
|
|
1125
1784
|
sec.customs.push(['@custom', '"jz:rest"', `"${JSON.stringify(restParamFuncs).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
1126
1785
|
|
|
1127
|
-
// Custom section: per-export i64 ABI map. Each entry describes an export
|
|
1128
|
-
// whose boundary wrapper carries NaN-boxed pointers via i64 (rather than
|
|
1129
|
-
// f64) to dodge V8's NaN canonicalization. Format: { name, p, r } where p
|
|
1130
|
-
// is an array of i64 param indices and r is 1 if result is i64. host.js
|
|
1131
|
-
// wrap() reinterprets BigInt↔f64 at i64 positions; numeric f64 positions
|
|
1132
|
-
// stay as Numbers on the JS side.
|
|
1133
|
-
const i64Exports = []
|
|
1134
|
-
for (const f of ctx.func.list) {
|
|
1135
|
-
if (!isExported(f) || !isBoundaryWrapped(f) || !f._exportUsesI64) continue
|
|
1136
|
-
const p = []
|
|
1137
|
-
f._exportI64Sig.params.forEach((b, i) => { if (b) p.push(i) })
|
|
1138
|
-
const r = f._exportI64Sig.result ? 1 : 0
|
|
1139
|
-
// One entry per JS-visible export name (inline + non-aliased + aliased
|
|
1140
|
-
// re-exports). `exportNamesOf` yields every name that resolves to f —
|
|
1141
|
-
// wrap() looks up by export name, not by internal symbol.
|
|
1142
|
-
for (const exportName of exportNamesOf(f.name)) i64Exports.push({ name: exportName, p, r })
|
|
1143
|
-
}
|
|
1144
|
-
if (i64Exports.length)
|
|
1145
|
-
sec.customs.push(['@custom', '"jz:i64exp"', `"${JSON.stringify(i64Exports).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
1146
|
-
|
|
1147
1786
|
// Custom section: per-export externref param positions. interop.js reads
|
|
1148
1787
|
// this to pass JS arguments straight through at those positions (no
|
|
1149
1788
|
// `mem.wrapVal`, no SSO encoding). Format: { name, p, d? } where p lists
|
|
@@ -1158,12 +1797,22 @@ export default function compile(ast, profiler) {
|
|
|
1158
1797
|
f._exportExtParams.forEach((b, i) => {
|
|
1159
1798
|
if (!b) return
|
|
1160
1799
|
p.push(i)
|
|
1161
|
-
|
|
1800
|
+
// String-key the index: object property keys are conceptually strings (JSON renders
|
|
1801
|
+
// `{"0":…}` either way), and the self-host kernel's objects don't enumerate a numeric
|
|
1802
|
+
// key — `d[0]=…` stores but Object.keys(d) misses it, so the `d` map would read empty
|
|
1803
|
+
// and the default never reach the jz:extparam section. Same coercion as the optimize
|
|
1804
|
+
// LEVEL_PRESETS lookup. (Native is unaffected: numeric keys auto-stringify.)
|
|
1805
|
+
if (typeof b === 'object' && b.def != null) d[String(i)] = b.def
|
|
1162
1806
|
})
|
|
1163
1807
|
if (!p.length) continue
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1808
|
+
// Build each export entry as a direct literal — no `entry.d = d` after the fact and no
|
|
1809
|
+
// `{...entry, name}` spread. The self-host kernel's fixed-schema objects don't enumerate
|
|
1810
|
+
// a key added to a non-empty literal (JSON.stringify/spread would silently drop a post-hoc
|
|
1811
|
+
// `d`), and spreading a 3-key literal mis-resolves the merged schema there. Constructing
|
|
1812
|
+
// the final shape directly sidesteps both.
|
|
1813
|
+
const hasDefaults = Object.keys(d).length > 0
|
|
1814
|
+
for (const exportName of exportNamesOf(f.name))
|
|
1815
|
+
extExports.push(hasDefaults ? { name: exportName, p, d } : { name: exportName, p })
|
|
1167
1816
|
}
|
|
1168
1817
|
if (extExports.length)
|
|
1169
1818
|
sec.customs.push(['@custom', '"jz:extparam"', `"${JSON.stringify(extExports).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
@@ -1192,7 +1841,8 @@ export default function compile(ast, profiler) {
|
|
|
1192
1841
|
const { callCount } = treeshake(
|
|
1193
1842
|
[{ arr: sec.stdlib }, { arr: sec.funcs }, { arr: sec.start }],
|
|
1194
1843
|
[...sec.start, ...sec.elem, ...sec.customs, ...sec.extStdlib, ...sec.imports, ...sec.tags],
|
|
1195
|
-
{ removeDead: !optCfg || optCfg.treeshake !== false, globals: sec.globals
|
|
1844
|
+
{ removeDead: !optCfg || optCfg.treeshake !== false, globals: sec.globals, userGlobals: ctx.scope.userGlobals,
|
|
1845
|
+
userFuncs: new Set(ctx.func.list.map(f => `$${f.name}`)) }
|
|
1196
1846
|
)
|
|
1197
1847
|
|
|
1198
1848
|
pruneUnusedThrowRuntime(sec)
|