jz 0.3.1 → 0.5.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 +277 -258
- package/cli.js +10 -52
- package/index.js +181 -29
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +195 -76
- package/module/collection.js +249 -20
- package/module/console.js +1 -1
- package/module/core.js +267 -119
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +367 -61
- package/module/math.js +568 -128
- package/module/number.js +390 -227
- package/module/object.js +352 -39
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +555 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +12 -6
- 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 +1872 -487
- package/src/assemble.js +58 -20
- package/src/autoload.js +13 -1
- package/src/codegen.js +177 -0
- package/src/compile.js +462 -186
- package/src/ctx.js +85 -18
- package/src/emit.js +668 -197
- package/src/infer.js +548 -0
- package/src/ir.js +302 -27
- package/src/jzify.js +898 -259
- package/src/narrow.js +455 -246
- package/src/optimize.js +263 -99
- package/src/plan.js +713 -35
- package/src/prepare.js +818 -293
- 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/compile.js
CHANGED
|
@@ -28,12 +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
|
-
|
|
31
|
+
T, VAL, analyzeBody,
|
|
32
|
+
unboxablePtrs, cseSafeLoadBases, typedElemAux, invalidateLocalsCache,
|
|
33
|
+
boxedCaptures, updateRep,
|
|
34
|
+
isBlockBody, analyzeStructInline,
|
|
34
35
|
} from './analyze.js'
|
|
36
|
+
import { inferLocals } from './infer.js'
|
|
35
37
|
import { optimizeFunc, treeshake } from './optimize.js'
|
|
36
38
|
import { emit, emitter, emitFlat, emitBody } from './emit.js'
|
|
39
|
+
import { emitCharDecompPrologue, JSS_IMPORT_SIGS } from './abi/string.js'
|
|
37
40
|
import {
|
|
38
41
|
typed, asF64, asI32, asPtrOffset, asParamType, toI32, asI64, fromI64,
|
|
39
42
|
NULL_NAN, UNDEF_NAN, NULL_WAT, UNDEF_WAT, NULL_IR, UNDEF_IR, nullExpr, undefExpr,
|
|
@@ -44,7 +47,9 @@ import {
|
|
|
44
47
|
isGlobal, isConst, boxedAddr, readVar, writeVar, isNullish, isUndef,
|
|
45
48
|
slotAddr, elemLoad, elemStore, arrayLoop, allocPtr,
|
|
46
49
|
multiCount, loopTop, flat, reconstructArgsWithSpreads,
|
|
47
|
-
valKindToPtr, findBodyStart,
|
|
50
|
+
valKindToPtr, findBodyStart, tcoTailRewrite,
|
|
51
|
+
boolBoxIR,
|
|
52
|
+
I32_MIN, I32_MAX,
|
|
48
53
|
} from './ir.js'
|
|
49
54
|
import plan from './plan.js'
|
|
50
55
|
import {
|
|
@@ -52,6 +57,53 @@ import {
|
|
|
52
57
|
pullStdlib, syncImports, optimizeModule, stripStaticDataPrefix,
|
|
53
58
|
} from './assemble.js'
|
|
54
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
|
+
|
|
55
107
|
const timePhase = (profiler, name, fn) => profiler ? profiler.time(name, fn) : fn()
|
|
56
108
|
|
|
57
109
|
// Per-compile func name set + map live on ctx.func.names / ctx.func.map,
|
|
@@ -62,7 +114,7 @@ const timePhase = (profiler, name, fn) => profiler ? profiler.time(name, fn) : f
|
|
|
62
114
|
// buildArrayWithSpreads) moved to src/emit.js.
|
|
63
115
|
|
|
64
116
|
// AST-analysis primitives (staticObjectProps, paramReps lattice helpers,
|
|
65
|
-
//
|
|
117
|
+
// infer* cross-call inference, collectProgramFacts) moved to src/analyze.js.
|
|
66
118
|
|
|
67
119
|
/**
|
|
68
120
|
* Boundary-wrap predicate: exports whose body-driven result OR any param narrowed
|
|
@@ -76,79 +128,136 @@ const timePhase = (profiler, name, fn) => profiler ? profiler.time(name, fn) : f
|
|
|
76
128
|
* fractional Number gets the same truncation it would get from `arr[n]`).
|
|
77
129
|
*/
|
|
78
130
|
const isBoundaryWrapped = (func) => {
|
|
79
|
-
if (!func
|
|
131
|
+
if (!isExported(func) || func.raw || func.sig.results.length !== 1) return false
|
|
80
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
|
|
81
136
|
return func.sig.params.some(p => p.type !== 'f64' || p.ptrKind != null)
|
|
82
137
|
}
|
|
83
138
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (Array.isArray(arm) && (arm[0] === 'then' || arm[0] === 'else') && arm.length > 1) {
|
|
123
|
-
const last = arm[arm.length - 1]
|
|
124
|
-
const rewritten = tcoTailRewrite(last, resultType)
|
|
125
|
-
if (rewritten !== last) {
|
|
126
|
-
newIr[i] = [...arm.slice(0, -1), rewritten]
|
|
127
|
-
changed = true
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return changed ? typed(newIr, ir.type) : ir
|
|
132
|
-
}
|
|
133
|
-
if (op === 'block' && ir.length > 1) {
|
|
134
|
-
const last = ir[ir.length - 1]
|
|
135
|
-
const rewritten = tcoTailRewrite(last, resultType)
|
|
136
|
-
if (rewritten !== last) return typed([...ir.slice(0, -1), rewritten], ir.type)
|
|
137
|
-
}
|
|
138
|
-
return ir
|
|
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
|
|
147
|
+
if (!ctx.runtime.throws) return
|
|
148
|
+
|
|
149
|
+
if (!ctx.scope.globals.has('__jz_last_err_bits'))
|
|
150
|
+
ctx.scope.globals.set('__jz_last_err_bits', '(global $__jz_last_err_bits (mut i64) (i64.const 0))')
|
|
151
|
+
if (!sec.tags.some(t => Array.isArray(t) && t[0] === 'tag' && t[1] === '$__jz_err'))
|
|
152
|
+
sec.tags.push(['tag', '$__jz_err', ['param', 'f64']])
|
|
153
|
+
if (!sec.tags.some(t => Array.isArray(t) && t[0] === 'export' && t[1] === '"__jz_last_err_bits"'))
|
|
154
|
+
sec.tags.push(['export', '"__jz_last_err_bits"', ['global', '$__jz_last_err_bits']])
|
|
155
|
+
}
|
|
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')
|
|
139
177
|
}
|
|
140
178
|
|
|
141
179
|
// === Module compilation ===
|
|
142
180
|
|
|
143
181
|
const cloneRepMap = map => map ? new Map([...map].map(([k, v]) => [k, { ...v }])) : null
|
|
144
182
|
|
|
145
|
-
|
|
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 } = {}) {
|
|
146
238
|
ctx.func.stack = []
|
|
147
|
-
ctx.func.uniq =
|
|
148
|
-
ctx.func.current =
|
|
149
|
-
ctx.func.body =
|
|
150
|
-
ctx.func.directClosures =
|
|
239
|
+
ctx.func.uniq = uniq
|
|
240
|
+
ctx.func.current = sig
|
|
241
|
+
ctx.func.body = body
|
|
242
|
+
ctx.func.directClosures = directClosures
|
|
151
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
|
|
152
261
|
}
|
|
153
262
|
|
|
154
263
|
function analyzeFuncForEmit(func, programFacts) {
|
|
@@ -156,11 +265,11 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
156
265
|
if (func.raw) return null
|
|
157
266
|
|
|
158
267
|
const { name, body, sig } = func
|
|
159
|
-
enterFunc(
|
|
268
|
+
enterFunc(sig, body)
|
|
160
269
|
|
|
161
|
-
const block =
|
|
270
|
+
const block = isBlockBody(body)
|
|
162
271
|
ctx.func.boxed = new Map()
|
|
163
|
-
ctx.func.
|
|
272
|
+
ctx.func.localReps = null
|
|
164
273
|
ctx.types.typedElem = ctx.scope.globalTypedElem ? new Map(ctx.scope.globalTypedElem) : null
|
|
165
274
|
|
|
166
275
|
const _reps = paramReps.get(name)
|
|
@@ -173,41 +282,52 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
173
282
|
if (!ctx.types.typedElem.has(pname)) ctx.types.typedElem.set(pname, r.typedCtor)
|
|
174
283
|
updateRep(pname, { val: VAL.TYPED })
|
|
175
284
|
}
|
|
176
|
-
if (r.val && !ctx.func.
|
|
285
|
+
if (r.val && !ctx.func.localReps?.get(pname)?.val) updateRep(pname, { val: r.val })
|
|
177
286
|
if (r.arrayElemSchema != null) updateRep(pname, { arrayElemSchema: r.arrayElemSchema })
|
|
178
287
|
if (r.arrayElemValType != null) updateRep(pname, { arrayElemValType: r.arrayElemValType })
|
|
179
288
|
if (r.intConst != null) updateRep(pname, { intConst: r.intConst })
|
|
180
289
|
}
|
|
181
290
|
}
|
|
182
|
-
// Drop any earlier-cached
|
|
183
|
-
// it before our pre-seed, when params still had no
|
|
184
|
-
// 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.
|
|
185
295
|
invalidateLocalsCache(body)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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)
|
|
200
322
|
if (block) {
|
|
201
|
-
|
|
202
|
-
analyzeIntCertain(body)
|
|
203
|
-
analyzeBoxedCaptures(body)
|
|
323
|
+
boxedCaptures(body)
|
|
204
324
|
// Lower provably-monomorphic pointer locals to i32 offset storage.
|
|
205
325
|
// VAL.TYPED unbox requires a known element ctor (aux byte) — without it,
|
|
206
326
|
// the use site can't pick the right i32.store{8,16}/i32.store width and
|
|
207
327
|
// the rebox path can't reconstruct the NaN-box. Heterogeneous decls (two
|
|
208
328
|
// `let arr = ...` with different ctors, or a multi-ctor ternary) leave
|
|
209
329
|
// typedElem unset; skip unbox so reads/writes go through `__typed_set_idx`.
|
|
210
|
-
const unbox =
|
|
330
|
+
const unbox = unboxablePtrs(body, ctx.func.locals, ctx.func.boxed)
|
|
211
331
|
if (unbox.size > 0) {
|
|
212
332
|
for (const [n, kind] of unbox) {
|
|
213
333
|
const fields = { ptrKind: kind }
|
|
@@ -222,7 +342,7 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
222
342
|
}
|
|
223
343
|
}
|
|
224
344
|
// Pointer-ABI params (from narrowing loop above): params already have type='i32' and
|
|
225
|
-
// ptrKind set. Register them in ctx.func.
|
|
345
|
+
// ptrKind set. Register them in ctx.func.localReps so readVar tags local.gets correctly.
|
|
226
346
|
// Boxed capture still works: the boxed-init path (below) uses a ptrKind-tagged local.get
|
|
227
347
|
// so asF64 reboxes to NaN-form before f64.store to the cell.
|
|
228
348
|
for (const p of sig.params) {
|
|
@@ -232,12 +352,21 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
232
352
|
updateRep(p.name, fields)
|
|
233
353
|
}
|
|
234
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
|
+
|
|
235
361
|
return {
|
|
236
362
|
block,
|
|
237
363
|
locals: new Map(ctx.func.locals),
|
|
238
364
|
boxed: new Map(ctx.func.boxed),
|
|
365
|
+
flatObjects: new Map(ctx.func.flatObjects),
|
|
366
|
+
sliceViews: new Set(ctx.func.sliceViews),
|
|
367
|
+
cseLoadBases,
|
|
239
368
|
typedElem: ctx.types.typedElem ? new Map(ctx.types.typedElem) : null,
|
|
240
|
-
|
|
369
|
+
localReps: cloneRepMap(ctx.func.localReps),
|
|
241
370
|
}
|
|
242
371
|
}
|
|
243
372
|
|
|
@@ -257,11 +386,13 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
257
386
|
const multi = sig.results.length > 1
|
|
258
387
|
const _reps = paramReps.get(name)
|
|
259
388
|
|
|
260
|
-
enterFunc(
|
|
389
|
+
enterFunc(sig, body)
|
|
261
390
|
const block = funcFacts.block
|
|
262
391
|
ctx.func.locals = new Map(funcFacts.locals)
|
|
263
392
|
ctx.func.boxed = new Map(funcFacts.boxed)
|
|
264
|
-
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)
|
|
265
396
|
ctx.types.typedElem = funcFacts.typedElem ? new Map(funcFacts.typedElem) : null
|
|
266
397
|
|
|
267
398
|
// D: Apply call-site param facts (only if body analysis didn't already set them).
|
|
@@ -272,11 +403,11 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
272
403
|
for (const [k, r] of _reps) {
|
|
273
404
|
if (k >= sig.params.length) continue
|
|
274
405
|
const pname = sig.params[k].name
|
|
275
|
-
if (r.val && !ctx.func.
|
|
406
|
+
if (r.val && !ctx.func.localReps?.get(pname)?.val) updateRep(pname, { val: r.val })
|
|
276
407
|
if (r.typedCtor) {
|
|
277
408
|
if (!ctx.types.typedElem) ctx.types.typedElem = new Map()
|
|
278
409
|
if (!ctx.types.typedElem.has(pname)) ctx.types.typedElem.set(pname, r.typedCtor)
|
|
279
|
-
if (!ctx.func.
|
|
410
|
+
if (!ctx.func.localReps?.get(pname)?.val) updateRep(pname, { val: VAL.TYPED })
|
|
280
411
|
}
|
|
281
412
|
if (r.schemaId != null && !exported && !ctx.schema.vars.has(pname)) {
|
|
282
413
|
ctx.schema.vars.set(pname, r.schemaId)
|
|
@@ -286,7 +417,18 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
286
417
|
}
|
|
287
418
|
|
|
288
419
|
const fn = ['func', `$${name}`]
|
|
289
|
-
//
|
|
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
|
|
290
432
|
// wrapper ($${name}$exp) that reboxes the narrowed result back to f64.
|
|
291
433
|
if (exported && !isBoundaryWrapped(func)) fn.push(['export', `"${name}"`])
|
|
292
434
|
fn.push(...sig.params.map(p => ['param', `$${p.name}`, p.type]))
|
|
@@ -294,19 +436,26 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
294
436
|
|
|
295
437
|
// Default params: ES spec says default applies only when arg is `undefined`
|
|
296
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.
|
|
297
442
|
const defaults = func.defaults || {}
|
|
298
|
-
const defaultInits =
|
|
443
|
+
const defaultInits = new Map()
|
|
299
444
|
for (const [pname, defVal] of Object.entries(defaults)) {
|
|
300
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
|
|
301
451
|
const t = p?.type || 'f64'
|
|
302
|
-
defaultInits.
|
|
452
|
+
defaultInits.set(pname,
|
|
303
453
|
['if', isUndef(typed(['local.get', `$${pname}`], 'f64')),
|
|
304
454
|
['then', ['local.set', `$${pname}`, t === 'f64' ? asF64(emit(defVal)) : asI32(emit(defVal))]]])
|
|
305
455
|
}
|
|
306
456
|
|
|
307
457
|
// Box params that are mutably captured: allocate cell, copy param value
|
|
308
458
|
const boxedParamInits = []
|
|
309
|
-
const preboxedLocalInits = []
|
|
310
459
|
ctx.func.preboxed = new Set()
|
|
311
460
|
const paramNames = new Set(sig.params.map(p => p.name))
|
|
312
461
|
for (const p of sig.params) {
|
|
@@ -321,31 +470,58 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
321
470
|
['f64.store', ['local.get', `$${cell}`], asF64(lget)])
|
|
322
471
|
}
|
|
323
472
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
331
497
|
}
|
|
332
498
|
|
|
333
499
|
if (block) {
|
|
334
500
|
const stmts = emitBody(body)
|
|
501
|
+
const paramInits = collectParamInits()
|
|
335
502
|
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
336
503
|
// I: Skip trailing fallback when last statement is return (unreachable code)
|
|
337
504
|
const lastStmt = stmts.at(-1)
|
|
338
505
|
const endsWithReturn = lastStmt && (lastStmt[0] === 'return' || lastStmt[0] === 'return_call')
|
|
339
|
-
|
|
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)
|
|
340
514
|
} else if (multi && body[0] === '[') {
|
|
341
515
|
const values = body.slice(1).map(e => asF64(emit(e)))
|
|
516
|
+
const paramInits = collectParamInits()
|
|
342
517
|
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
343
|
-
fn.push(...boxedParamInits, ...preboxedLocalInits, ...values)
|
|
518
|
+
fn.push(...paramInits, ...boxedParamInits, ...preboxedLocalInits, ...values)
|
|
344
519
|
} else {
|
|
345
520
|
const ir = emit(body)
|
|
521
|
+
const paramInits = collectParamInits()
|
|
346
522
|
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
347
523
|
const finalIR = sig.ptrKind != null ? asPtrOffset(ir, sig.ptrKind) : asParamType(ir, sig.results[0])
|
|
348
|
-
fn.push(...
|
|
524
|
+
fn.push(...paramInits, ...boxedParamInits, ...preboxedLocalInits, tcoTailRewrite(finalIR, sig.results[0]))
|
|
349
525
|
}
|
|
350
526
|
|
|
351
527
|
// Restore schema.vars so param bindings don't leak to next function.
|
|
@@ -361,7 +537,7 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
361
537
|
* - takes i64 params always — JS-side carrier is BigInt that reinterprets to
|
|
362
538
|
* f64 NaN-box bits. i64 dodges V8's spec-permitted NaN canonicalization at
|
|
363
539
|
* the wasm↔JS boundary (see ToJSValue / ToWebAssemblyValue). Host wrap()
|
|
364
|
-
* in
|
|
540
|
+
* in interop.js pairs by converting BigInt↔f64 via reinterpret bits.
|
|
365
541
|
* - converts each narrowed param at the call: f64 → i32 (truncate-sat) for
|
|
366
542
|
* numeric narrowed, f64 → i32-offset (`i32.wrap_i64 + i64.reinterpret_f64`)
|
|
367
543
|
* for pointer narrowed. The reinterpret happens once at param decode and
|
|
@@ -375,7 +551,8 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
375
551
|
*
|
|
376
552
|
* Result rebox cases (then reinterpret to i64 at the boundary):
|
|
377
553
|
* - sig.ptrKind != null → mkPtrIR(ptrKind, ptrAux ?? 0, callIR)
|
|
378
|
-
* - sig.results[0] = i32 → f64.convert_i32_s(callIR)
|
|
554
|
+
* - sig.results[0] = i32 → f64.convert_i32_s(callIR), or `_u` when
|
|
555
|
+
* sig.unsignedResult (preserves `(x >>> 0)` ∈ [0, 2³²))
|
|
379
556
|
* - sig.results[0] = f64 → callIR (some params narrowed but result stayed f64)
|
|
380
557
|
*/
|
|
381
558
|
function synthesizeBoundaryWrappers() {
|
|
@@ -387,13 +564,31 @@ function synthesizeBoundaryWrappers() {
|
|
|
387
564
|
// actually crosses the boundary (param.ptrKind set, or result with
|
|
388
565
|
// sig.ptrKind set). Numeric narrowing (i32 trunc-sat / convert) keeps f64
|
|
389
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.
|
|
390
568
|
const paramI64 = sig.params.map(p => p.ptrKind != null)
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
+
})
|
|
394
587
|
wrapNode.push(['result', resultI64 ? 'i64' : 'f64'])
|
|
395
588
|
const args = sig.params.map((p, i) => {
|
|
396
589
|
const get = ['local.get', `$${p.name}`]
|
|
590
|
+
// jsstring: externref flows through unchanged — inner func also takes externref.
|
|
591
|
+
if (p.jsstring) return get
|
|
397
592
|
if (p.ptrKind != null) {
|
|
398
593
|
// ptrKind: i64 carrier carries NaN-box bits → wrap to i32 offset
|
|
399
594
|
return ['i32.wrap_i64', get]
|
|
@@ -407,14 +602,28 @@ function synthesizeBoundaryWrappers() {
|
|
|
407
602
|
if (sig.ptrKind != null) {
|
|
408
603
|
const ptrType = valKindToPtr(sig.ptrKind)
|
|
409
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
|
|
410
609
|
} else if (sig.results[0] === 'i32') {
|
|
411
|
-
body = ['f64.convert_i32_s', callIR]
|
|
610
|
+
body = [sig.unsignedResult ? 'f64.convert_i32_u' : 'f64.convert_i32_s', callIR]
|
|
412
611
|
} else {
|
|
413
612
|
body = callIR
|
|
414
613
|
}
|
|
415
614
|
wrapNode.push(resultI64 ? ['i64.reinterpret_f64', body] : body)
|
|
416
615
|
func._exportUsesI64 = resultI64 || paramI64.some(Boolean)
|
|
417
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
|
|
418
627
|
wrappers.push(wrapNode)
|
|
419
628
|
}
|
|
420
629
|
return wrappers
|
|
@@ -428,7 +637,7 @@ function synthesizeBoundaryWrappers() {
|
|
|
428
637
|
* so any closure can be invoked via call_indirect on $ftN. This function
|
|
429
638
|
* builds one body fn given the body record (cb) created by ctx.closure.make.
|
|
430
639
|
*
|
|
431
|
-
* Mutates ctx.func.* per-body state (locals, boxed,
|
|
640
|
+
* Mutates ctx.func.* per-body state (locals, boxed, localReps) and
|
|
432
641
|
* ctx.schema.vars / ctx.types.typedElem (restored on exit so capture-binding
|
|
433
642
|
* leaks don't poison the next body). Returns the WAT IR for the func node.
|
|
434
643
|
*/
|
|
@@ -437,8 +646,9 @@ function emitClosureBody(cb) {
|
|
|
437
646
|
const prevTypedElems = ctx.types.typedElem
|
|
438
647
|
// Reset per-function state for closure body
|
|
439
648
|
ctx.func.locals = new Map()
|
|
440
|
-
ctx.func.
|
|
649
|
+
ctx.func.localReps = null
|
|
441
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 })
|
|
442
652
|
if (cb.valTypes) for (const [name, vt] of cb.valTypes) updateRep(name, { val: vt })
|
|
443
653
|
if (cb.schemaVars) {
|
|
444
654
|
ctx.schema.vars = new Map([...prevSchemaVars, ...cb.schemaVars])
|
|
@@ -456,18 +666,24 @@ function emitClosureBody(cb) {
|
|
|
456
666
|
ctx.func.boxed = cb.boxed ? new Map([...cb.boxed].map(v => [v, v])) : new Map()
|
|
457
667
|
const parentBoxedCaptures = new Set(cb.boxed || [])
|
|
458
668
|
ctx.func.preboxed = new Set()
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
// them so calls to a captured `peek` lower to `call $closureN` instead of call_indirect.
|
|
465
|
-
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]
|
|
466
674
|
// Uniform convention: (env f64, argc i32, a0..a{width-1} f64) → f64
|
|
467
675
|
const W = ctx.closure.width ?? MAX_CLOSURE_ARITY
|
|
468
676
|
const paramDecls = [{ name: '__env', type: 'f64' }, { name: '__argc', type: 'i32' }]
|
|
469
677
|
for (let i = 0; i < W; i++) paramDecls.push({ name: `__a${i}`, type: 'f64' })
|
|
470
|
-
|
|
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
|
+
})
|
|
471
687
|
|
|
472
688
|
const fn = ['func', `$${cb.name}`]
|
|
473
689
|
fn.push(['param', '$__env', 'f64'])
|
|
@@ -485,28 +701,20 @@ function emitClosureBody(cb) {
|
|
|
485
701
|
}
|
|
486
702
|
|
|
487
703
|
// Emit body
|
|
488
|
-
const block =
|
|
704
|
+
const block = isBlockBody(cb.body)
|
|
489
705
|
let bodyIR
|
|
490
706
|
if (block) {
|
|
491
707
|
invalidateLocalsCache(cb.body)
|
|
492
|
-
for (const [k, v] of
|
|
493
|
-
// 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.
|
|
494
710
|
// (Captures already have their parent's val type via cb.valTypes above.)
|
|
495
|
-
|
|
496
|
-
const candidates = cb.params.filter(p => !ctx.func.repByLocal?.get(p)?.val)
|
|
497
|
-
if (candidates.length) {
|
|
498
|
-
const inferred = inferStringParams(cb.body, candidates)
|
|
499
|
-
for (const [n, vt] of inferred) updateRep(n, { val: vt })
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
analyzeValTypes(cb.body)
|
|
503
|
-
analyzeIntCertain(cb.body)
|
|
711
|
+
inferLocals(cb.body, cb.params.filter(p => !ctx.func.localReps?.get(p)?.val))
|
|
504
712
|
// Detect captures from deeper nested arrows that mutate this body's locals/params/captures
|
|
505
|
-
|
|
713
|
+
boxedCaptures(cb.body)
|
|
506
714
|
for (const name of ctx.func.boxed.keys()) {
|
|
507
715
|
if (parentBoxedCaptures.has(name) && ctx.func.locals.get(name) === 'f64') ctx.func.locals.set(name, 'i32')
|
|
508
716
|
}
|
|
509
|
-
const unbox =
|
|
717
|
+
const unbox = unboxablePtrs(cb.body, ctx.func.locals, ctx.func.boxed)
|
|
510
718
|
for (const [name, kind] of unbox) {
|
|
511
719
|
if (cb.params.includes(name) || cb.captures.includes(name)) continue
|
|
512
720
|
const fields = { ptrKind: kind }
|
|
@@ -548,17 +756,27 @@ function emitClosureBody(cb) {
|
|
|
548
756
|
ctx.func.locals.set(ctx.func.boxed.get(name), 'i32')
|
|
549
757
|
ctx.func.preboxed.add(name)
|
|
550
758
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
ctx.func.preboxed.add(name)
|
|
556
|
-
preboxedLocalInits.push(
|
|
557
|
-
['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]],
|
|
558
|
-
['f64.store', ['local.get', `$${cell}`], nullExpr()])
|
|
559
|
-
}
|
|
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))
|
|
560
763
|
|
|
561
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
|
+
|
|
562
780
|
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
563
781
|
|
|
564
782
|
// Load captures from env: boxed → i32.load (raw cell pointer), immutable → f64.load value.
|
|
@@ -612,7 +830,7 @@ function emitClosureBody(cb) {
|
|
|
612
830
|
['then', ['local.set', `$${restLen}`, ['i32.const', restSlots]]]])
|
|
613
831
|
fn.push(['local.set', `$${restOff}`,
|
|
614
832
|
['call', '$__alloc_hdr',
|
|
615
|
-
['local.get', `$${restLen}`], ['local.get', `$${restLen}`]
|
|
833
|
+
['local.get', `$${restLen}`], ['local.get', `$${restLen}`]]])
|
|
616
834
|
for (let i = 0; i < restSlots; i++) {
|
|
617
835
|
fn.push(['if', ['i32.gt_s', ['local.get', `$${restLen}`], ['i32.const', i]],
|
|
618
836
|
['then', ['f64.store',
|
|
@@ -631,17 +849,7 @@ function emitClosureBody(cb) {
|
|
|
631
849
|
|
|
632
850
|
// Default params for closures (check sentinel after unpack)
|
|
633
851
|
// Only `undefined` triggers default per spec — `null`/`0`/`false` pass through.
|
|
634
|
-
|
|
635
|
-
for (const [pname, defVal] of Object.entries(cb.defaults)) {
|
|
636
|
-
if (boxedParamNames.has(pname)) {
|
|
637
|
-
fn.push(['if', isUndef(['f64.load', boxedAddr(pname)]),
|
|
638
|
-
['then', ['f64.store', boxedAddr(pname), asF64(emit(defVal))]]])
|
|
639
|
-
} else {
|
|
640
|
-
fn.push(['if', isUndef(['local.get', `$${pname}`]),
|
|
641
|
-
['then', ['local.set', `$${pname}`, asF64(emit(defVal))]]])
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
852
|
+
fn.push(...defaultParamInits)
|
|
645
853
|
fn.push(...preboxedLocalInits)
|
|
646
854
|
fn.push(...bodyIR)
|
|
647
855
|
// I: Skip trailing fallback when last statement is return
|
|
@@ -687,7 +895,11 @@ export default function compile(ast, profiler) {
|
|
|
687
895
|
err(`'${name}' conflicts with a compiler internal — choose a different name`)
|
|
688
896
|
|
|
689
897
|
// Pre-fold const globals: evaluate constant initializers before function compilation
|
|
690
|
-
// 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.
|
|
691
903
|
if (ast) {
|
|
692
904
|
const evalConst = n => {
|
|
693
905
|
if (typeof n === 'number') return n
|
|
@@ -706,8 +918,10 @@ export default function compile(ast, profiler) {
|
|
|
706
918
|
if (op === '>>') return va >> vb; if (op === '>>>') return va >>> vb
|
|
707
919
|
return null
|
|
708
920
|
}
|
|
709
|
-
const
|
|
710
|
-
: 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))
|
|
711
925
|
for (const s of stmts) {
|
|
712
926
|
if (!Array.isArray(s) || s[0] !== 'const') continue
|
|
713
927
|
for (const decl of s.slice(1)) {
|
|
@@ -716,7 +930,7 @@ export default function compile(ast, profiler) {
|
|
|
716
930
|
if (!ctx.scope.globals.has(name) || !ctx.scope.consts?.has(name)) continue
|
|
717
931
|
const v = evalConst(init)
|
|
718
932
|
if (v == null || !isFinite(v)) continue
|
|
719
|
-
const isInt = Number.isInteger(v) && v >=
|
|
933
|
+
const isInt = Number.isInteger(v) && v >= I32_MIN && v <= I32_MAX
|
|
720
934
|
ctx.scope.globals.set(name, isInt
|
|
721
935
|
? `(global $${name} i32 (i32.const ${v}))`
|
|
722
936
|
: `(global $${name} f64 (f64.const ${v}))`)
|
|
@@ -730,8 +944,22 @@ export default function compile(ast, profiler) {
|
|
|
730
944
|
|
|
731
945
|
const programFacts = timePhase(profiler, 'plan', () => plan(ast))
|
|
732
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
|
+
|
|
733
952
|
const funcFacts = new Map()
|
|
734
|
-
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)
|
|
735
963
|
const funcs = ctx.func.list.map(func => emitFunc(func, funcFacts.get(func), programFacts))
|
|
736
964
|
funcs.push(...synthesizeBoundaryWrappers())
|
|
737
965
|
|
|
@@ -746,10 +974,28 @@ export default function compile(ast, profiler) {
|
|
|
746
974
|
}
|
|
747
975
|
compilePendingClosures()
|
|
748
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
|
+
|
|
749
995
|
// Build module sections — named slots, assembled at the end (no index bookkeeping)
|
|
750
996
|
const sec = {
|
|
751
997
|
extStdlib: [], // external stdlib (imports that must precede all other imports)
|
|
752
|
-
imports: [...ctx.module.imports],
|
|
998
|
+
imports: [...jssImports, ...ctx.module.imports],
|
|
753
999
|
types: [], // function types for call_indirect
|
|
754
1000
|
memory: [], // memory declaration
|
|
755
1001
|
data: [], // data segment (filled after emit)
|
|
@@ -775,12 +1021,6 @@ export default function compile(ast, profiler) {
|
|
|
775
1021
|
|
|
776
1022
|
// Memory section deferred — emitted after resolveIncludes() when __alloc is needed
|
|
777
1023
|
|
|
778
|
-
if (ctx.runtime.throws) {
|
|
779
|
-
ctx.scope.globals.set('__jz_last_err_bits', '(global $__jz_last_err_bits (mut i64) (i64.const 0))')
|
|
780
|
-
sec.tags.push(['tag', '$__jz_err', ['param', 'f64']])
|
|
781
|
-
sec.tags.push(['export', '"__jz_last_err_bits"', ['global', '$__jz_last_err_bits']])
|
|
782
|
-
}
|
|
783
|
-
|
|
784
1024
|
if (ctx.closure.table?.length)
|
|
785
1025
|
sec.table.push(['table', ['export', '"__jz_table"'], ctx.closure.table.length, 'funcref'])
|
|
786
1026
|
|
|
@@ -827,6 +1067,8 @@ export default function compile(ast, profiler) {
|
|
|
827
1067
|
|
|
828
1068
|
stripStaticDataPrefix(sec)
|
|
829
1069
|
|
|
1070
|
+
ensureThrowRuntime(sec)
|
|
1071
|
+
|
|
830
1072
|
optimizeModule(sec)
|
|
831
1073
|
|
|
832
1074
|
// Populate globals (after __start — const folding may update declarations)
|
|
@@ -866,9 +1108,19 @@ export default function compile(ast, profiler) {
|
|
|
866
1108
|
sec.customs.push(['@custom', '"jz:schema"', bytes])
|
|
867
1109
|
}
|
|
868
1110
|
|
|
869
|
-
// Custom section: rest params for exported functions (JS-side wrapping)
|
|
870
|
-
|
|
871
|
-
|
|
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
|
+
}
|
|
872
1124
|
if (restParamFuncs.length)
|
|
873
1125
|
sec.customs.push(['@custom', '"jz:rest"', `"${JSON.stringify(restParamFuncs).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
874
1126
|
|
|
@@ -880,20 +1132,42 @@ export default function compile(ast, profiler) {
|
|
|
880
1132
|
// stay as Numbers on the JS side.
|
|
881
1133
|
const i64Exports = []
|
|
882
1134
|
for (const f of ctx.func.list) {
|
|
883
|
-
if (!f
|
|
1135
|
+
if (!isExported(f) || !isBoundaryWrapped(f) || !f._exportUsesI64) continue
|
|
884
1136
|
const p = []
|
|
885
1137
|
f._exportI64Sig.params.forEach((b, i) => { if (b) p.push(i) })
|
|
886
1138
|
const r = f._exportI64Sig.result ? 1 : 0
|
|
887
|
-
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
for (const
|
|
891
|
-
if (val === f.name && alias !== f.name) i64Exports.push({ name: alias, p, r })
|
|
892
|
-
}
|
|
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 })
|
|
893
1143
|
}
|
|
894
1144
|
if (i64Exports.length)
|
|
895
1145
|
sec.customs.push(['@custom', '"jz:i64exp"', `"${JSON.stringify(i64Exports).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
896
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
|
+
|
|
897
1171
|
// Named export aliases: export { name } or export { source as alias }
|
|
898
1172
|
for (const [name, val] of Object.entries(ctx.func.exports)) {
|
|
899
1173
|
if (wasiCommandExports.has(name)) continue
|
|
@@ -917,10 +1191,12 @@ export default function compile(ast, profiler) {
|
|
|
917
1191
|
const optCfg = ctx.transform.optimize
|
|
918
1192
|
const { callCount } = treeshake(
|
|
919
1193
|
[{ arr: sec.stdlib }, { arr: sec.funcs }, { arr: sec.start }],
|
|
920
|
-
[...sec.start, ...sec.elem, ...sec.customs, ...sec.extStdlib, ...sec.imports],
|
|
921
|
-
{ removeDead: !optCfg || optCfg.treeshake !== false }
|
|
1194
|
+
[...sec.start, ...sec.elem, ...sec.customs, ...sec.extStdlib, ...sec.imports, ...sec.tags],
|
|
1195
|
+
{ removeDead: !optCfg || optCfg.treeshake !== false, globals: sec.globals }
|
|
922
1196
|
)
|
|
923
1197
|
|
|
1198
|
+
pruneUnusedThrowRuntime(sec)
|
|
1199
|
+
|
|
924
1200
|
// Reorder non-import funcs by call count: hot callees get low LEB128 indices.
|
|
925
1201
|
// `call $f` encodes funcidx as ULEB128 (1 B for idx < 128, 2 B for idx < 16384).
|
|
926
1202
|
// On watr self-host this saves ~6 KB (hot specialized helpers migrate to idx < 128).
|