jz 0.1.1 → 0.2.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 +257 -90
- package/cli.js +34 -8
- package/index.js +102 -13
- package/module/array.js +396 -133
- package/module/collection.js +455 -212
- package/module/console.js +169 -88
- package/module/core.js +246 -144
- package/module/date.js +691 -0
- package/module/function.js +81 -21
- package/module/index.js +2 -1
- package/module/json.js +483 -138
- package/module/math.js +79 -39
- package/module/number.js +188 -78
- package/module/object.js +227 -47
- package/module/regex.js +33 -30
- package/module/schema.js +14 -17
- package/module/string.js +434 -235
- package/module/symbol.js +4 -3
- package/module/timer.js +89 -31
- package/module/typedarray.js +174 -44
- package/package.json +3 -10
- package/src/analyze.js +626 -132
- package/src/autoload.js +14 -6
- package/src/compile.js +399 -70
- package/src/ctx.js +41 -12
- package/src/emit.js +634 -145
- package/src/host.js +244 -51
- package/src/ir.js +81 -31
- package/src/jzify.js +181 -24
- package/src/narrow.js +32 -4
- package/src/optimize.js +628 -42
- package/src/plan.js +1132 -4
- package/src/prepare.js +321 -75
- package/src/vectorize.js +1016 -0
- package/wasi.js +13 -0
- package/src/key.js +0 -73
- package/src/source.js +0 -76
package/src/compile.js
CHANGED
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { parse as parseWat } from 'watr'
|
|
29
|
-
import { ctx, err, inc, resolveIncludes, PTR } from './ctx.js'
|
|
29
|
+
import { ctx, err, inc, resolveIncludes, PTR, LAYOUT } from './ctx.js'
|
|
30
30
|
import {
|
|
31
31
|
T, VAL, analyzeValTypes, analyzeIntCertain, analyzeLocals,
|
|
32
32
|
analyzePtrUnboxable, typedElemAux, invalidateLocalsCache,
|
|
33
|
-
analyzeBoxedCaptures, updateRep,
|
|
33
|
+
analyzeBoxedCaptures, updateRep, inferStringParams,
|
|
34
34
|
} from './analyze.js'
|
|
35
|
-
import { optimizeFunc, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, treeshake } from './optimize.js'
|
|
35
|
+
import { optimizeFunc, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, treeshake, arenaRewindModule } from './optimize.js'
|
|
36
36
|
import { emit, emitter, emitFlat, emitBody } from './emit.js'
|
|
37
37
|
import {
|
|
38
38
|
typed, asF64, asI32, asPtrOffset, asParamType, toI32, asI64, fromI64,
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
isLit, litVal, isNullishLit, isPureIR, isPostfix, emitNum,
|
|
43
43
|
temp, tempI32, tempI64, f64rem, toNumF64, truthyIR, toBoolFromEmitted,
|
|
44
44
|
keyValType, usesDynProps, needsDynShadow,
|
|
45
|
-
isGlobal, isConst, boxedAddr, readVar, writeVar, isNullish,
|
|
45
|
+
isGlobal, isConst, boxedAddr, readVar, writeVar, isNullish, isUndef,
|
|
46
46
|
slotAddr, elemLoad, elemStore, arrayLoop, allocPtr,
|
|
47
47
|
multiCount, loopTop, flat, reconstructArgsWithSpreads,
|
|
48
48
|
valKindToPtr,
|
|
@@ -54,10 +54,15 @@ const timePhase = (profiler, name, fn) => profiler ? profiler.time(name, fn) : f
|
|
|
54
54
|
// Per-compile func name set + map live on ctx.func.names / ctx.func.map,
|
|
55
55
|
// populated at compile() entry. Both reset by ctx.js reset() and re-filled here.
|
|
56
56
|
|
|
57
|
-
// NaN-
|
|
58
|
-
// identify pointer slots in the data segment.
|
|
59
|
-
|
|
60
|
-
const
|
|
57
|
+
// NaN-prefix top-13-bits as BigInt — used by the static-prefix-strip pass
|
|
58
|
+
// below to identify pointer slots in the data segment.
|
|
59
|
+
const NAN_PREFIX = BigInt(LAYOUT.NAN_PREFIX)
|
|
60
|
+
const TAG_MASK_BIG = BigInt(LAYOUT.TAG_MASK)
|
|
61
|
+
const AUX_MASK_BIG = BigInt(LAYOUT.AUX_MASK)
|
|
62
|
+
const OFFSET_MASK_BIG = BigInt(LAYOUT.OFFSET_MASK)
|
|
63
|
+
const TAG_SHIFT_BIG = BigInt(LAYOUT.TAG_SHIFT)
|
|
64
|
+
const AUX_SHIFT_BIG = BigInt(LAYOUT.AUX_SHIFT)
|
|
65
|
+
const SSO_BIT_BIG = BigInt(LAYOUT.SSO_BIT)
|
|
61
66
|
|
|
62
67
|
// Low-level IR helpers previously lived here. Pure ones moved to src/ir.js;
|
|
63
68
|
// emit-calling ones (toBool, emitTypeofCmp, emitDecl, materializeMulti,
|
|
@@ -140,6 +145,95 @@ const tcoTailRewrite = (ir, resultType) => {
|
|
|
140
145
|
return ir
|
|
141
146
|
}
|
|
142
147
|
|
|
148
|
+
const ARENA_SAFE_CALLS = new Set([
|
|
149
|
+
'$__alloc', '$__alloc_hdr', '$__mkptr',
|
|
150
|
+
'$__ptr_offset', '$__ptr_type', '$__ptr_aux',
|
|
151
|
+
'$__len', '$__cap', '$__typed_shift', '$__typed_data',
|
|
152
|
+
])
|
|
153
|
+
|
|
154
|
+
const findFuncBodyStart = (fn) => {
|
|
155
|
+
for (let i = 2; i < fn.length; i++) {
|
|
156
|
+
const n = fn[i]
|
|
157
|
+
if (Array.isArray(n) && (n[0] === 'param' || n[0] === 'result' || n[0] === 'local' || n[0] === 'export')) continue
|
|
158
|
+
return i
|
|
159
|
+
}
|
|
160
|
+
return fn.length
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const heapGetIR = () => ctx.memory.shared
|
|
164
|
+
? ['i32.load', ['i32.const', 1020]]
|
|
165
|
+
: ['global.get', '$__heap']
|
|
166
|
+
|
|
167
|
+
const heapSetIR = value => ctx.memory.shared
|
|
168
|
+
? ['i32.store', ['i32.const', 1020], value]
|
|
169
|
+
: ['global.set', '$__heap', value]
|
|
170
|
+
|
|
171
|
+
function applyArenaRewind(func, fn, safeCallees) {
|
|
172
|
+
if (ctx.transform.optimize?.arenaRewind === false) return false
|
|
173
|
+
if (func.raw || func.sig.params.length !== 0 || func.sig.results.length !== 1) return false
|
|
174
|
+
if (func.sig.ptrKind != null) return false
|
|
175
|
+
if (func.sig.results[0] === 'f64' && func.valResult !== VAL.NUMBER) return false
|
|
176
|
+
if (func.sig.results[0] !== 'f64' && func.sig.results[0] !== 'i32') return false
|
|
177
|
+
|
|
178
|
+
const bodyStart = findFuncBodyStart(fn)
|
|
179
|
+
let hasAlloc = false
|
|
180
|
+
let unsafe = false
|
|
181
|
+
const scan = node => {
|
|
182
|
+
if (unsafe || !Array.isArray(node)) return
|
|
183
|
+
const op = node[0]
|
|
184
|
+
if (op === 'global.set' || op === 'return_call' || op === 'call_indirect' || op === 'call_ref') {
|
|
185
|
+
unsafe = true
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
if (op === 'call') {
|
|
189
|
+
const name = node[1]
|
|
190
|
+
if (name === '$__alloc' || name === '$__alloc_hdr') hasAlloc = true
|
|
191
|
+
if (!(safeCallees ?? ARENA_SAFE_CALLS).has(name)) {
|
|
192
|
+
unsafe = true
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (let i = 1; i < node.length; i++) scan(node[i])
|
|
197
|
+
}
|
|
198
|
+
for (let i = bodyStart; i < fn.length; i++) scan(fn[i])
|
|
199
|
+
if (unsafe || !hasAlloc) return false
|
|
200
|
+
|
|
201
|
+
let id = 0
|
|
202
|
+
const hasLocal = name => fn.some(n => Array.isArray(n) && n[0] === 'local' && n[1] === name)
|
|
203
|
+
while (hasLocal(`$${T}heap_save${id}`) || hasLocal(`$${T}arena_ret${id}`)) id++
|
|
204
|
+
const save = `$${T}heap_save${id}`
|
|
205
|
+
const ret = `$${T}arena_ret${id}`
|
|
206
|
+
const restore = () => heapSetIR(['local.get', save])
|
|
207
|
+
const resultType = func.sig.results[0]
|
|
208
|
+
|
|
209
|
+
const rewriteReturns = node => {
|
|
210
|
+
if (!Array.isArray(node)) return node
|
|
211
|
+
if (node[0] === 'return' && node.length > 1) {
|
|
212
|
+
return ['block',
|
|
213
|
+
['result', resultType],
|
|
214
|
+
['local.set', ret, node[1]],
|
|
215
|
+
restore(),
|
|
216
|
+
['return', ['local.get', ret]],
|
|
217
|
+
['unreachable']]
|
|
218
|
+
}
|
|
219
|
+
for (let i = 1; i < node.length; i++) node[i] = rewriteReturns(node[i])
|
|
220
|
+
return node
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const endsWithReturn = fn.at(-1)?.[0] === 'return' || fn.at(-1)?.[0] === 'return_call'
|
|
224
|
+
for (let i = bodyStart; i < fn.length; i++) fn[i] = rewriteReturns(fn[i])
|
|
225
|
+
const newBodyStart = findFuncBodyStart(fn)
|
|
226
|
+
fn.splice(newBodyStart, 0,
|
|
227
|
+
['local', save, 'i32'],
|
|
228
|
+
['local', ret, resultType],
|
|
229
|
+
['local.set', save, heapGetIR()])
|
|
230
|
+
if (!endsWithReturn) {
|
|
231
|
+
const last = fn.pop()
|
|
232
|
+
fn.push(['local.set', ret, last], restore(), ['local.get', ret])
|
|
233
|
+
}
|
|
234
|
+
return true
|
|
235
|
+
}
|
|
236
|
+
|
|
143
237
|
// === Module compilation ===
|
|
144
238
|
|
|
145
239
|
const cloneRepMap = map => map ? new Map([...map].map(([k, v]) => [k, { ...v }])) : null
|
|
@@ -186,20 +280,39 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
186
280
|
// cached widths reflect the pre-narrow state. Re-walk now with reps in place.
|
|
187
281
|
invalidateLocalsCache(body)
|
|
188
282
|
ctx.func.locals = block ? analyzeLocals(body) : new Map()
|
|
283
|
+
// Usage-based VAL.STRING inference for params not already typed by paramReps.
|
|
284
|
+
// Descends into nested closures so a param used as STRING only inside an inner
|
|
285
|
+
// arrow (e.g. parseLevel's `str` capture in watr) still gets seeded — the
|
|
286
|
+
// closure capture path then propagates VAL.STRING via captureValTypes.
|
|
287
|
+
if (block) {
|
|
288
|
+
const candidates = sig.params
|
|
289
|
+
.filter(p => !ctx.func.repByLocal?.get(p.name)?.val)
|
|
290
|
+
.map(p => p.name)
|
|
291
|
+
if (candidates.length) {
|
|
292
|
+
const inferred = inferStringParams(body, candidates)
|
|
293
|
+
for (const [n, vt] of inferred) updateRep(n, { val: vt })
|
|
294
|
+
}
|
|
295
|
+
}
|
|
189
296
|
if (block) {
|
|
190
297
|
analyzeValTypes(body)
|
|
191
298
|
analyzeIntCertain(body)
|
|
192
299
|
analyzeBoxedCaptures(body)
|
|
193
300
|
// Lower provably-monomorphic pointer locals to i32 offset storage.
|
|
301
|
+
// VAL.TYPED unbox requires a known element ctor (aux byte) — without it,
|
|
302
|
+
// the use site can't pick the right i32.store{8,16}/i32.store width and
|
|
303
|
+
// the rebox path can't reconstruct the NaN-box. Heterogeneous decls (two
|
|
304
|
+
// `let arr = ...` with different ctors, or a multi-ctor ternary) leave
|
|
305
|
+
// typedElem unset; skip unbox so reads/writes go through `__typed_set_idx`.
|
|
194
306
|
const unbox = analyzePtrUnboxable(body, ctx.func.locals, ctx.func.boxed)
|
|
195
307
|
if (unbox.size > 0) {
|
|
196
308
|
for (const [n, kind] of unbox) {
|
|
197
|
-
ctx.func.locals.set(n, 'i32')
|
|
198
309
|
const fields = { ptrKind: kind }
|
|
199
310
|
if (kind === VAL.TYPED) {
|
|
200
311
|
const aux = typedElemAux(ctx.types.typedElem?.get(n))
|
|
201
|
-
if (aux
|
|
312
|
+
if (aux == null) continue
|
|
313
|
+
fields.ptrAux = aux
|
|
202
314
|
}
|
|
315
|
+
ctx.func.locals.set(n, 'i32')
|
|
203
316
|
updateRep(n, fields)
|
|
204
317
|
}
|
|
205
318
|
}
|
|
@@ -233,7 +346,7 @@ function analyzeFuncForEmit(func, programFacts) {
|
|
|
233
346
|
function emitFunc(func, funcFacts, programFacts) {
|
|
234
347
|
const { paramReps } = programFacts
|
|
235
348
|
|
|
236
|
-
// Raw WAT functions (e.g., _alloc,
|
|
349
|
+
// Raw WAT functions (e.g., _alloc, _clear from memory module)
|
|
237
350
|
if (func.raw) return parseWat(func.raw)
|
|
238
351
|
|
|
239
352
|
const { name, body, exported, sig } = func
|
|
@@ -275,24 +388,28 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
275
388
|
fn.push(...sig.params.map(p => ['param', `$${p.name}`, p.type]))
|
|
276
389
|
fn.push(...sig.results.map(t => ['result', t]))
|
|
277
390
|
|
|
278
|
-
// Default params:
|
|
279
|
-
//
|
|
391
|
+
// Default params: ES spec says default applies only when arg is `undefined`
|
|
392
|
+
// (or missing). `null`, `0`, `false`, etc. all skip the default.
|
|
280
393
|
const defaults = func.defaults || {}
|
|
281
394
|
const defaultInits = []
|
|
282
395
|
for (const [pname, defVal] of Object.entries(defaults)) {
|
|
283
396
|
const p = sig.params.find(p => p.name === pname)
|
|
284
397
|
const t = p?.type || 'f64'
|
|
285
398
|
defaultInits.push(
|
|
286
|
-
['if',
|
|
399
|
+
['if', isUndef(typed(['local.get', `$${pname}`], 'f64')),
|
|
287
400
|
['then', ['local.set', `$${pname}`, t === 'f64' ? asF64(emit(defVal)) : asI32(emit(defVal))]]])
|
|
288
401
|
}
|
|
289
402
|
|
|
290
403
|
// Box params that are mutably captured: allocate cell, copy param value
|
|
291
404
|
const boxedParamInits = []
|
|
405
|
+
const preboxedLocalInits = []
|
|
406
|
+
ctx.func.preboxed = new Set()
|
|
407
|
+
const paramNames = new Set(sig.params.map(p => p.name))
|
|
292
408
|
for (const p of sig.params) {
|
|
293
409
|
if (ctx.func.boxed.has(p.name)) {
|
|
294
410
|
const cell = ctx.func.boxed.get(p.name)
|
|
295
411
|
ctx.func.locals.set(cell, 'i32')
|
|
412
|
+
ctx.func.preboxed.add(p.name)
|
|
296
413
|
const lget = typed(['local.get', `$${p.name}`], p.type)
|
|
297
414
|
if (p.ptrKind != null) lget.ptrKind = p.ptrKind
|
|
298
415
|
boxedParamInits.push(
|
|
@@ -300,6 +417,14 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
300
417
|
['f64.store', ['local.get', `$${cell}`], asF64(lget)])
|
|
301
418
|
}
|
|
302
419
|
}
|
|
420
|
+
for (const [name, cell] of ctx.func.boxed) {
|
|
421
|
+
if (paramNames.has(name)) continue
|
|
422
|
+
ctx.func.locals.set(cell, 'i32')
|
|
423
|
+
ctx.func.preboxed.add(name)
|
|
424
|
+
preboxedLocalInits.push(
|
|
425
|
+
['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]],
|
|
426
|
+
['f64.store', ['local.get', `$${cell}`], nullExpr()])
|
|
427
|
+
}
|
|
303
428
|
|
|
304
429
|
if (block) {
|
|
305
430
|
const stmts = emitBody(body)
|
|
@@ -307,16 +432,16 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
307
432
|
// I: Skip trailing fallback when last statement is return (unreachable code)
|
|
308
433
|
const lastStmt = stmts.at(-1)
|
|
309
434
|
const endsWithReturn = lastStmt && (lastStmt[0] === 'return' || lastStmt[0] === 'return_call')
|
|
310
|
-
fn.push(...defaultInits, ...boxedParamInits, ...stmts, ...(endsWithReturn ? [] : sig.results.map(t => [`${t}.const`, 0])))
|
|
435
|
+
fn.push(...defaultInits, ...boxedParamInits, ...preboxedLocalInits, ...stmts, ...(endsWithReturn ? [] : sig.results.map(t => [`${t}.const`, 0])))
|
|
311
436
|
} else if (multi && body[0] === '[') {
|
|
312
437
|
const values = body.slice(1).map(e => asF64(emit(e)))
|
|
313
438
|
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
314
|
-
fn.push(...boxedParamInits, ...values)
|
|
439
|
+
fn.push(...boxedParamInits, ...preboxedLocalInits, ...values)
|
|
315
440
|
} else {
|
|
316
441
|
const ir = emit(body)
|
|
317
442
|
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
318
443
|
const finalIR = sig.ptrKind != null ? asPtrOffset(ir, sig.ptrKind) : asParamType(ir, sig.results[0])
|
|
319
|
-
fn.push(...defaultInits, ...boxedParamInits, tcoTailRewrite(finalIR, sig.results[0]))
|
|
444
|
+
fn.push(...defaultInits, ...boxedParamInits, ...preboxedLocalInits, tcoTailRewrite(finalIR, sig.results[0]))
|
|
320
445
|
}
|
|
321
446
|
|
|
322
447
|
// Restore schema.vars so param bindings don't leak to next function.
|
|
@@ -329,18 +454,22 @@ function emitFunc(func, funcFacts, programFacts) {
|
|
|
329
454
|
*
|
|
330
455
|
* For each `isBoundaryWrapped(func)`, emit a sibling `$${name}$exp` that:
|
|
331
456
|
* - holds the (export "name") attribute (JS sees the wrapper)
|
|
332
|
-
* - takes
|
|
457
|
+
* - takes i64 params always — JS-side carrier is BigInt that reinterprets to
|
|
458
|
+
* f64 NaN-box bits. i64 dodges V8's spec-permitted NaN canonicalization at
|
|
459
|
+
* the wasm↔JS boundary (see ToJSValue / ToWebAssemblyValue). Host wrap()
|
|
460
|
+
* in src/host.js pairs by converting BigInt↔f64 via reinterpret bits.
|
|
333
461
|
* - converts each narrowed param at the call: f64 → i32 (truncate-sat) for
|
|
334
462
|
* numeric narrowed, f64 → i32-offset (`i32.wrap_i64 + i64.reinterpret_f64`)
|
|
335
|
-
* for pointer narrowed
|
|
463
|
+
* for pointer narrowed. The reinterpret happens once at param decode and
|
|
464
|
+
* once at result encode; numeric exports without narrowing skip wrapping
|
|
465
|
+
* entirely (no NaN-class values).
|
|
336
466
|
* - forwards args to the inner $${name}
|
|
337
|
-
* - reboxes the narrowed result
|
|
467
|
+
* - reboxes the narrowed result and reinterprets to i64 for the boundary
|
|
338
468
|
*
|
|
339
|
-
* Param
|
|
340
|
-
*
|
|
341
|
-
* - p.type = 'i32', ptrKind set → i32.wrap_i64(i64.reinterpret_f64(local.get $p))
|
|
469
|
+
* Param decode (i64 → f64): each param gets `f64.reinterpret_i64` before the
|
|
470
|
+
* existing narrowing convert. f64 inner params just need the reinterpret.
|
|
342
471
|
*
|
|
343
|
-
* Result rebox cases:
|
|
472
|
+
* Result rebox cases (then reinterpret to i64 at the boundary):
|
|
344
473
|
* - sig.ptrKind != null → mkPtrIR(ptrKind, ptrAux ?? 0, callIR)
|
|
345
474
|
* - sig.results[0] = i32 → f64.convert_i32_s(callIR)
|
|
346
475
|
* - sig.results[0] = f64 → callIR (some params narrowed but result stayed f64)
|
|
@@ -350,18 +479,23 @@ function synthesizeBoundaryWrappers() {
|
|
|
350
479
|
for (const func of ctx.func.list) {
|
|
351
480
|
if (!isBoundaryWrapped(func)) continue
|
|
352
481
|
const { name, sig } = func
|
|
482
|
+
// Per-position i64 carrier: only swap to i64 where a NaN-boxed pointer
|
|
483
|
+
// actually crosses the boundary (param.ptrKind set, or result with
|
|
484
|
+
// sig.ptrKind set). Numeric narrowing (i32 trunc-sat / convert) keeps f64
|
|
485
|
+
// so callers seeing the raw export get a plain Number for numerics.
|
|
486
|
+
const paramI64 = sig.params.map(p => p.ptrKind != null)
|
|
487
|
+
const resultI64 = sig.ptrKind != null
|
|
353
488
|
const wrapNode = ['func', `$${name}$exp`, ['export', `"${name}"`]]
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const args = sig.params.map(p => {
|
|
489
|
+
sig.params.forEach((p, i) => wrapNode.push(['param', `$${p.name}`, paramI64[i] ? 'i64' : 'f64']))
|
|
490
|
+
wrapNode.push(['result', resultI64 ? 'i64' : 'f64'])
|
|
491
|
+
const args = sig.params.map((p, i) => {
|
|
358
492
|
const get = ['local.get', `$${p.name}`]
|
|
359
|
-
if (p.type === 'f64') return get
|
|
360
493
|
if (p.ptrKind != null) {
|
|
361
|
-
// NaN-
|
|
362
|
-
return ['i32.wrap_i64',
|
|
494
|
+
// ptrKind: i64 carrier carries NaN-box bits → wrap to i32 offset
|
|
495
|
+
return ['i32.wrap_i64', get]
|
|
363
496
|
}
|
|
364
|
-
|
|
497
|
+
if (p.type === 'f64') return get
|
|
498
|
+
// Numeric narrowing: f64 → i32 truncate
|
|
365
499
|
return ['i32.trunc_sat_f64_s', get]
|
|
366
500
|
})
|
|
367
501
|
const callIR = ['call', `$${name}`, ...args]
|
|
@@ -374,7 +508,9 @@ function synthesizeBoundaryWrappers() {
|
|
|
374
508
|
} else {
|
|
375
509
|
body = callIR
|
|
376
510
|
}
|
|
377
|
-
wrapNode.push(body)
|
|
511
|
+
wrapNode.push(resultI64 ? ['i64.reinterpret_f64', body] : body)
|
|
512
|
+
func._exportUsesI64 = resultI64 || paramI64.some(Boolean)
|
|
513
|
+
func._exportI64Sig = { params: paramI64, result: resultI64 }
|
|
378
514
|
wrappers.push(wrapNode)
|
|
379
515
|
}
|
|
380
516
|
return wrappers
|
|
@@ -398,6 +534,7 @@ function emitClosureBody(cb) {
|
|
|
398
534
|
// Reset per-function state for closure body
|
|
399
535
|
ctx.func.locals = new Map()
|
|
400
536
|
ctx.func.repByLocal = null
|
|
537
|
+
if (cb.intConsts) for (const [name, v] of cb.intConsts) updateRep(name, { intConst: v })
|
|
401
538
|
if (cb.valTypes) for (const [name, vt] of cb.valTypes) updateRep(name, { val: vt })
|
|
402
539
|
if (cb.schemaVars) {
|
|
403
540
|
ctx.schema.vars = new Map([...prevSchemaVars, ...cb.schemaVars])
|
|
@@ -413,6 +550,8 @@ function emitClosureBody(cb) {
|
|
|
413
550
|
}
|
|
414
551
|
// In closure bodies, boxed captures use the original name as both var and cell local
|
|
415
552
|
ctx.func.boxed = cb.boxed ? new Map([...cb.boxed].map(v => [v, v])) : new Map()
|
|
553
|
+
const parentBoxedCaptures = new Set(cb.boxed || [])
|
|
554
|
+
ctx.func.preboxed = new Set()
|
|
416
555
|
ctx.func.stack = []
|
|
417
556
|
ctx.func.uniq = Math.max(ctx.func.uniq, 100) // avoid label collisions
|
|
418
557
|
ctx.func.body = cb.body
|
|
@@ -445,11 +584,35 @@ function emitClosureBody(cb) {
|
|
|
445
584
|
const block = Array.isArray(cb.body) && cb.body[0] === '{}' && cb.body[1]?.[0] !== ':'
|
|
446
585
|
let bodyIR
|
|
447
586
|
if (block) {
|
|
587
|
+
invalidateLocalsCache(cb.body)
|
|
448
588
|
for (const [k, v] of analyzeLocals(cb.body)) if (!ctx.func.locals.has(k)) ctx.func.locals.set(k, v)
|
|
589
|
+
// Usage-based STRING inference for closure params not seeded by captureValTypes.
|
|
590
|
+
// (Captures already have their parent's val type via cb.valTypes above.)
|
|
591
|
+
{
|
|
592
|
+
const candidates = cb.params.filter(p => !ctx.func.repByLocal?.get(p)?.val)
|
|
593
|
+
if (candidates.length) {
|
|
594
|
+
const inferred = inferStringParams(cb.body, candidates)
|
|
595
|
+
for (const [n, vt] of inferred) updateRep(n, { val: vt })
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
analyzeValTypes(cb.body)
|
|
599
|
+
analyzeIntCertain(cb.body)
|
|
449
600
|
// Detect captures from deeper nested arrows that mutate this body's locals/params/captures
|
|
450
601
|
analyzeBoxedCaptures(cb.body)
|
|
451
602
|
for (const name of ctx.func.boxed.keys()) {
|
|
452
|
-
if (ctx.func.locals.get(name) === 'f64') ctx.func.locals.set(name, 'i32')
|
|
603
|
+
if (parentBoxedCaptures.has(name) && ctx.func.locals.get(name) === 'f64') ctx.func.locals.set(name, 'i32')
|
|
604
|
+
}
|
|
605
|
+
const unbox = analyzePtrUnboxable(cb.body, ctx.func.locals, ctx.func.boxed)
|
|
606
|
+
for (const [name, kind] of unbox) {
|
|
607
|
+
if (cb.params.includes(name) || cb.captures.includes(name)) continue
|
|
608
|
+
const fields = { ptrKind: kind }
|
|
609
|
+
if (kind === VAL.TYPED) {
|
|
610
|
+
const aux = typedElemAux(ctx.types.typedElem?.get(name))
|
|
611
|
+
if (aux == null) continue
|
|
612
|
+
fields.ptrAux = aux
|
|
613
|
+
}
|
|
614
|
+
ctx.func.locals.set(name, 'i32')
|
|
615
|
+
updateRep(name, fields)
|
|
453
616
|
}
|
|
454
617
|
bodyIR = emitBody(cb.body)
|
|
455
618
|
} else {
|
|
@@ -469,6 +632,28 @@ function emitClosureBody(cb) {
|
|
|
469
632
|
inc('__alloc_hdr', '__mkptr')
|
|
470
633
|
}
|
|
471
634
|
|
|
635
|
+
const boxedCaptureNames = new Set(cb.captures.filter(name => parentBoxedCaptures.has(name)))
|
|
636
|
+
for (const name of boxedCaptureNames) ctx.func.preboxed.add(name)
|
|
637
|
+
const boxedValueCaptureNames = new Set(cb.captures.filter(name => ctx.func.boxed.has(name) && !parentBoxedCaptures.has(name)))
|
|
638
|
+
for (const name of boxedValueCaptureNames) {
|
|
639
|
+
ctx.func.locals.set(ctx.func.boxed.get(name), 'i32')
|
|
640
|
+
ctx.func.preboxed.add(name)
|
|
641
|
+
}
|
|
642
|
+
const boxedParamNames = new Set(cb.params.filter(name => ctx.func.boxed.has(name)))
|
|
643
|
+
for (const name of boxedParamNames) {
|
|
644
|
+
ctx.func.locals.set(ctx.func.boxed.get(name), 'i32')
|
|
645
|
+
ctx.func.preboxed.add(name)
|
|
646
|
+
}
|
|
647
|
+
const preboxedLocalInits = []
|
|
648
|
+
for (const [name, cell] of ctx.func.boxed) {
|
|
649
|
+
if (boxedCaptureNames.has(name) || boxedValueCaptureNames.has(name) || boxedParamNames.has(name)) continue
|
|
650
|
+
ctx.func.locals.set(cell, 'i32')
|
|
651
|
+
ctx.func.preboxed.add(name)
|
|
652
|
+
preboxedLocalInits.push(
|
|
653
|
+
['local.set', `$${cell}`, ['call', '$__alloc', ['i32.const', 8]]],
|
|
654
|
+
['f64.store', ['local.get', `$${cell}`], nullExpr()])
|
|
655
|
+
}
|
|
656
|
+
|
|
472
657
|
// Insert locals (captures + params + declared)
|
|
473
658
|
for (const [l, t] of ctx.func.locals) fn.push(['local', `$${l}`, t])
|
|
474
659
|
|
|
@@ -481,8 +666,15 @@ function emitClosureBody(cb) {
|
|
|
481
666
|
for (let i = 0; i < cb.captures.length; i++) {
|
|
482
667
|
const name = cb.captures[i]
|
|
483
668
|
const addr = ['i32.add', ['local.get', `$${envBase}`], ['i32.const', i * 8]]
|
|
484
|
-
|
|
485
|
-
|
|
669
|
+
if (parentBoxedCaptures.has(name)) {
|
|
670
|
+
fn.push(['local.set', `$${name}`, ['i32.load', addr]])
|
|
671
|
+
} else if (boxedValueCaptureNames.has(name)) {
|
|
672
|
+
fn.push(
|
|
673
|
+
['local.set', `$${ctx.func.boxed.get(name)}`, ['call', '$__alloc', ['i32.const', 8]]],
|
|
674
|
+
['f64.store', boxedAddr(name), ['f64.load', addr]])
|
|
675
|
+
} else {
|
|
676
|
+
fn.push(['local.set', `$${name}`, ['f64.load', addr]])
|
|
677
|
+
}
|
|
486
678
|
}
|
|
487
679
|
}
|
|
488
680
|
|
|
@@ -490,7 +682,14 @@ function emitClosureBody(cb) {
|
|
|
490
682
|
// Rest name (if present) is last in cb.params — handled separately below.
|
|
491
683
|
const fixedParamN = cb.params.length - (cb.rest ? 1 : 0)
|
|
492
684
|
for (let i = 0; i < fixedParamN && i < W; i++) {
|
|
493
|
-
|
|
685
|
+
const pname = cb.params[i]
|
|
686
|
+
if (boxedParamNames.has(pname)) {
|
|
687
|
+
fn.push(
|
|
688
|
+
['local.set', `$${ctx.func.boxed.get(pname)}`, ['call', '$__alloc', ['i32.const', 8]]],
|
|
689
|
+
['f64.store', boxedAddr(pname), ['local.get', `$__a${i}`]])
|
|
690
|
+
} else {
|
|
691
|
+
fn.push(['local.set', `$${pname}`, ['local.get', `$__a${i}`]])
|
|
692
|
+
}
|
|
494
693
|
}
|
|
495
694
|
|
|
496
695
|
// Rest param: pack slots a[fixedParams..argc-1] into fresh array.
|
|
@@ -516,20 +715,34 @@ function emitClosureBody(cb) {
|
|
|
516
715
|
['i32.add', ['local.get', `$${restOff}`], ['i32.const', i * 8]],
|
|
517
716
|
['local.get', `$__a${fixedN + i}`]]]])
|
|
518
717
|
}
|
|
519
|
-
|
|
520
|
-
|
|
718
|
+
const restValue = ['call', '$__mkptr', ['i32.const', PTR.ARRAY], ['i32.const', 0], ['local.get', `$${restOff}`]]
|
|
719
|
+
if (boxedParamNames.has(cb.rest)) {
|
|
720
|
+
fn.push(
|
|
721
|
+
['local.set', `$${ctx.func.boxed.get(cb.rest)}`, ['call', '$__alloc', ['i32.const', 8]]],
|
|
722
|
+
['f64.store', boxedAddr(cb.rest), restValue])
|
|
723
|
+
} else {
|
|
724
|
+
fn.push(['local.set', `$${cb.rest}`, restValue])
|
|
725
|
+
}
|
|
521
726
|
}
|
|
522
727
|
|
|
523
728
|
// Default params for closures (check sentinel after unpack)
|
|
729
|
+
// Only `undefined` triggers default per spec — `null`/`0`/`false` pass through.
|
|
524
730
|
if (cb.defaults) {
|
|
525
731
|
for (const [pname, defVal] of Object.entries(cb.defaults)) {
|
|
526
|
-
|
|
527
|
-
['
|
|
732
|
+
if (boxedParamNames.has(pname)) {
|
|
733
|
+
fn.push(['if', isUndef(['f64.load', boxedAddr(pname)]),
|
|
734
|
+
['then', ['f64.store', boxedAddr(pname), asF64(emit(defVal))]]])
|
|
735
|
+
} else {
|
|
736
|
+
fn.push(['if', isUndef(['local.get', `$${pname}`]),
|
|
737
|
+
['then', ['local.set', `$${pname}`, asF64(emit(defVal))]]])
|
|
738
|
+
}
|
|
528
739
|
}
|
|
529
740
|
}
|
|
741
|
+
fn.push(...preboxedLocalInits)
|
|
530
742
|
fn.push(...bodyIR)
|
|
531
743
|
// I: Skip trailing fallback when last statement is return
|
|
532
|
-
|
|
744
|
+
// Implicit fall-through return is `undefined` per JS spec, not 0.
|
|
745
|
+
if (block && !(bodyIR.at(-1)?.[0] === 'return' || bodyIR.at(-1)?.[0] === 'return_call')) fn.push(undefExpr())
|
|
533
746
|
ctx.schema.vars = prevSchemaVars
|
|
534
747
|
ctx.types.typedElem = prevTypedElems
|
|
535
748
|
return fn
|
|
@@ -582,9 +795,9 @@ function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
582
795
|
const bt = `${T}box`
|
|
583
796
|
ctx.func.locals.set(bt, 'i32')
|
|
584
797
|
for (const [name, { schemaId, schema }] of ctx.schema.autoBox) {
|
|
585
|
-
inc('
|
|
798
|
+
inc('__alloc_hdr', '__mkptr')
|
|
586
799
|
boxInit.push(
|
|
587
|
-
['local.set', `$${bt}`, ['call', '$
|
|
800
|
+
['local.set', `$${bt}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)], ['i32.const', 8]]],
|
|
588
801
|
['f64.store', ['local.get', `$${bt}`],
|
|
589
802
|
ctx.func.names.has(name) ? ['f64.const', 0] : ['global.get', `$${name}`]],
|
|
590
803
|
...schema.slice(1).map((_, i) =>
|
|
@@ -602,7 +815,12 @@ function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
602
815
|
// __dyn_get (set transitively by resolveIncludes() later) are listed
|
|
603
816
|
// explicitly here because the dep graph hasn't been expanded yet at
|
|
604
817
|
// start-fn build time.
|
|
605
|
-
|
|
818
|
+
// __jp_obj registers schemas at runtime, so the table must be allocated
|
|
819
|
+
// (and __schema_next initialized) even with zero compile-time schemas.
|
|
820
|
+
// __jp pulls __jp_obj transitively via the include-graph but resolveIncludes
|
|
821
|
+
// runs after buildStartFn, so check the top-level __jp parser as a proxy.
|
|
822
|
+
const hasJpObj = ctx.core.includes.has('__jp_obj') || ctx.core.includes.has('__jp')
|
|
823
|
+
const needsSchemaTbl = (ctx.schema.list.length && (
|
|
606
824
|
ctx.core.includes.has('__stringify') ||
|
|
607
825
|
ctx.core.includes.has('__dyn_get') ||
|
|
608
826
|
ctx.core.includes.has('__dyn_get_t') ||
|
|
@@ -610,17 +828,24 @@ function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
610
828
|
ctx.core.includes.has('__dyn_get_any_t') ||
|
|
611
829
|
ctx.core.includes.has('__dyn_get_expr') ||
|
|
612
830
|
ctx.core.includes.has('__dyn_get_expr_t') ||
|
|
613
|
-
ctx.core.includes.has('__dyn_get_or'))
|
|
831
|
+
ctx.core.includes.has('__dyn_get_or'))) ||
|
|
832
|
+
hasJpObj
|
|
614
833
|
if (needsSchemaTbl) {
|
|
615
834
|
const nSchemas = ctx.schema.list.length
|
|
835
|
+
// Reserve trailing slots for runtime-registered schemas (used by JSON.parse
|
|
836
|
+
// shape caching). __schema_next tracks the first free runtime slot.
|
|
837
|
+
const runtimeReserve = hasJpObj ? 256 : 0
|
|
616
838
|
const stbl = `${T}stbl`
|
|
617
839
|
const sarr = `${T}sarr`
|
|
618
840
|
ctx.func.locals.set(stbl, 'i32')
|
|
619
841
|
ctx.func.locals.set(sarr, 'i32')
|
|
620
842
|
inc('__alloc', '__alloc_hdr', '__mkptr')
|
|
621
843
|
schemaInit.push(
|
|
622
|
-
['local.set', `$${stbl}`, ['call', '$__alloc', ['i32.const', nSchemas * 8]]],
|
|
844
|
+
['local.set', `$${stbl}`, ['call', '$__alloc', ['i32.const', (nSchemas + runtimeReserve) * 8]]],
|
|
623
845
|
['global.set', '$__schema_tbl', ['local.get', `$${stbl}`]])
|
|
846
|
+
if (runtimeReserve) {
|
|
847
|
+
schemaInit.push(['global.set', '$__schema_next', ['i32.const', nSchemas]])
|
|
848
|
+
}
|
|
624
849
|
for (let s = 0; s < nSchemas; s++) {
|
|
625
850
|
const keys = ctx.schema.list[s]
|
|
626
851
|
const n = keys.length
|
|
@@ -651,12 +876,15 @@ function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
651
876
|
for (const s of ctx.runtime.typeofStrs)
|
|
652
877
|
typeofInit.push(['global.set', `$__tof_${s}`, emit(['str', s])])
|
|
653
878
|
}
|
|
654
|
-
|
|
879
|
+
// WASI timer queue needs an init call in __start; JS-host mode delegates
|
|
880
|
+
// scheduling to the host so no init/loop is needed.
|
|
881
|
+
const wasiTimers = ctx.features.timers && ctx.transform.host === 'wasi'
|
|
882
|
+
if (moduleInits.length || init?.length || boxInit.length || schemaInit.length || typeofInit.length || strPoolInit.length || wasiTimers) {
|
|
655
883
|
const initIR = normalizeIR(init)
|
|
656
884
|
const startFn = ['func', '$__start']
|
|
657
885
|
for (const [l, t] of ctx.func.locals) startFn.push(['local', `$${l}`, t])
|
|
658
886
|
startFn.push(...strPoolInit, ...typeofInit, ...boxInit, ...schemaInit,
|
|
659
|
-
...(
|
|
887
|
+
...(wasiTimers ? [['call', '$__timer_init']] : []),
|
|
660
888
|
...moduleInits, ...initIR,
|
|
661
889
|
...(ctx.features.blockingTimers ? [['call', '$__timer_loop']] : []),
|
|
662
890
|
)
|
|
@@ -721,12 +949,21 @@ function dedupClosureBodies(closureFuncs, sec) {
|
|
|
721
949
|
else { hashToName.set(key, name); keepSet.add(name) }
|
|
722
950
|
}
|
|
723
951
|
if (!redirect.size) return
|
|
724
|
-
|
|
952
|
+
// Remove duplicate declarations BEFORE rewriting references — otherwise redirectRefs
|
|
953
|
+
// renames the declaration itself and the filter can't find the original name.
|
|
725
954
|
const kept = sec.funcs.filter(fn => {
|
|
726
955
|
if (!Array.isArray(fn) || fn[0] !== 'func') return true
|
|
727
956
|
const name = typeof fn[1] === 'string' && fn[1][0] === '$' ? fn[1].slice(1) : null
|
|
728
957
|
return !name || !redirect.has(name)
|
|
729
958
|
})
|
|
959
|
+
const redirectRefs = node => {
|
|
960
|
+
if (typeof node === 'string') return node[0] === '$' && redirect.has(node.slice(1)) ? `$${redirect.get(node.slice(1))}` : node
|
|
961
|
+
if (!Array.isArray(node)) return node
|
|
962
|
+
for (let i = 0; i < node.length; i++) node[i] = redirectRefs(node[i])
|
|
963
|
+
return node
|
|
964
|
+
}
|
|
965
|
+
for (const fn of kept) redirectRefs(fn)
|
|
966
|
+
ctx.closure.table = ctx.closure.table.map(n => redirect.get(n) || n)
|
|
730
967
|
sec.funcs.length = 0
|
|
731
968
|
sec.funcs.push(...kept)
|
|
732
969
|
}
|
|
@@ -752,8 +989,7 @@ function dedupClosureBodies(closureFuncs, sec) {
|
|
|
752
989
|
* Both `call` and `return_call` (tail call) sites are rewritten in the same walk.
|
|
753
990
|
*/
|
|
754
991
|
function finalizeClosureTable(sec) {
|
|
755
|
-
|
|
756
|
-
let indirectUsed = false
|
|
992
|
+
let indirectUsed = ctx.transform.host === 'wasi'
|
|
757
993
|
const scan = (n) => {
|
|
758
994
|
if (!Array.isArray(n) || indirectUsed) return
|
|
759
995
|
if (n[0] === 'call_indirect') { indirectUsed = true; return }
|
|
@@ -767,8 +1003,9 @@ function finalizeClosureTable(sec) {
|
|
|
767
1003
|
}
|
|
768
1004
|
// Keep table if call_indirect is used (closures, timer dispatch, etc.)
|
|
769
1005
|
if (indirectUsed) {
|
|
1006
|
+
if (!ctx.closure.table) ctx.closure.table = []
|
|
770
1007
|
sec.table = [['table', ['export', '"__jz_table"'], ctx.closure.table.length, 'funcref']]
|
|
771
|
-
sec.elem = [['elem', ['i32.const', 0], 'func', ...ctx.closure.table.map(n => `$${n}`)]]
|
|
1008
|
+
sec.elem = ctx.closure.table.length ? [['elem', ['i32.const', 0], 'func', ...ctx.closure.table.map(n => `$${n}`)]] : []
|
|
772
1009
|
return
|
|
773
1010
|
}
|
|
774
1011
|
sec.table = []
|
|
@@ -832,7 +1069,7 @@ function finalizeClosureTable(sec) {
|
|
|
832
1069
|
* 1. resolveIncludes() — close the include set under stdlib dependencies.
|
|
833
1070
|
* 2. Emit memory section ONLY when some included helper uses memory ops
|
|
834
1071
|
* (G optimization: pure scalar programs ship without memory + __heap).
|
|
835
|
-
* When memory is needed, the allocator (__alloc + __alloc_hdr +
|
|
1072
|
+
* When memory is needed, the allocator (__alloc + __alloc_hdr + __clear)
|
|
836
1073
|
* is force-included since stdlib funcs may call into it.
|
|
837
1074
|
* 3. Pull external (host) stdlibs into sec.extStdlib (must precede normal
|
|
838
1075
|
* imports in the module byte order).
|
|
@@ -848,11 +1085,11 @@ function pullStdlib(sec) {
|
|
|
848
1085
|
const needsMemory = [...ctx.core.includes].some(n => ctx.core.stdlib[n] && MEM_OPS.test(ctx.core.stdlib[n]))
|
|
849
1086
|
if (!needsMemory) ctx.scope.globals.delete('__heap')
|
|
850
1087
|
if (needsMemory && ctx.module.modules.core) {
|
|
851
|
-
for (const fn of ['__alloc', '__alloc_hdr', '
|
|
1088
|
+
for (const fn of ['__alloc', '__alloc_hdr', '__clear']) if (!ctx.core.includes.has(fn)) ctx.core.includes.add(fn)
|
|
852
1089
|
const pages = ctx.memory.pages || 1
|
|
853
1090
|
if (ctx.memory.shared) sec.imports.push(['import', '"env"', '"memory"', ['memory', pages]])
|
|
854
1091
|
else sec.memory.push(['memory', ['export', '"memory"'], pages])
|
|
855
|
-
if (ctx.transform.
|
|
1092
|
+
if (ctx.transform.alloc !== false && ctx.core._allocRawFuncs)
|
|
856
1093
|
sec.funcs.push(...ctx.core._allocRawFuncs.map(s => parseWat(s)))
|
|
857
1094
|
}
|
|
858
1095
|
|
|
@@ -860,10 +1097,15 @@ function pullStdlib(sec) {
|
|
|
860
1097
|
const v = ctx.core.stdlib[name]
|
|
861
1098
|
return typeof v === 'function' ? v() : v
|
|
862
1099
|
}
|
|
1100
|
+
// Track __ext_* names emitted to host imports. The cleanup below deletes them
|
|
1101
|
+
// from ctx.core.includes (moved into sec.extStdlib instead), so any post-compile
|
|
1102
|
+
// audit (e.g. host: 'wasi') needs to read this list rather than the includes set.
|
|
1103
|
+
ctx.core.extImports ??= new Set()
|
|
863
1104
|
for (const name of Object.keys(ctx.core.stdlib)) {
|
|
864
1105
|
if (name.startsWith('__ext_') && ctx.core.includes.has(name)) {
|
|
865
1106
|
const parsed = parseWat(stdlibStr(name))
|
|
866
1107
|
sec.extStdlib.push(parsed[0] === "module" ? parsed[1] : parsed)
|
|
1108
|
+
ctx.core.extImports.add(name)
|
|
867
1109
|
ctx.core.includes.delete(name)
|
|
868
1110
|
}
|
|
869
1111
|
}
|
|
@@ -871,6 +1113,12 @@ function pullStdlib(sec) {
|
|
|
871
1113
|
sec.stdlib.push(...[...ctx.core.includes].map(n => parseWat(stdlibStr(n))))
|
|
872
1114
|
}
|
|
873
1115
|
|
|
1116
|
+
function syncImports(sec) {
|
|
1117
|
+
for (const imp of ctx.module.imports) {
|
|
1118
|
+
if (!sec.imports.some(i => i[1] === imp[1] && i[2] === imp[2])) sec.imports.push(imp)
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
874
1122
|
/**
|
|
875
1123
|
* Phase: whole-module + per-function optimization passes.
|
|
876
1124
|
*
|
|
@@ -904,6 +1152,19 @@ function optimizeModule(sec) {
|
|
|
904
1152
|
ctx.runtime.strPool = poolRef.pool
|
|
905
1153
|
}
|
|
906
1154
|
for (const s of [...sec.funcs, ...sec.stdlib, ...sec.start]) optimizeFunc(s, cfg)
|
|
1155
|
+
if (!cfg || cfg.arenaRewind !== false) {
|
|
1156
|
+
const safeCallees = arenaRewindModule([...sec.funcs, ...sec.stdlib, ...sec.start])
|
|
1157
|
+
// Build name → WAT IR map for user functions
|
|
1158
|
+
const fnByName = new Map()
|
|
1159
|
+
for (const fn of sec.funcs) {
|
|
1160
|
+
if (Array.isArray(fn) && fn[0] === 'func' && typeof fn[1] === 'string')
|
|
1161
|
+
fnByName.set(fn[1], fn)
|
|
1162
|
+
}
|
|
1163
|
+
for (const func of ctx.func.list) {
|
|
1164
|
+
const fn = fnByName.get(`$${func.name}`)
|
|
1165
|
+
if (fn) applyArenaRewind(func, fn, safeCallees)
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
907
1168
|
if (!cfg || cfg.hoistConstantPool !== false)
|
|
908
1169
|
hoistConstantPool([...sec.funcs, ...sec.stdlib, ...sec.start], (name, wat) => ctx.scope.globals.set(name, wat))
|
|
909
1170
|
|
|
@@ -911,8 +1172,10 @@ function optimizeModule(sec) {
|
|
|
911
1172
|
if (dataLen > 1024 && !ctx.memory.shared) {
|
|
912
1173
|
const heapBase = (dataLen + 7) & ~7
|
|
913
1174
|
ctx.scope.globals.set('__heap', `(global $__heap (mut i32) (i32.const ${heapBase}))`)
|
|
1175
|
+
if (ctx.scope.globals.has('__heap_start'))
|
|
1176
|
+
ctx.scope.globals.set('__heap_start', `(global $__heap_start (mut i32) (i32.const ${heapBase}))`)
|
|
914
1177
|
for (const s of sec.stdlib)
|
|
915
|
-
if (s[0] === 'func' && s[1] === '$
|
|
1178
|
+
if (s[0] === 'func' && s[1] === '$__clear')
|
|
916
1179
|
for (let i = 2; i < s.length; i++)
|
|
917
1180
|
if (Array.isArray(s[i]) && s[i][0] === 'global.set' && Array.isArray(s[i][2]) && s[i][2][0] === 'i32.const')
|
|
918
1181
|
s[i][2][1] = `${heapBase}`
|
|
@@ -942,12 +1205,14 @@ function stripStaticDataPrefix(sec) {
|
|
|
942
1205
|
for (const slotOff of ctx.runtime.staticPtrSlots) {
|
|
943
1206
|
if (slotOff < prefix) continue
|
|
944
1207
|
const bits = dv.getBigUint64(slotOff, true)
|
|
945
|
-
if (((bits >> 48n) & 0xFFF8n) !==
|
|
946
|
-
const ty = Number((bits >>
|
|
1208
|
+
if (((bits >> 48n) & 0xFFF8n) !== NAN_PREFIX) continue
|
|
1209
|
+
const ty = Number((bits >> TAG_SHIFT_BIG) & TAG_MASK_BIG)
|
|
947
1210
|
if (!SHIFTABLE.has(ty)) continue
|
|
948
|
-
|
|
1211
|
+
// SSO STRING: "offset" holds packed bytes, not a heap address — never shift.
|
|
1212
|
+
if (ty === PTR.STRING && ((bits >> AUX_SHIFT_BIG) & SSO_BIT_BIG)) continue
|
|
1213
|
+
const off = Number(bits & OFFSET_MASK_BIG)
|
|
949
1214
|
if (off < prefix) continue
|
|
950
|
-
const hi = bits & ~
|
|
1215
|
+
const hi = bits & ~OFFSET_MASK_BIG
|
|
951
1216
|
dv.setBigUint64(slotOff, hi | BigInt(off - prefix), true)
|
|
952
1217
|
}
|
|
953
1218
|
}
|
|
@@ -965,16 +1230,21 @@ function stripStaticDataPrefix(sec) {
|
|
|
965
1230
|
Array.isArray(child[2]) && SHIFTABLE.has(child[2][1]) &&
|
|
966
1231
|
Array.isArray(child[4]) && child[4][0] === 'i32.const' &&
|
|
967
1232
|
typeof child[4][1] === 'number' && child[4][1] >= prefix) {
|
|
968
|
-
|
|
1233
|
+
// SSO STRING: aux carries SSO_BIT, "offset" holds packed bytes — don't shift.
|
|
1234
|
+
const isSsoString = child[2][1] === PTR.STRING &&
|
|
1235
|
+
Array.isArray(child[3]) && child[3][0] === 'i32.const' &&
|
|
1236
|
+
typeof child[3][1] === 'number' && (child[3][1] & LAYOUT.SSO_BIT)
|
|
1237
|
+
if (!isSsoString) child[4][1] -= prefix
|
|
969
1238
|
} else if (child[0] === 'f64.const' &&
|
|
970
1239
|
typeof child[1] === 'string' && child[1].startsWith('nan:0x')) {
|
|
971
1240
|
const bits = BigInt(child[1].slice(4)) | 0x7FF0000000000000n
|
|
972
|
-
if (((bits >> 48n) & 0xFFF8n) ===
|
|
973
|
-
const ty = Number((bits >>
|
|
974
|
-
if (SHIFTABLE.has(ty)
|
|
975
|
-
|
|
1241
|
+
if (((bits >> 48n) & 0xFFF8n) === NAN_PREFIX) {
|
|
1242
|
+
const ty = Number((bits >> TAG_SHIFT_BIG) & TAG_MASK_BIG)
|
|
1243
|
+
if (SHIFTABLE.has(ty) &&
|
|
1244
|
+
!(ty === PTR.STRING && ((bits >> AUX_SHIFT_BIG) & SSO_BIT_BIG))) {
|
|
1245
|
+
const off = Number(bits & OFFSET_MASK_BIG)
|
|
976
1246
|
if (off >= prefix) {
|
|
977
|
-
const hi = bits & ~
|
|
1247
|
+
const hi = bits & ~OFFSET_MASK_BIG
|
|
978
1248
|
const newBits = hi | BigInt(off - prefix)
|
|
979
1249
|
child[1] = 'nan:0x' + newBits.toString(16).toUpperCase().padStart(16, '0')
|
|
980
1250
|
}
|
|
@@ -997,9 +1267,24 @@ export default function compile(ast, profiler) {
|
|
|
997
1267
|
ctx.func.names.clear()
|
|
998
1268
|
ctx.func.map.clear()
|
|
999
1269
|
for (const f of ctx.func.list) { ctx.func.names.add(f.name); ctx.func.map.set(f.name, f) }
|
|
1000
|
-
// Include imported functions for call resolution (e.g. template interpolations)
|
|
1001
|
-
|
|
1002
|
-
|
|
1270
|
+
// Include imported functions for call resolution (e.g. template interpolations).
|
|
1271
|
+
// Also register a synthesized sig in func.map so emit's arity-aware branches see
|
|
1272
|
+
// the import's declared param count — needed for arg pad/truncate to match it.
|
|
1273
|
+
for (const imp of ctx.module.imports) {
|
|
1274
|
+
if (imp[3]?.[0] !== 'func') continue
|
|
1275
|
+
const fname = imp[3][1].replace(/^\$/, '')
|
|
1276
|
+
ctx.func.names.add(fname)
|
|
1277
|
+
if (!ctx.func.map.has(fname)) {
|
|
1278
|
+
const params = []
|
|
1279
|
+
let result = 'f64'
|
|
1280
|
+
for (let k = 2; k < imp[3].length; k++) {
|
|
1281
|
+
const part = imp[3][k]
|
|
1282
|
+
if (Array.isArray(part) && part[0] === 'param') params.push({ type: part[1] || 'f64' })
|
|
1283
|
+
else if (Array.isArray(part) && part[0] === 'result') result = part[1] || 'f64'
|
|
1284
|
+
}
|
|
1285
|
+
ctx.func.map.set(fname, { name: fname, sig: { params, results: [result] } })
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1003
1288
|
|
|
1004
1289
|
// Check user globals don't conflict with runtime globals (modules loaded after user decls)
|
|
1005
1290
|
for (const name of ctx.scope.userGlobals)
|
|
@@ -1106,11 +1391,33 @@ export default function compile(ast, profiler) {
|
|
|
1106
1391
|
|
|
1107
1392
|
sec.funcs.push(...closureFuncs, ...funcs)
|
|
1108
1393
|
|
|
1394
|
+
// WASI command-mode entries (`run`, `_start`) must export as () -> ();
|
|
1395
|
+
// wasmtime/wasmer reject f64-returning functions under those names.
|
|
1396
|
+
// Parametric entries skip this — a CLI invocation has no way to supply args.
|
|
1397
|
+
if (ctx.transform.host === 'wasi') {
|
|
1398
|
+
const WASI_ENTRIES = new Set(['run', '_start'])
|
|
1399
|
+
for (const func of ctx.func.list) {
|
|
1400
|
+
if (!func.exported || !WASI_ENTRIES.has(func.name)) continue
|
|
1401
|
+
if (func.sig.params.length) continue
|
|
1402
|
+
const inner = isBoundaryWrapped(func) ? `$${func.name}$exp` : `$${func.name}`
|
|
1403
|
+
for (const f of sec.funcs) {
|
|
1404
|
+
if (f[1] === inner || f[1] === `$${func.name}`) {
|
|
1405
|
+
const expIdx = f.findIndex(n => Array.isArray(n) && n[0] === 'export')
|
|
1406
|
+
if (expIdx >= 0) f.splice(expIdx, 1)
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
sec.funcs.push(['func', `$${func.name}$wasi`, ['export', `"${func.name}"`],
|
|
1410
|
+
['drop', ['call', inner]]])
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1109
1414
|
if (ctx.closure.table?.length)
|
|
1110
1415
|
sec.elem.push(['elem', ['i32.const', 0], 'func', ...ctx.closure.table.map(n => `$${n}`)])
|
|
1111
1416
|
|
|
1112
1417
|
buildStartFn(ast, sec, closureFuncs, compilePendingClosures)
|
|
1113
1418
|
|
|
1419
|
+
syncImports(sec)
|
|
1420
|
+
|
|
1114
1421
|
dedupClosureBodies(closureFuncs, sec)
|
|
1115
1422
|
|
|
1116
1423
|
finalizeClosureTable(sec)
|
|
@@ -1164,6 +1471,28 @@ export default function compile(ast, profiler) {
|
|
|
1164
1471
|
if (restParamFuncs.length)
|
|
1165
1472
|
sec.customs.push(['@custom', '"jz:rest"', `"${JSON.stringify(restParamFuncs).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
1166
1473
|
|
|
1474
|
+
// Custom section: per-export i64 ABI map. Each entry describes an export
|
|
1475
|
+
// whose boundary wrapper carries NaN-boxed pointers via i64 (rather than
|
|
1476
|
+
// f64) to dodge V8's NaN canonicalization. Format: { name, p, r } where p
|
|
1477
|
+
// is an array of i64 param indices and r is 1 if result is i64. host.js
|
|
1478
|
+
// wrap() reinterprets BigInt↔f64 at i64 positions; numeric f64 positions
|
|
1479
|
+
// stay as Numbers on the JS side.
|
|
1480
|
+
const i64Exports = []
|
|
1481
|
+
for (const f of ctx.func.list) {
|
|
1482
|
+
if (!f.exported || !isBoundaryWrapped(f) || !f._exportUsesI64) continue
|
|
1483
|
+
const p = []
|
|
1484
|
+
f._exportI64Sig.params.forEach((b, i) => { if (b) p.push(i) })
|
|
1485
|
+
const r = f._exportI64Sig.result ? 1 : 0
|
|
1486
|
+
i64Exports.push({ name: f.name, p, r })
|
|
1487
|
+
// Aliases (export { foo as bar }) re-export the same wrapper under a
|
|
1488
|
+
// different JS-visible name; list each alias too so wrap() finds it.
|
|
1489
|
+
for (const [alias, val] of Object.entries(ctx.func.exports)) {
|
|
1490
|
+
if (val === f.name && alias !== f.name) i64Exports.push({ name: alias, p, r })
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (i64Exports.length)
|
|
1494
|
+
sec.customs.push(['@custom', '"jz:i64exp"', `"${JSON.stringify(i64Exports).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`])
|
|
1495
|
+
|
|
1167
1496
|
// Named export aliases: export { name } or export { source as alias }
|
|
1168
1497
|
for (const [name, val] of Object.entries(ctx.func.exports)) {
|
|
1169
1498
|
if (val === true) {
|