jz 0.4.0 → 0.5.1
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 +275 -255
- package/cli.js +8 -51
- package/index.js +166 -31
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +181 -71
- package/module/collection.js +222 -8
- package/module/console.js +1 -1
- package/module/core.js +277 -118
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +361 -55
- package/module/math.js +601 -130
- package/module/number.js +390 -227
- package/module/object.js +252 -34
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +545 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +10 -7
- package/src/abi/array.js +89 -0
- package/src/abi/index.js +35 -0
- package/src/abi/number.js +137 -0
- package/src/abi/object.js +57 -0
- package/src/abi/string.js +529 -0
- package/src/analyze.js +1870 -485
- package/src/assemble.js +27 -12
- package/src/autoload.js +13 -1
- package/src/compile.js +446 -179
- package/src/ctx.js +85 -18
- package/src/emit.js +705 -196
- package/src/infer.js +548 -0
- package/src/ir.js +291 -23
- package/src/jzify.js +851 -132
- package/src/narrow.js +433 -251
- package/src/optimize.js +126 -122
- package/src/plan.js +703 -34
- package/src/prepare.js +833 -153
- package/src/resolve.js +85 -0
- package/src/vectorize.js +99 -18
- package/src/ast.js +0 -160
- package/src/auto-config.js +0 -120
- package/src/fuse.js +0 -159
package/src/compile.js
CHANGED
|
@@ -28,13 +28,15 @@
|
|
|
28
28
|
import { parse as parseWat } from 'watr'
|
|
29
29
|
import { ctx, err, inc, resolveIncludes, PTR, LAYOUT } from './ctx.js'
|
|
30
30
|
import {
|
|
31
|
-
T, VAL,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
isBlockBody,
|
|
31
|
+
T, VAL, analyzeBody,
|
|
32
|
+
unboxablePtrs, cseSafeLoadBases, typedElemAux, invalidateLocalsCache,
|
|
33
|
+
boxedCaptures, updateRep,
|
|
34
|
+
isBlockBody, analyzeStructInline,
|
|
35
35
|
} from './analyze.js'
|
|
36
|
+
import { inferLocals } from './infer.js'
|
|
36
37
|
import { optimizeFunc, treeshake } from './optimize.js'
|
|
37
38
|
import { emit, emitter, emitFlat, emitBody } from './emit.js'
|
|
39
|
+
import { emitCharDecompPrologue, JSS_IMPORT_SIGS } from './abi/string.js'
|
|
38
40
|
import {
|
|
39
41
|
typed, asF64, asI32, asPtrOffset, asParamType, toI32, asI64, fromI64,
|
|
40
42
|
NULL_NAN, UNDEF_NAN, NULL_WAT, UNDEF_WAT, NULL_IR, UNDEF_IR, nullExpr, undefExpr,
|
|
@@ -45,7 +47,9 @@ import {
|
|
|
45
47
|
isGlobal, isConst, boxedAddr, readVar, writeVar, isNullish, isUndef,
|
|
46
48
|
slotAddr, elemLoad, elemStore, arrayLoop, allocPtr,
|
|
47
49
|
multiCount, loopTop, flat, reconstructArgsWithSpreads,
|
|
48
|
-
valKindToPtr, findBodyStart,
|
|
50
|
+
valKindToPtr, findBodyStart, tcoTailRewrite,
|
|
51
|
+
boolBoxIR,
|
|
52
|
+
I32_MIN, I32_MAX,
|
|
49
53
|
} from './ir.js'
|
|
50
54
|
import plan from './plan.js'
|
|
51
55
|
import {
|
|
@@ -53,6 +57,53 @@ import {
|
|
|
53
57
|
pullStdlib, syncImports, optimizeModule, stripStaticDataPrefix,
|
|
54
58
|
} from './assemble.js'
|
|
55
59
|
|
|
60
|
+
// =============================================================================
|
|
61
|
+
// Single-source export semantics
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Two distinct concepts that callers used to conflate:
|
|
64
|
+
//
|
|
65
|
+
// 1. `f.exported` — *syntactic* inline-export form, snapshot at `defFunc`
|
|
66
|
+
// time (prepare.js). True iff the func decl carried the inline `export`
|
|
67
|
+
// keyword AND `ctx.func.exports[name]` was already populated by parent
|
|
68
|
+
// decl processing. Only the inline-emit gate below (`(func (export "name") ...)`)
|
|
69
|
+
// should read it — that emit path requires the inline-syntax invariant
|
|
70
|
+
// to avoid duplicate-export collisions with sec.customs.
|
|
71
|
+
//
|
|
72
|
+
// 2. `isExported(f)` — *semantic* "is this func reachable from JS via any
|
|
73
|
+
// export?". Covers the four forms equally:
|
|
74
|
+
// • inline: `export function foo` → exports[foo]=true
|
|
75
|
+
// • non-aliased: `function foo; export { foo }` → exports[foo]='foo'
|
|
76
|
+
// • aliased: `function foo; export { foo as bar }` → exports[bar]='foo'
|
|
77
|
+
// • default-by-name: `function foo; export default foo` → exports['default']='foo'
|
|
78
|
+
// Every public-ABI gate (boundary wrap, rest-param packing, i64 ABI,
|
|
79
|
+
// cross-call signature narrowing) should consult this.
|
|
80
|
+
|
|
81
|
+
/** Semantic export predicate. Use everywhere the question is "should this
|
|
82
|
+
* func behave as part of the public ABI?" — boundary-wrap, rest-pack,
|
|
83
|
+
* i64-ABI, sig-narrowing gates.
|
|
84
|
+
*
|
|
85
|
+
* `f.exported` short-circuits the inline-export case (no map walk needed);
|
|
86
|
+
* the value-scan picks up `export { f }` / `export { f as g }` / `export
|
|
87
|
+
* default f` where the source name appears as a *value* keyed under the
|
|
88
|
+
* public name. */
|
|
89
|
+
const isExported = f => {
|
|
90
|
+
if (f.exported) return true
|
|
91
|
+
for (const val of Object.values(ctx.func.exports)) {
|
|
92
|
+
if (val === f.name) return true
|
|
93
|
+
}
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Iterate JS-visible export names that resolve to `funcName`. Used to emit
|
|
98
|
+
* per-export ABI metadata in custom sections — one entry per JS-visible name,
|
|
99
|
+
* since the host (interop.js wrap) keys by export name. */
|
|
100
|
+
function* exportNamesOf(funcName) {
|
|
101
|
+
for (const [key, val] of Object.entries(ctx.func.exports)) {
|
|
102
|
+
if (val === true && key === funcName) yield key
|
|
103
|
+
else if (val === funcName) yield key
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
56
107
|
const timePhase = (profiler, name, fn) => profiler ? profiler.time(name, fn) : fn()
|
|
57
108
|
|
|
58
109
|
// Per-compile func name set + map live on ctx.func.names / ctx.func.map,
|
|
@@ -63,7 +114,7 @@ const timePhase = (profiler, name, fn) => profiler ? profiler.time(name, fn) : f
|
|
|
63
114
|
// buildArrayWithSpreads) moved to src/emit.js.
|
|
64
115
|
|
|
65
116
|
// AST-analysis primitives (staticObjectProps, paramReps lattice helpers,
|
|
66
|
-
//
|
|
117
|
+
// infer* cross-call inference, collectProgramFacts) moved to src/analyze.js.
|
|
67
118
|
|
|
68
119
|
/**
|
|
69
120
|
* Boundary-wrap predicate: exports whose body-driven result OR any param narrowed
|
|
@@ -77,69 +128,22 @@ const timePhase = (profiler, name, fn) => profiler ? profiler.time(name, fn) : f
|
|
|
77
128
|
* fractional Number gets the same truncation it would get from `arr[n]`).
|
|
78
129
|
*/
|
|
79
130
|
const isBoundaryWrapped = (func) => {
|
|
80
|
-
if (!func
|
|
131
|
+
if (!isExported(func) || func.raw || func.sig.results.length !== 1) return false
|
|
81
132
|
if (func.sig.results[0] !== 'f64' || func.sig.ptrKind != null) return true
|
|
133
|
+
// A boolean result rides the 0/1 number carrier internally; the export thunk
|
|
134
|
+
// boxes it to the TRUE_NAN/FALSE_NAN atom so the host sees a real boolean.
|
|
135
|
+
if (func.valResult === VAL.BOOL) return true
|
|
82
136
|
return func.sig.params.some(p => p.type !== 'f64' || p.ptrKind != null)
|
|
83
137
|
}
|
|
84
138
|
|
|
85
|
-
/**
|
|
86
|
-
* Tail-call rewrite: walks tail positions of an emitted IR tree and replaces
|
|
87
|
-
* direct `(call $name args...)` ops with `(return_call $name args...)`.
|
|
88
|
-
*
|
|
89
|
-
* Tail positions, recursively from the IR root:
|
|
90
|
-
* - the root itself (function's terminal value-producing expression)
|
|
91
|
-
* - both arms of `(if (result T) cond (then ...) (else ...))`
|
|
92
|
-
* - last instruction of `(block (result T) ...)`
|
|
93
|
-
*
|
|
94
|
-
* Only fires when caller and callee result types match — if they didn't match,
|
|
95
|
-
* `asParamType`/`asPtrOffset` would have wrapped the call in a conversion op,
|
|
96
|
-
* pushing the `call` away from the tail position. We don't recurse into
|
|
97
|
-
* arithmetic / select / loop ops: their results aren't standalone-tail control
|
|
98
|
-
* transfers.
|
|
99
|
-
*
|
|
100
|
-
* Mirrors the existing `'return'` op handler in emit.js (which already does
|
|
101
|
-
* TCO when the return statement is explicit). This pass closes the gap for
|
|
102
|
-
* expression-bodied arrows like `(n, acc) => n <= 0 ? acc : sum(n-1, acc+n)`
|
|
103
|
-
* — the AST has no `return` keyword so the emit-time handler never fires.
|
|
104
|
-
*/
|
|
105
|
-
const tcoTailRewrite = (ir, resultType) => {
|
|
106
|
-
if (ctx.transform.noTailCall || ctx.func.inTry) return ir
|
|
107
|
-
if (!Array.isArray(ir)) return ir
|
|
108
|
-
const op = ir[0]
|
|
109
|
-
if (op === 'call' && typeof ir[1] === 'string') {
|
|
110
|
-
// IR call name is `$name`; func.map keys are bare `name`.
|
|
111
|
-
const calleeName = ir[1].startsWith('$') ? ir[1].slice(1) : ir[1]
|
|
112
|
-
const callee = ctx.func.map.get(calleeName)
|
|
113
|
-
if (!callee || callee.raw) return ir
|
|
114
|
-
const calleeRT = callee.sig?.results?.[0] ?? 'f64'
|
|
115
|
-
if (calleeRT !== resultType) return ir
|
|
116
|
-
return typed(['return_call', ...ir.slice(1)], resultType)
|
|
117
|
-
}
|
|
118
|
-
if (op === 'if' && Array.isArray(ir[1]) && ir[1][0] === 'result') {
|
|
119
|
-
let changed = false
|
|
120
|
-
const newIr = ir.slice()
|
|
121
|
-
for (let i = 3; i < newIr.length; i++) {
|
|
122
|
-
const arm = newIr[i]
|
|
123
|
-
if (Array.isArray(arm) && (arm[0] === 'then' || arm[0] === 'else') && arm.length > 1) {
|
|
124
|
-
const last = arm[arm.length - 1]
|
|
125
|
-
const rewritten = tcoTailRewrite(last, resultType)
|
|
126
|
-
if (rewritten !== last) {
|
|
127
|
-
newIr[i] = [...arm.slice(0, -1), rewritten]
|
|
128
|
-
changed = true
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return changed ? typed(newIr, ir.type) : ir
|
|
133
|
-
}
|
|
134
|
-
if (op === 'block' && ir.length > 1) {
|
|
135
|
-
const last = ir[ir.length - 1]
|
|
136
|
-
const rewritten = tcoTailRewrite(last, resultType)
|
|
137
|
-
if (rewritten !== last) return typed([...ir.slice(0, -1), rewritten], ir.type)
|
|
138
|
-
}
|
|
139
|
-
return ir
|
|
140
|
-
}
|
|
141
|
-
|
|
142
139
|
const ensureThrowRuntime = (sec) => {
|
|
140
|
+
// A pulled stdlib helper may throw $__jz_err even when no user `throw` set the
|
|
141
|
+
// flag (e.g. __to_num on a Symbol). Detect it from the included stdlib bodies
|
|
142
|
+
// so the $__jz_err tag is always present when something can raise it.
|
|
143
|
+
if (!ctx.runtime.throws && [...ctx.core.includes].some(n => {
|
|
144
|
+
const body = ctx.core.stdlib[n]
|
|
145
|
+
return typeof body === 'string' && body.includes('(throw ')
|
|
146
|
+
})) ctx.runtime.throws = true
|
|
143
147
|
if (!ctx.runtime.throws) return
|
|
144
148
|
|
|
145
149
|
if (!ctx.scope.globals.has('__jz_last_err_bits'))
|
|
@@ -150,17 +154,110 @@ const ensureThrowRuntime = (sec) => {
|
|
|
150
154
|
sec.tags.push(['export', '"__jz_last_err_bits"', ['global', '$__jz_last_err_bits']])
|
|
151
155
|
}
|
|
152
156
|
|
|
157
|
+
// Drop the $__jz_err tag + __jz_last_err_bits globals when optimization
|
|
158
|
+
// eliminated every actual throw site. ensureThrowRuntime runs before
|
|
159
|
+
// optimizeModule so dead-throw analysis can see the tag/global as live; once
|
|
160
|
+
// opt has finished, an unused tag still forces consumers (wasmtime, wasm2c) to
|
|
161
|
+
// enable the exceptions proposal just to parse the module. User-written
|
|
162
|
+
// throw/try/catch/finally is an ABI contract (JS-side may inspect
|
|
163
|
+
// __jz_last_err_bits), so `userThrows` keeps the runtime declared regardless;
|
|
164
|
+
// the prune fires only when `throws` was set purely by stdlib pattern matching
|
|
165
|
+
// or compiler-internal coercion sites.
|
|
166
|
+
const pruneUnusedThrowRuntime = (sec) => {
|
|
167
|
+
if (!ctx.runtime.throws || ctx.runtime.userThrows) return
|
|
168
|
+
const hasThrow = (n) => Array.isArray(n) && (n[0] === 'throw' || n.some(hasThrow))
|
|
169
|
+
for (const arr of [sec.funcs, sec.stdlib, sec.start])
|
|
170
|
+
for (const f of arr) if (hasThrow(f)) return
|
|
171
|
+
sec.tags = sec.tags.filter(t => !(Array.isArray(t) &&
|
|
172
|
+
((t[0] === 'tag' && t[1] === '$__jz_err') ||
|
|
173
|
+
(t[0] === 'export' && t[1] === '"__jz_last_err_bits"'))))
|
|
174
|
+
sec.globals = sec.globals.filter(g => !(Array.isArray(g) &&
|
|
175
|
+
g[0] === 'global' && g[1] === '$__jz_last_err_bits'))
|
|
176
|
+
ctx.scope.globals.delete('__jz_last_err_bits')
|
|
177
|
+
}
|
|
178
|
+
|
|
153
179
|
// === Module compilation ===
|
|
154
180
|
|
|
155
181
|
const cloneRepMap = map => map ? new Map([...map].map(([k, v]) => [k, { ...v }])) : null
|
|
156
182
|
|
|
157
|
-
|
|
183
|
+
/** Serialize a ValueRep entry into a plain object for inspect output.
|
|
184
|
+
* Omits undefined fields so consumers can JSON-stringify without noise. */
|
|
185
|
+
const repView = (rep) => {
|
|
186
|
+
if (!rep) return null
|
|
187
|
+
const out = {}
|
|
188
|
+
for (const k of ['val', 'ptrKind', 'ptrAux', 'schemaId', 'intConst', 'intCertain', 'notString', 'arrayElemSchema', 'arrayElemValType', 'typedCtor', 'jsonShape', 'wasm']) {
|
|
189
|
+
if (rep[k] != null) out[k] = rep[k]
|
|
190
|
+
}
|
|
191
|
+
return Object.keys(out).length ? out : null
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Capture a function's inferred shape into ctx.inspect.functions. Called after
|
|
195
|
+
* analyzeFuncForEmit when transform.inspect is set — reads from funcFacts +
|
|
196
|
+
* programFacts.paramReps, never from the live ctx.func.* (which churns per emit). */
|
|
197
|
+
function captureFuncInspect(func, facts, programFacts) {
|
|
198
|
+
if (!ctx.inspect || func.raw) return
|
|
199
|
+
const { name, sig } = func
|
|
200
|
+
const reps = facts?.localReps
|
|
201
|
+
const paramNames = new Set(sig.params.map(p => p.name))
|
|
202
|
+
const params = sig.params.map(p => ({
|
|
203
|
+
name: p.name, type: p.type,
|
|
204
|
+
...(p.ptrKind != null ? { ptrKind: p.ptrKind } : {}),
|
|
205
|
+
...(p.ptrAux != null ? { ptrAux: p.ptrAux } : {}),
|
|
206
|
+
...(repView(reps?.get(p.name)) || {}),
|
|
207
|
+
}))
|
|
208
|
+
const locals = {}
|
|
209
|
+
if (facts?.locals) {
|
|
210
|
+
for (const [lname, ltype] of facts.locals) {
|
|
211
|
+
if (paramNames.has(lname)) continue
|
|
212
|
+
const v = repView(reps?.get(lname))
|
|
213
|
+
locals[lname] = v ? { type: ltype, ...v } : { type: ltype }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const callerReps = {}
|
|
217
|
+
const cr = programFacts.paramReps?.get(name)
|
|
218
|
+
if (cr) for (const [idx, r] of cr) {
|
|
219
|
+
const v = repView(r)
|
|
220
|
+
if (v) callerReps[idx] = v
|
|
221
|
+
}
|
|
222
|
+
ctx.inspect.functions[name] = {
|
|
223
|
+
exported: isExported(func),
|
|
224
|
+
params,
|
|
225
|
+
results: sig.results.slice(),
|
|
226
|
+
...(sig.ptrKind != null ? { resultPtrKind: sig.ptrKind } : {}),
|
|
227
|
+
...(sig.ptrAux != null ? { resultPtrAux: sig.ptrAux } : {}),
|
|
228
|
+
locals,
|
|
229
|
+
...(Object.keys(callerReps).length ? { callerReps } : {}),
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Reset per-function emit-frame state — the single source of frame entry.
|
|
234
|
+
// `emitFunc`, `analyzeFuncForEmit`, and `emitClosureBody` all route through
|
|
235
|
+
// here. Top-level funcs start `uniq` at 0; closures pass a higher base so
|
|
236
|
+
// their synthetic labels can't collide with the parent frame's.
|
|
237
|
+
function enterFunc(sig, body, { uniq = 0, directClosures = null } = {}) {
|
|
158
238
|
ctx.func.stack = []
|
|
159
|
-
ctx.func.uniq =
|
|
160
|
-
ctx.func.current =
|
|
161
|
-
ctx.func.body =
|
|
162
|
-
ctx.func.directClosures =
|
|
239
|
+
ctx.func.uniq = uniq
|
|
240
|
+
ctx.func.current = sig
|
|
241
|
+
ctx.func.body = body
|
|
242
|
+
ctx.func.directClosures = directClosures
|
|
163
243
|
ctx.func.localProps = null
|
|
244
|
+
ctx.func.charDecomp = null
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Allocate + null-init a heap cell for every boxed local that isn't seeded
|
|
248
|
+
// from an incoming param/capture value. Registers the cell as an i32 local
|
|
249
|
+
// and marks the name preboxed; `isSeeded(name)` skips the already-seeded.
|
|
250
|
+
function emitPreboxedLocalInits(isSeeded) {
|
|
251
|
+
const inits = []
|
|
252
|
+
for (const [name, cell] of ctx.func.boxed) {
|
|
253
|
+
if (isSeeded(name)) continue
|
|
254
|
+
ctx.func.locals.set(cell, 'i32')
|
|
255
|
+
ctx.func.preboxed.add(name)
|
|
256
|
+
inits.push(
|
|
257
|
+
['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]],
|
|
258
|
+
['f64.store', ['local.get', `$${cell}`], nullExpr()])
|
|
259
|
+
}
|
|
260
|
+
return inits
|
|
164
261
|
}
|
|
165
262
|
|
|
166
263
|
function analyzeFuncForEmit(func, programFacts) {
|
|
@@ -168,11 +265,11 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
168
265
|
if (func.raw) return null
|
|
169
266
|
|
|
170
267
|
const { name, body, sig } = func
|
|
171
|
-
enterFunc(
|
|
268
|
+
enterFunc(sig, body)
|
|
172
269
|
|
|
173
270
|
const block = isBlockBody(body)
|
|
174
271
|
ctx.func.boxed = new Map()
|
|
175
|
-
ctx.func.
|
|
272
|
+
ctx.func.localReps = null
|
|
176
273
|
ctx.types.typedElem = ctx.scope.globalTypedElem ? new Map(ctx.scope.globalTypedElem) : null
|
|
177
274
|
|
|
178
275
|
const _reps = paramReps.get(name)
|
|
@@ -185,41 +282,52 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
185
282
|
if (!ctx.types.typedElem.has(pname)) ctx.types.typedElem.set(pname, r.typedCtor)
|
|
186
283
|
updateRep(pname, { val: VAL.TYPED })
|
|
187
284
|
}
|
|
188
|
-
if (r.val && !ctx.func.
|
|
285
|
+
if (r.val && !ctx.func.localReps?.get(pname)?.val) updateRep(pname, { val: r.val })
|
|
189
286
|
if (r.arrayElemSchema != null) updateRep(pname, { arrayElemSchema: r.arrayElemSchema })
|
|
190
287
|
if (r.arrayElemValType != null) updateRep(pname, { arrayElemValType: r.arrayElemValType })
|
|
191
288
|
if (r.intConst != null) updateRep(pname, { intConst: r.intConst })
|
|
192
289
|
}
|
|
193
290
|
}
|
|
194
|
-
// Drop any earlier-cached
|
|
195
|
-
// it before our pre-seed, when params still had no
|
|
196
|
-
// cached widths reflect the pre-narrow state.
|
|
291
|
+
// Drop any earlier-cached analyzeBody.locals slice for this body —
|
|
292
|
+
// narrowSignatures called it before our pre-seed, when params still had no
|
|
293
|
+
// inferred VAL.TYPED, so the cached widths reflect the pre-narrow state.
|
|
294
|
+
// Re-walk now with reps in place.
|
|
197
295
|
invalidateLocalsCache(body)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
296
|
+
const bodyFacts = block ? analyzeBody(body) : null
|
|
297
|
+
ctx.func.locals = bodyFacts ? bodyFacts.locals : new Map()
|
|
298
|
+
// Proven uint32 accumulator locals — readVar tags reads `.unsigned` so the
|
|
299
|
+
// f64 round-trip widens with convert_i32_u (not _s).
|
|
300
|
+
if (bodyFacts?.unsignedLocals) for (const n of bodyFacts.unsignedLocals) updateRep(n, { unsigned: true })
|
|
301
|
+
// SRoA flat-object bindings — `let o = {...}` dissolved into `o#i` field
|
|
302
|
+
// locals. Consumed by the codegen flat hooks (emitDecl, `.`/`[]` read+write).
|
|
303
|
+
ctx.func.flatObjects = bodyFacts ? bodyFacts.flatObjects : new Map()
|
|
304
|
+
// No-copy slice views — `let t = s.slice(...)` bindings proven non-escaping.
|
|
305
|
+
// Consumed by emitDecl to lower the initializer to a SLICE_BIT view.
|
|
306
|
+
ctx.func.sliceViews = bodyFacts ? bodyFacts.sliceViews : new Set()
|
|
307
|
+
// Usage-based shape inference (STRING / ARRAY) for params not already typed
|
|
308
|
+
// by paramReps. Descends into nested closures so a param used in a definite
|
|
309
|
+
// shape only inside an inner arrow (e.g. parseLevel's `str` capture in watr)
|
|
310
|
+
// still gets seeded — the closure capture path then propagates the VAL via
|
|
311
|
+
// captureValTypes.
|
|
312
|
+
//
|
|
313
|
+
// `inferLocals` is body-shape-agnostic — it walks any AST node, so we run it
|
|
314
|
+
// for expression-bodied arrows too (`(s) => s.charCodeAt(0) + s.length` gets
|
|
315
|
+
// `s: VAL.STRING` via methodEvidence the same way the block-bodied variant
|
|
316
|
+
// does). Only `boxedCaptures` / `unboxablePtrs` stay gated:
|
|
317
|
+
// both need `ctx.func.locals` populated, which only block bodies produce.
|
|
318
|
+
const candidates = sig.params
|
|
319
|
+
.filter(p => !ctx.func.localReps?.get(p.name)?.val)
|
|
320
|
+
.map(p => p.name)
|
|
321
|
+
inferLocals(body, candidates)
|
|
212
322
|
if (block) {
|
|
213
|
-
|
|
214
|
-
analyzeIntCertain(body)
|
|
215
|
-
analyzeBoxedCaptures(body)
|
|
323
|
+
boxedCaptures(body)
|
|
216
324
|
// Lower provably-monomorphic pointer locals to i32 offset storage.
|
|
217
325
|
// VAL.TYPED unbox requires a known element ctor (aux byte) — without it,
|
|
218
326
|
// the use site can't pick the right i32.store{8,16}/i32.store width and
|
|
219
327
|
// the rebox path can't reconstruct the NaN-box. Heterogeneous decls (two
|
|
220
328
|
// `let arr = ...` with different ctors, or a multi-ctor ternary) leave
|
|
221
329
|
// typedElem unset; skip unbox so reads/writes go through `__typed_set_idx`.
|
|
222
|
-
const unbox =
|
|
330
|
+
const unbox = unboxablePtrs(body, ctx.func.locals, ctx.func.boxed)
|
|
223
331
|
if (unbox.size > 0) {
|
|
224
332
|
for (const [n, kind] of unbox) {
|
|
225
333
|
const fields = { ptrKind: kind }
|
|
@@ -234,7 +342,7 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
234
342
|
}
|
|
235
343
|
}
|
|
236
344
|
// Pointer-ABI params (from narrowing loop above): params already have type='i32' and
|
|
237
|
-
// ptrKind set. Register them in ctx.func.
|
|
345
|
+
// ptrKind set. Register them in ctx.func.localReps so readVar tags local.gets correctly.
|
|
238
346
|
// Boxed capture still works: the boxed-init path (below) uses a ptrKind-tagged local.get
|
|
239
347
|
// so asF64 reboxes to NaN-form before f64.store to the cell.
|
|
240
348
|
for (const p of sig.params) {
|
|
@@ -244,12 +352,21 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
244
352
|
updateRep(p.name, fields)
|
|
245
353
|
}
|
|
246
354
|
|
|
355
|
+
// CSE-safe load bases — pointer locals whose memory reads `cseScalarLoad`
|
|
356
|
+
// may scalar-replace. Computed last: needs every `let`/param ptrKind in place.
|
|
357
|
+
const cseLoadBases = block
|
|
358
|
+
? cseSafeLoadBases(body, ctx.func.locals, ctx.func.localReps)
|
|
359
|
+
: new Set()
|
|
360
|
+
|
|
247
361
|
return {
|
|
248
362
|
block,
|
|
249
363
|
locals: new Map(ctx.func.locals),
|
|
250
364
|
boxed: new Map(ctx.func.boxed),
|
|
365
|
+
flatObjects: new Map(ctx.func.flatObjects),
|
|
366
|
+
sliceViews: new Set(ctx.func.sliceViews),
|
|
367
|
+
cseLoadBases,
|
|
251
368
|
typedElem: ctx.types.typedElem ? new Map(ctx.types.typedElem) : null,
|
|
252
|
-
|
|
369
|
+
localReps: cloneRepMap(ctx.func.localReps),
|
|
253
370
|
}
|
|
254
371
|
}
|
|
255
372
|
|
|
@@ -269,11 +386,13 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
269
386
|
const multi = sig.results.length > 1
|
|
270
387
|
const _reps = paramReps.get(name)
|
|
271
388
|
|
|
272
|
-
enterFunc(
|
|
389
|
+
enterFunc(sig, body)
|
|
273
390
|
const block = funcFacts.block
|
|
274
391
|
ctx.func.locals = new Map(funcFacts.locals)
|
|
275
392
|
ctx.func.boxed = new Map(funcFacts.boxed)
|
|
276
|
-
ctx.func.
|
|
393
|
+
ctx.func.flatObjects = new Map(funcFacts.flatObjects)
|
|
394
|
+
ctx.func.sliceViews = new Set(funcFacts.sliceViews)
|
|
395
|
+
ctx.func.localReps = cloneRepMap(funcFacts.localReps)
|
|
277
396
|
ctx.types.typedElem = funcFacts.typedElem ? new Map(funcFacts.typedElem) : null
|
|
278
397
|
|
|
279
398
|
// D: Apply call-site param facts (only if body analysis didn't already set them).
|
|
@@ -284,11 +403,11 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
284
403
|
for (const [k, r] of _reps) {
|
|
285
404
|
if (k >= sig.params.length) continue
|
|
286
405
|
const pname = sig.params[k].name
|
|
287
|
-
if (r.val && !ctx.func.
|
|
406
|
+
if (r.val && !ctx.func.localReps?.get(pname)?.val) updateRep(pname, { val: r.val })
|
|
288
407
|
if (r.typedCtor) {
|
|
289
408
|
if (!ctx.types.typedElem) ctx.types.typedElem = new Map()
|
|
290
409
|
if (!ctx.types.typedElem.has(pname)) ctx.types.typedElem.set(pname, r.typedCtor)
|
|
291
|
-
if (!ctx.func.
|
|
410
|
+
if (!ctx.func.localReps?.get(pname)?.val) updateRep(pname, { val: VAL.TYPED })
|
|
292
411
|
}
|
|
293
412
|
if (r.schemaId != null && !exported && !ctx.schema.vars.has(pname)) {
|
|
294
413
|
ctx.schema.vars.set(pname, r.schemaId)
|
|
@@ -298,7 +417,18 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
298
417
|
}
|
|
299
418
|
|
|
300
419
|
const fn = ['func', `$${name}`]
|
|
301
|
-
//
|
|
420
|
+
// Stamp the emit-side CSE soundness whitelist onto the func node (expando —
|
|
421
|
+
// watr print/compile ignore non-index props). `cseScalarLoad` reads it; absent
|
|
422
|
+
// it the pass is a no-op. `$`-prefixed to match WAT local names directly.
|
|
423
|
+
if (funcFacts.cseLoadBases?.size)
|
|
424
|
+
fn.cseLoadBases = new Set([...funcFacts.cseLoadBases].map(n => `$${n}`))
|
|
425
|
+
// Inline `(export ...)` attribute only for the syntactic inline-export
|
|
426
|
+
// form (`export function foo`, snapshot in `func.exported` at defFunc
|
|
427
|
+
// time). Re-exports (`function foo; export { foo }`) and aliases (`export
|
|
428
|
+
// { foo as bar }`) flow through sec.customs below — emitting an inline
|
|
429
|
+
// attribute under the internal symbol would collide with the customs
|
|
430
|
+
// entry on the same name, or leak the internal symbol publicly.
|
|
431
|
+
// Boundary-wrapped exports also defer the attribute to the synthesized
|
|
302
432
|
// wrapper ($${name}$exp) that reboxes the narrowed result back to f64.
|
|
303
433
|
if (exported && !isBoundaryWrapped(func)) fn.push(['export', `"${name}"`])
|
|
304
434
|
fn.push(...sig.params.map(p => ['param', `$${p.name}`, p.type]))
|
|
@@ -306,19 +436,26 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
306
436
|
|
|
307
437
|
// Default params: ES spec says default applies only when arg is `undefined`
|
|
308
438
|
// (or missing). `null`, `0`, `false`, etc. all skip the default.
|
|
439
|
+
// Emitted here (registers any `charCodeAt` decomposition the default's
|
|
440
|
+
// initializer triggers) but keyed by param name — final ordering vs the
|
|
441
|
+
// charDecomp prologue is resolved in `collectParamInits` below.
|
|
309
442
|
const defaults = func.defaults || {}
|
|
310
|
-
const defaultInits =
|
|
443
|
+
const defaultInits = new Map()
|
|
311
444
|
for (const [pname, defVal] of Object.entries(defaults)) {
|
|
312
445
|
const p = sig.params.find(p => p.name === pname)
|
|
446
|
+
// jsstring-carrier params with string-literal defaults skip wasm-side
|
|
447
|
+
// substitution — the interop wrapper applies the default JS-side (the
|
|
448
|
+
// value rides through `jz:extparam`). The wasm side never sees a null
|
|
449
|
+
// externref so no `ref.is_null` branch is needed.
|
|
450
|
+
if (p?.jsstring && p.jsstringDefault != null) continue
|
|
313
451
|
const t = p?.type || 'f64'
|
|
314
|
-
defaultInits.
|
|
452
|
+
defaultInits.set(pname,
|
|
315
453
|
['if', isUndef(typed(['local.get', `$${pname}`], 'f64')),
|
|
316
454
|
['then', ['local.set', `$${pname}`, t === 'f64' ? asF64(emit(defVal)) : asI32(emit(defVal))]]])
|
|
317
455
|
}
|
|
318
456
|
|
|
319
457
|
// Box params that are mutably captured: allocate cell, copy param value
|
|
320
458
|
const boxedParamInits = []
|
|
321
|
-
const preboxedLocalInits = []
|
|
322
459
|
ctx.func.preboxed = new Set()
|
|
323
460
|
const paramNames = new Set(sig.params.map(p => p.name))
|
|
324
461
|
for (const p of sig.params) {
|
|
@@ -333,31 +470,58 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
333
470
|
['f64.store', ['local.get', `$${cell}`], asF64(lget)])
|
|
334
471
|
}
|
|
335
472
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
473
|
+
// Remaining boxed locals (non-params) get a fresh null-init cell.
|
|
474
|
+
const preboxedLocalInits = emitPreboxedLocalInits(name => paramNames.has(name))
|
|
475
|
+
|
|
476
|
+
// Drain `ctx.func.charDecomp` after body emit: any param `charCodeAt` use
|
|
477
|
+
// registered a decomposition request that needs a function-entry prologue
|
|
478
|
+
// initialising its four i32 locals (base / len / sso / loadbase). Locals
|
|
479
|
+
// themselves were already added to `ctx.func.locals` during emit so they
|
|
480
|
+
// appear in the local-decl block below.
|
|
481
|
+
//
|
|
482
|
+
// Interleave with the per-param default inits in `sig.params` order so each
|
|
483
|
+
// param's prologue runs *after* that param's own default init (the prologue
|
|
484
|
+
// reads the param's final value) and *before* any later param's default
|
|
485
|
+
// init — a default like `c = op.charCodeAt(0)` must see `op`'s prologue
|
|
486
|
+
// locals already populated, else its bounds check reads len=0 and the
|
|
487
|
+
// in-bounds char wrongly decodes as the OOB NaN.
|
|
488
|
+
const collectParamInits = () => {
|
|
489
|
+
const inits = []
|
|
490
|
+
for (const p of sig.params) {
|
|
491
|
+
const di = defaultInits.get(p.name)
|
|
492
|
+
if (di) inits.push(di)
|
|
493
|
+
const dec = ctx.func.charDecomp?.get(p.name)
|
|
494
|
+
if (dec) inits.push(...emitCharDecompPrologue(dec))
|
|
495
|
+
}
|
|
496
|
+
return inits
|
|
343
497
|
}
|
|
344
498
|
|
|
345
499
|
if (block) {
|
|
346
500
|
const stmts = emitBody(body)
|
|
501
|
+
const paramInits = collectParamInits()
|
|
347
502
|
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
348
503
|
// I: Skip trailing fallback when last statement is return (unreachable code)
|
|
349
504
|
const lastStmt = stmts.at(-1)
|
|
350
505
|
const endsWithReturn = lastStmt && (lastStmt[0] === 'return' || lastStmt[0] === 'return_call')
|
|
351
|
-
|
|
506
|
+
// Implicit fall-through return is `undefined` per JS spec, not 0 — same as
|
|
507
|
+
// the closure path below. A reachable fall-through forces an f64 result
|
|
508
|
+
// (it must carry undefined); concretely-typed results keep the `.const 0`
|
|
509
|
+
// form since they can only be reached via explicit typed returns.
|
|
510
|
+
const fallthrough = endsWithReturn ? []
|
|
511
|
+
: sig.results.length === 1 && sig.results[0] === 'f64' ? [undefExpr()]
|
|
512
|
+
: sig.results.map(t => [`${t}.const`, 0])
|
|
513
|
+
fn.push(...paramInits, ...boxedParamInits, ...preboxedLocalInits, ...stmts, ...fallthrough)
|
|
352
514
|
} else if (multi && body[0] === '[') {
|
|
353
515
|
const values = body.slice(1).map(e => asF64(emit(e)))
|
|
516
|
+
const paramInits = collectParamInits()
|
|
354
517
|
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
355
|
-
fn.push(...boxedParamInits, ...preboxedLocalInits, ...values)
|
|
518
|
+
fn.push(...paramInits, ...boxedParamInits, ...preboxedLocalInits, ...values)
|
|
356
519
|
} else {
|
|
357
520
|
const ir = emit(body)
|
|
521
|
+
const paramInits = collectParamInits()
|
|
358
522
|
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
359
523
|
const finalIR = sig.ptrKind != null ? asPtrOffset(ir, sig.ptrKind) : asParamType(ir, sig.results[0])
|
|
360
|
-
fn.push(...
|
|
524
|
+
fn.push(...paramInits, ...boxedParamInits, ...preboxedLocalInits, tcoTailRewrite(finalIR, sig.results[0]))
|
|
361
525
|
}
|
|
362
526
|
|
|
363
527
|
// Restore schema.vars so param bindings don't leak to next function.
|
|
@@ -373,7 +537,7 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
373
537
|
* - takes i64 params always — JS-side carrier is BigInt that reinterprets to
|
|
374
538
|
* f64 NaN-box bits. i64 dodges V8's spec-permitted NaN canonicalization at
|
|
375
539
|
* the wasm↔JS boundary (see ToJSValue / ToWebAssemblyValue). Host wrap()
|
|
376
|
-
* in
|
|
540
|
+
* in interop.js pairs by converting BigInt↔f64 via reinterpret bits.
|
|
377
541
|
* - converts each narrowed param at the call: f64 → i32 (truncate-sat) for
|
|
378
542
|
* numeric narrowed, f64 → i32-offset (`i32.wrap_i64 + i64.reinterpret_f64`)
|
|
379
543
|
* for pointer narrowed. The reinterpret happens once at param decode and
|
|
@@ -400,13 +564,31 @@ function synthesizeBoundaryWrappers() {
|
|
|
400
564
|
// actually crosses the boundary (param.ptrKind set, or result with
|
|
401
565
|
// sig.ptrKind set). Numeric narrowing (i32 trunc-sat / convert) keeps f64
|
|
402
566
|
// so callers seeing the raw export get a plain Number for numerics.
|
|
567
|
+
// jsstring params bypass both i64 and f64 — they flow as externref end-to-end.
|
|
403
568
|
const paramI64 = sig.params.map(p => p.ptrKind != null)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
569
|
+
// A VAL.BOOL result is boxed to a NaN-box atom here, so it crosses as i64
|
|
570
|
+
// (same carrier as a pointer result), even though the inner func returns the
|
|
571
|
+
// plain 0/1 number carrier.
|
|
572
|
+
const resultBool = func.valResult === VAL.BOOL && sig.ptrKind == null
|
|
573
|
+
const resultI64 = sig.ptrKind != null || resultBool
|
|
574
|
+
// Inline `(export ...)` attribute only when the func decl carried the
|
|
575
|
+
// inline-export keyword (`export function foo`). For re-exports
|
|
576
|
+
// (`function foo; export { foo as bar }`) the `name` is the *internal*
|
|
577
|
+
// symbol; sec.customs holds the JS-visible export pointing at this
|
|
578
|
+
// wrapper. Emitting an inline attribute here under the internal name
|
|
579
|
+
// would leak the symbol publicly and collide with the customs entry.
|
|
580
|
+
const wrapNode = func.exported
|
|
581
|
+
? ['func', `$${name}$exp`, ['export', `"${name}"`]]
|
|
582
|
+
: ['func', `$${name}$exp`]
|
|
583
|
+
sig.params.forEach((p, i) => {
|
|
584
|
+
const t = p.jsstring ? 'externref' : (paramI64[i] ? 'i64' : 'f64')
|
|
585
|
+
wrapNode.push(['param', `$${p.name}`, t])
|
|
586
|
+
})
|
|
407
587
|
wrapNode.push(['result', resultI64 ? 'i64' : 'f64'])
|
|
408
588
|
const args = sig.params.map((p, i) => {
|
|
409
589
|
const get = ['local.get', `$${p.name}`]
|
|
590
|
+
// jsstring: externref flows through unchanged — inner func also takes externref.
|
|
591
|
+
if (p.jsstring) return get
|
|
410
592
|
if (p.ptrKind != null) {
|
|
411
593
|
// ptrKind: i64 carrier carries NaN-box bits → wrap to i32 offset
|
|
412
594
|
return ['i32.wrap_i64', get]
|
|
@@ -420,6 +602,10 @@ function synthesizeBoundaryWrappers() {
|
|
|
420
602
|
if (sig.ptrKind != null) {
|
|
421
603
|
const ptrType = valKindToPtr(sig.ptrKind)
|
|
422
604
|
body = mkPtrIR(ptrType, sig.ptrAux ?? 0, callIR)
|
|
605
|
+
} else if (resultBool) {
|
|
606
|
+
// boolBoxIR's truthy extraction depends on the carrier type, so the bare
|
|
607
|
+
// call node must declare it (f64 0/1, or i32 when the result narrowed).
|
|
608
|
+
body = boolBoxIR(typed(callIR, sig.results[0])) // 0/1 carrier → TRUE_NAN/FALSE_NAN atom
|
|
423
609
|
} else if (sig.results[0] === 'i32') {
|
|
424
610
|
body = [sig.unsignedResult ? 'f64.convert_i32_u' : 'f64.convert_i32_s', callIR]
|
|
425
611
|
} else {
|
|
@@ -428,6 +614,16 @@ function synthesizeBoundaryWrappers() {
|
|
|
428
614
|
wrapNode.push(resultI64 ? ['i64.reinterpret_f64', body] : body)
|
|
429
615
|
func._exportUsesI64 = resultI64 || paramI64.some(Boolean)
|
|
430
616
|
func._exportI64Sig = { params: paramI64, result: resultI64 }
|
|
617
|
+
// Track externref param positions so interop.js can pass JS values
|
|
618
|
+
// raw (skipping `mem.wrapVal`) at those slots. Today this only fires
|
|
619
|
+
// for `jsstring`-tagged params; future externref carriers wire here too.
|
|
620
|
+
// `extParams` is per-slot: false (non-ext) | { def: '...' }-bearing object
|
|
621
|
+
// for jsstring params with a JS-side default substitution.
|
|
622
|
+
const extParams = sig.params.map(p => {
|
|
623
|
+
if (!p.jsstring) return false
|
|
624
|
+
return p.jsstringDefault != null ? { def: p.jsstringDefault } : true
|
|
625
|
+
})
|
|
626
|
+
if (extParams.some(Boolean)) func._exportExtParams = extParams
|
|
431
627
|
wrappers.push(wrapNode)
|
|
432
628
|
}
|
|
433
629
|
return wrappers
|
|
@@ -441,7 +637,7 @@ function synthesizeBoundaryWrappers() {
|
|
|
441
637
|
* so any closure can be invoked via call_indirect on $ftN. This function
|
|
442
638
|
* builds one body fn given the body record (cb) created by ctx.closure.make.
|
|
443
639
|
*
|
|
444
|
-
* Mutates ctx.func.* per-body state (locals, boxed,
|
|
640
|
+
* Mutates ctx.func.* per-body state (locals, boxed, localReps) and
|
|
445
641
|
* ctx.schema.vars / ctx.types.typedElem (restored on exit so capture-binding
|
|
446
642
|
* leaks don't poison the next body). Returns the WAT IR for the func node.
|
|
447
643
|
*/
|
|
@@ -450,8 +646,9 @@ function emitClosureBody(cb) {
|
|
|
450
646
|
const prevTypedElems = ctx.types.typedElem
|
|
451
647
|
// Reset per-function state for closure body
|
|
452
648
|
ctx.func.locals = new Map()
|
|
453
|
-
ctx.func.
|
|
649
|
+
ctx.func.localReps = null
|
|
454
650
|
if (cb.intConsts) for (const [name, v] of cb.intConsts) updateRep(name, { intConst: v })
|
|
651
|
+
if (cb.intCertain) for (const name of cb.intCertain) updateRep(name, { intCertain: true })
|
|
455
652
|
if (cb.valTypes) for (const [name, vt] of cb.valTypes) updateRep(name, { val: vt })
|
|
456
653
|
if (cb.schemaVars) {
|
|
457
654
|
ctx.schema.vars = new Map([...prevSchemaVars, ...cb.schemaVars])
|
|
@@ -469,18 +666,24 @@ function emitClosureBody(cb) {
|
|
|
469
666
|
ctx.func.boxed = cb.boxed ? new Map([...cb.boxed].map(v => [v, v])) : new Map()
|
|
470
667
|
const parentBoxedCaptures = new Set(cb.boxed || [])
|
|
471
668
|
ctx.func.preboxed = new Set()
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
// them so calls to a captured `peek` lower to `call $closureN` instead of call_indirect.
|
|
478
|
-
ctx.func.directClosures = cb.directClosures ? new Map(cb.directClosures) : null
|
|
669
|
+
// Bare `;`-sequence bodies (no enclosing `{}`) reach us when callers built a
|
|
670
|
+
// statement list directly — wrap into a block body so the multi-stmt path
|
|
671
|
+
// runs (otherwise emit returns an untyped list and asF64 wraps it with
|
|
672
|
+
// `f64.convert_i32_s`, yielding invalid WAT).
|
|
673
|
+
if (Array.isArray(cb.body) && cb.body[0] === ';') cb.body = ['{}', cb.body]
|
|
479
674
|
// Uniform convention: (env f64, argc i32, a0..a{width-1} f64) → f64
|
|
480
675
|
const W = ctx.closure.width ?? MAX_CLOSURE_ARITY
|
|
481
676
|
const paramDecls = [{ name: '__env', type: 'f64' }, { name: '__argc', type: 'i32' }]
|
|
482
677
|
for (let i = 0; i < W; i++) paramDecls.push({ name: `__a${i}`, type: 'f64' })
|
|
483
|
-
|
|
678
|
+
// Enter the closure frame. uniq ≥ 100 keeps synthetic labels from colliding
|
|
679
|
+
// with the parent. directClosures: closure.make snapshotted the parent's
|
|
680
|
+
// direct-call map for each capture, so a call to a captured const closure
|
|
681
|
+
// still lowers to `call $closureN` instead of call_indirect (A3 across the
|
|
682
|
+
// capture boundary).
|
|
683
|
+
enterFunc({ params: paramDecls, results: ['f64'] }, cb.body, {
|
|
684
|
+
uniq: Math.max(ctx.func.uniq, 100),
|
|
685
|
+
directClosures: cb.directClosures ? new Map(cb.directClosures) : null,
|
|
686
|
+
})
|
|
484
687
|
|
|
485
688
|
const fn = ['func', `$${cb.name}`]
|
|
486
689
|
fn.push(['param', '$__env', 'f64'])
|
|
@@ -502,24 +705,16 @@ function emitClosureBody(cb) {
|
|
|
502
705
|
let bodyIR
|
|
503
706
|
if (block) {
|
|
504
707
|
invalidateLocalsCache(cb.body)
|
|
505
|
-
for (const [k, v] of
|
|
506
|
-
// Usage-based
|
|
708
|
+
for (const [k, v] of analyzeBody(cb.body).locals) if (!ctx.func.locals.has(k)) ctx.func.locals.set(k, v)
|
|
709
|
+
// Usage-based shape inference for closure params not seeded by captureValTypes.
|
|
507
710
|
// (Captures already have their parent's val type via cb.valTypes above.)
|
|
508
|
-
|
|
509
|
-
const candidates = cb.params.filter(p => !ctx.func.repByLocal?.get(p)?.val)
|
|
510
|
-
if (candidates.length) {
|
|
511
|
-
const inferred = inferStringParams(cb.body, candidates)
|
|
512
|
-
for (const [n, vt] of inferred) updateRep(n, { val: vt })
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
analyzeValTypes(cb.body)
|
|
516
|
-
analyzeIntCertain(cb.body)
|
|
711
|
+
inferLocals(cb.body, cb.params.filter(p => !ctx.func.localReps?.get(p)?.val))
|
|
517
712
|
// Detect captures from deeper nested arrows that mutate this body's locals/params/captures
|
|
518
|
-
|
|
713
|
+
boxedCaptures(cb.body)
|
|
519
714
|
for (const name of ctx.func.boxed.keys()) {
|
|
520
715
|
if (parentBoxedCaptures.has(name) && ctx.func.locals.get(name) === 'f64') ctx.func.locals.set(name, 'i32')
|
|
521
716
|
}
|
|
522
|
-
const unbox =
|
|
717
|
+
const unbox = unboxablePtrs(cb.body, ctx.func.locals, ctx.func.boxed)
|
|
523
718
|
for (const [name, kind] of unbox) {
|
|
524
719
|
if (cb.params.includes(name) || cb.captures.includes(name)) continue
|
|
525
720
|
const fields = { ptrKind: kind }
|
|
@@ -561,17 +756,27 @@ function emitClosureBody(cb) {
|
|
|
561
756
|
ctx.func.locals.set(ctx.func.boxed.get(name), 'i32')
|
|
562
757
|
ctx.func.preboxed.add(name)
|
|
563
758
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
ctx.func.preboxed.add(name)
|
|
569
|
-
preboxedLocalInits.push(
|
|
570
|
-
['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]],
|
|
571
|
-
['f64.store', ['local.get', `$${cell}`], nullExpr()])
|
|
572
|
-
}
|
|
759
|
+
// Boxed locals that aren't captures or params get a fresh null-init cell;
|
|
760
|
+
// captures/params already carry their incoming value.
|
|
761
|
+
const preboxedLocalInits = emitPreboxedLocalInits(name =>
|
|
762
|
+
boxedCaptureNames.has(name) || boxedValueCaptureNames.has(name) || boxedParamNames.has(name))
|
|
573
763
|
|
|
574
764
|
// Insert locals (captures + params + declared)
|
|
765
|
+
// Build default-param initializer IR before local declarations are emitted:
|
|
766
|
+
// default expressions can allocate temporaries (for example `param = []`).
|
|
767
|
+
const defaultParamInits = []
|
|
768
|
+
if (cb.defaults) {
|
|
769
|
+
for (const [pname, defVal] of Object.entries(cb.defaults)) {
|
|
770
|
+
if (boxedParamNames.has(pname)) {
|
|
771
|
+
defaultParamInits.push(['if', isUndef(['f64.load', boxedAddr(pname)]),
|
|
772
|
+
['then', ['f64.store', boxedAddr(pname), asF64(emit(defVal))]]])
|
|
773
|
+
} else {
|
|
774
|
+
defaultParamInits.push(['if', isUndef(['local.get', `$${pname}`]),
|
|
775
|
+
['then', ['local.set', `$${pname}`, asF64(emit(defVal))]]])
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
575
780
|
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
576
781
|
|
|
577
782
|
// Load captures from env: boxed → i32.load (raw cell pointer), immutable → f64.load value.
|
|
@@ -644,17 +849,7 @@ function emitClosureBody(cb) {
|
|
|
644
849
|
|
|
645
850
|
// Default params for closures (check sentinel after unpack)
|
|
646
851
|
// Only `undefined` triggers default per spec — `null`/`0`/`false` pass through.
|
|
647
|
-
|
|
648
|
-
for (const [pname, defVal] of Object.entries(cb.defaults)) {
|
|
649
|
-
if (boxedParamNames.has(pname)) {
|
|
650
|
-
fn.push(['if', isUndef(['f64.load', boxedAddr(pname)]),
|
|
651
|
-
['then', ['f64.store', boxedAddr(pname), asF64(emit(defVal))]]])
|
|
652
|
-
} else {
|
|
653
|
-
fn.push(['if', isUndef(['local.get', `$${pname}`]),
|
|
654
|
-
['then', ['local.set', `$${pname}`, asF64(emit(defVal))]]])
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
852
|
+
fn.push(...defaultParamInits)
|
|
658
853
|
fn.push(...preboxedLocalInits)
|
|
659
854
|
fn.push(...bodyIR)
|
|
660
855
|
// I: Skip trailing fallback when last statement is return
|
|
@@ -700,7 +895,11 @@ export default function compile(ast, profiler) {
|
|
|
700
895
|
err(`'${name}' conflicts with a compiler internal — choose a different name`)
|
|
701
896
|
|
|
702
897
|
// Pre-fold const globals: evaluate constant initializers before function compilation
|
|
703
|
-
// so functions see the correct global types (i32 vs f64).
|
|
898
|
+
// so functions see the correct global types (i32 vs f64). Covers the main module
|
|
899
|
+
// and every bundled sub-module — a sub-module's top-level `const SPACE = 32` lands
|
|
900
|
+
// in `moduleInits` (emitted from __start), not `ast`, so without this it stays a
|
|
901
|
+
// `(mut f64)` global. Folding it makes the scanner's char-code constants immutable
|
|
902
|
+
// globals V8 constant-folds at each read site.
|
|
704
903
|
if (ast) {
|
|
705
904
|
const evalConst = n => {
|
|
706
905
|
if (typeof n === 'number') return n
|
|
@@ -719,8 +918,10 @@ export default function compile(ast, profiler) {
|
|
|
719
918
|
if (op === '>>') return va >> vb; if (op === '>>>') return va >>> vb
|
|
720
919
|
return null
|
|
721
920
|
}
|
|
722
|
-
const
|
|
723
|
-
: Array.isArray(
|
|
921
|
+
const topStmts = n => Array.isArray(n) && n[0] === ';' ? n.slice(1)
|
|
922
|
+
: Array.isArray(n) && n[0] === 'const' ? [n] : []
|
|
923
|
+
const stmts = [...topStmts(ast)]
|
|
924
|
+
for (const mi of ctx.module.moduleInits || []) stmts.push(...topStmts(mi))
|
|
724
925
|
for (const s of stmts) {
|
|
725
926
|
if (!Array.isArray(s) || s[0] !== 'const') continue
|
|
726
927
|
for (const decl of s.slice(1)) {
|
|
@@ -729,7 +930,7 @@ export default function compile(ast, profiler) {
|
|
|
729
930
|
if (!ctx.scope.globals.has(name) || !ctx.scope.consts?.has(name)) continue
|
|
730
931
|
const v = evalConst(init)
|
|
731
932
|
if (v == null || !isFinite(v)) continue
|
|
732
|
-
const isInt = Number.isInteger(v) && v >=
|
|
933
|
+
const isInt = Number.isInteger(v) && v >= I32_MIN && v <= I32_MAX
|
|
733
934
|
ctx.scope.globals.set(name, isInt
|
|
734
935
|
? `(global $${name} i32 (i32.const ${v}))`
|
|
735
936
|
: `(global $${name} f64 (f64.const ${v}))`)
|
|
@@ -743,8 +944,22 @@ export default function compile(ast, profiler) {
|
|
|
743
944
|
|
|
744
945
|
const programFacts = timePhase(profiler, 'plan', () => plan(ast))
|
|
745
946
|
|
|
947
|
+
// Inspect sink: editor hosts opt in via { inspect: true } to read inferred shapes.
|
|
948
|
+
// Initialized here (post-plan) so paramReps and schema.list are stable, populated
|
|
949
|
+
// per-function below as funcFacts settle. Bytes themselves are unchanged.
|
|
950
|
+
if (ctx.transform.inspect) ctx.inspect = { functions: {}, schemas: ctx.schema.list.map(s => s.slice()) }
|
|
951
|
+
|
|
746
952
|
const funcFacts = new Map()
|
|
747
|
-
for (const func of ctx.func.list)
|
|
953
|
+
for (const func of ctx.func.list) {
|
|
954
|
+
if (func.raw) continue
|
|
955
|
+
const facts = analyzeFuncForEmit(func, programFacts)
|
|
956
|
+
funcFacts.set(func, facts)
|
|
957
|
+
captureFuncInspect(func, facts, programFacts)
|
|
958
|
+
}
|
|
959
|
+
// Whole-program SRoA: pick the schemas whose `Array<S>` instances use the
|
|
960
|
+
// `structInline` carrier. Runs once the per-function reps have settled (they
|
|
961
|
+
// are codegen truth) and before any function is emitted.
|
|
962
|
+
analyzeStructInline(funcFacts, programFacts)
|
|
748
963
|
const funcs = ctx.func.list.map(func => emitFunc(func, funcFacts.get(func), programFacts))
|
|
749
964
|
funcs.push(...synthesizeBoundaryWrappers())
|
|
750
965
|
|
|
@@ -759,10 +974,28 @@ export default function compile(ast, profiler) {
|
|
|
759
974
|
}
|
|
760
975
|
compilePendingClosures()
|
|
761
976
|
|
|
977
|
+
// `wasm:js-string` imports — drained from `ctx.core.jsstring`, one
|
|
978
|
+
// `(import …)` per builtin referenced by emitted code. Engines with
|
|
979
|
+
// js-string-builtins support intercept the namespace; engines without
|
|
980
|
+
// fall back to JS-side polyfills wired in interop.js. The import nodes
|
|
981
|
+
// precede user imports so the host providing them sees them first.
|
|
982
|
+
const jssImports = []
|
|
983
|
+
if (ctx.core.jsstring?.size) {
|
|
984
|
+
for (const name of ctx.core.jsstring) {
|
|
985
|
+
const sig = JSS_IMPORT_SIGS[name]
|
|
986
|
+
if (!sig) continue // unknown builtin — silently skip (defensive)
|
|
987
|
+
const funcNode = ['func', `$__jss_${name}`,
|
|
988
|
+
...sig.params.map(t => ['param', t]),
|
|
989
|
+
['result', sig.result],
|
|
990
|
+
]
|
|
991
|
+
jssImports.push(['import', '"wasm:js-string"', `"${name}"`, funcNode])
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
762
995
|
// Build module sections — named slots, assembled at the end (no index bookkeeping)
|
|
763
996
|
const sec = {
|
|
764
997
|
extStdlib: [], // external stdlib (imports that must precede all other imports)
|
|
765
|
-
imports: [...ctx.module.imports],
|
|
998
|
+
imports: [...jssImports, ...ctx.module.imports],
|
|
766
999
|
types: [], // function types for call_indirect
|
|
767
1000
|
memory: [], // memory declaration
|
|
768
1001
|
data: [], // data segment (filled after emit)
|
|
@@ -834,10 +1067,10 @@ export default function compile(ast, profiler) {
|
|
|
834
1067
|
|
|
835
1068
|
stripStaticDataPrefix(sec)
|
|
836
1069
|
|
|
837
|
-
optimizeModule(sec)
|
|
838
|
-
|
|
839
1070
|
ensureThrowRuntime(sec)
|
|
840
1071
|
|
|
1072
|
+
optimizeModule(sec)
|
|
1073
|
+
|
|
841
1074
|
// Populate globals (after __start — const folding may update declarations)
|
|
842
1075
|
sec.globals.push(...[...ctx.scope.globals.values()].filter(g => g).map(g => parseWat(g)))
|
|
843
1076
|
|
|
@@ -875,9 +1108,19 @@ export default function compile(ast, profiler) {
|
|
|
875
1108
|
sec.customs.push(['@custom', '"jz:schema"', bytes])
|
|
876
1109
|
}
|
|
877
1110
|
|
|
878
|
-
// Custom section: rest params for exported functions (JS-side wrapping)
|
|
879
|
-
|
|
880
|
-
|
|
1111
|
+
// Custom section: rest params for exported functions (JS-side wrapping).
|
|
1112
|
+
// Entry per JS-visible export name (not per internal func name) — host's
|
|
1113
|
+
// interop.js wrap() keys by export name. Aliased re-export
|
|
1114
|
+
// (`function foo (...rest); export { foo as bar }`) needs `bar` in the
|
|
1115
|
+
// list; otherwise JS pads the missing args with UNDEF_NAN and the
|
|
1116
|
+
// VAL.ARRAY narrow path reads i32 at `__ptr_offset(UNDEF_NAN) - 8`, hitting
|
|
1117
|
+
// OOB instead of the polymorphic length-check fallback's tag-aware return-0.
|
|
1118
|
+
const restParamFuncs = []
|
|
1119
|
+
for (const f of ctx.func.list) {
|
|
1120
|
+
if (!isExported(f) || !f.rest) continue
|
|
1121
|
+
const fixed = f.sig.params.length - 1
|
|
1122
|
+
for (const exportName of exportNamesOf(f.name)) restParamFuncs.push({ name: exportName, fixed })
|
|
1123
|
+
}
|
|
881
1124
|
if (restParamFuncs.length)
|
|
882
1125
|
sec.customs.push(['@custom', '"jz:rest"', `"${JSON.stringify(restParamFuncs).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
883
1126
|
|
|
@@ -889,20 +1132,42 @@ export default function compile(ast, profiler) {
|
|
|
889
1132
|
// stay as Numbers on the JS side.
|
|
890
1133
|
const i64Exports = []
|
|
891
1134
|
for (const f of ctx.func.list) {
|
|
892
|
-
if (!f
|
|
1135
|
+
if (!isExported(f) || !isBoundaryWrapped(f) || !f._exportUsesI64) continue
|
|
893
1136
|
const p = []
|
|
894
1137
|
f._exportI64Sig.params.forEach((b, i) => { if (b) p.push(i) })
|
|
895
1138
|
const r = f._exportI64Sig.result ? 1 : 0
|
|
896
|
-
|
|
897
|
-
//
|
|
898
|
-
//
|
|
899
|
-
for (const
|
|
900
|
-
if (val === f.name && alias !== f.name) i64Exports.push({ name: alias, p, r })
|
|
901
|
-
}
|
|
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 })
|
|
902
1143
|
}
|
|
903
1144
|
if (i64Exports.length)
|
|
904
1145
|
sec.customs.push(['@custom', '"jz:i64exp"', `"${JSON.stringify(i64Exports).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
905
1146
|
|
|
1147
|
+
// Custom section: per-export externref param positions. interop.js reads
|
|
1148
|
+
// this to pass JS arguments straight through at those positions (no
|
|
1149
|
+
// `mem.wrapVal`, no SSO encoding). Format: { name, p, d? } where p lists
|
|
1150
|
+
// 0-based externref param indices and d (optional) is a map idx→default
|
|
1151
|
+
// string for jsstring-carrier params whose default-substitution happens
|
|
1152
|
+
// JS-side. Empty list emits nothing.
|
|
1153
|
+
const extExports = []
|
|
1154
|
+
for (const f of ctx.func.list) {
|
|
1155
|
+
if (!isExported(f) || !isBoundaryWrapped(f) || !f._exportExtParams) continue
|
|
1156
|
+
const p = []
|
|
1157
|
+
const d = {}
|
|
1158
|
+
f._exportExtParams.forEach((b, i) => {
|
|
1159
|
+
if (!b) return
|
|
1160
|
+
p.push(i)
|
|
1161
|
+
if (typeof b === 'object' && b.def != null) d[i] = b.def
|
|
1162
|
+
})
|
|
1163
|
+
if (!p.length) continue
|
|
1164
|
+
const entry = { name: '', p }
|
|
1165
|
+
if (Object.keys(d).length) entry.d = d
|
|
1166
|
+
for (const exportName of exportNamesOf(f.name)) extExports.push({ ...entry, name: exportName })
|
|
1167
|
+
}
|
|
1168
|
+
if (extExports.length)
|
|
1169
|
+
sec.customs.push(['@custom', '"jz:extparam"', `"${JSON.stringify(extExports).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
1170
|
+
|
|
906
1171
|
// Named export aliases: export { name } or export { source as alias }
|
|
907
1172
|
for (const [name, val] of Object.entries(ctx.func.exports)) {
|
|
908
1173
|
if (wasiCommandExports.has(name)) continue
|
|
@@ -926,10 +1191,12 @@ export default function compile(ast, profiler) {
|
|
|
926
1191
|
const optCfg = ctx.transform.optimize
|
|
927
1192
|
const { callCount } = treeshake(
|
|
928
1193
|
[{ arr: sec.stdlib }, { arr: sec.funcs }, { arr: sec.start }],
|
|
929
|
-
[...sec.start, ...sec.elem, ...sec.customs, ...sec.extStdlib, ...sec.imports],
|
|
1194
|
+
[...sec.start, ...sec.elem, ...sec.customs, ...sec.extStdlib, ...sec.imports, ...sec.tags],
|
|
930
1195
|
{ removeDead: !optCfg || optCfg.treeshake !== false, globals: sec.globals }
|
|
931
1196
|
)
|
|
932
1197
|
|
|
1198
|
+
pruneUnusedThrowRuntime(sec)
|
|
1199
|
+
|
|
933
1200
|
// Reorder non-import funcs by call count: hot callees get low LEB128 indices.
|
|
934
1201
|
// `call $f` encodes funcidx as ULEB128 (1 B for idx < 128, 2 B for idx < 16384).
|
|
935
1202
|
// On watr self-host this saves ~6 KB (hot specialized helpers migrate to idx < 128).
|