jz 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -26
- package/cli.js +53 -8
- package/index.js +14 -1
- package/module/array.js +66 -9
- package/module/collection.js +68 -35
- package/module/core.js +65 -19
- package/module/date.js +5 -0
- package/module/function.js +2 -1
- package/module/json.js +66 -8
- package/module/number.js +127 -5
- package/module/object.js +88 -1
- package/module/string.js +3 -2
- package/module/typedarray.js +7 -1
- package/package.json +3 -3
- package/src/analyze.js +16 -4
- package/src/assemble.js +544 -0
- package/src/ast.js +160 -0
- package/src/auto-config.js +120 -0
- package/src/autoload.js +4 -1
- package/src/compile.js +22 -620
- package/src/ctx.js +33 -5
- package/src/emit.js +73 -165
- package/src/host.js +12 -0
- package/src/ir.js +13 -0
- package/src/jzify.js +348 -9
- package/src/narrow.js +2 -1
- package/src/optimize.js +298 -24
- package/src/plan.js +220 -34
- package/src/prepare.js +22 -2
- package/src/vectorize.js +3 -12
package/src/compile.js
CHANGED
|
@@ -32,38 +32,31 @@ import {
|
|
|
32
32
|
analyzePtrUnboxable, typedElemAux, invalidateLocalsCache,
|
|
33
33
|
analyzeBoxedCaptures, updateRep, inferStringParams,
|
|
34
34
|
} from './analyze.js'
|
|
35
|
-
import { optimizeFunc,
|
|
35
|
+
import { optimizeFunc, treeshake } 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,
|
|
39
39
|
NULL_NAN, UNDEF_NAN, NULL_WAT, UNDEF_WAT, NULL_IR, UNDEF_IR, nullExpr, undefExpr,
|
|
40
|
-
MAX_CLOSURE_ARITY,
|
|
41
|
-
mkPtrIR,
|
|
42
|
-
isLit, litVal, isNullishLit,
|
|
43
|
-
temp,
|
|
44
|
-
keyValType, usesDynProps, needsDynShadow,
|
|
40
|
+
MAX_CLOSURE_ARITY,
|
|
41
|
+
mkPtrIR,
|
|
42
|
+
isLit, litVal, isNullishLit, emitNum,
|
|
43
|
+
temp,
|
|
45
44
|
isGlobal, isConst, boxedAddr, readVar, writeVar, isNullish, isUndef,
|
|
46
45
|
slotAddr, elemLoad, elemStore, arrayLoop, allocPtr,
|
|
47
46
|
multiCount, loopTop, flat, reconstructArgsWithSpreads,
|
|
48
|
-
valKindToPtr,
|
|
47
|
+
valKindToPtr, findBodyStart,
|
|
49
48
|
} from './ir.js'
|
|
50
49
|
import plan from './plan.js'
|
|
50
|
+
import {
|
|
51
|
+
buildStartFn, dedupClosureBodies, finalizeClosureTable,
|
|
52
|
+
pullStdlib, syncImports, optimizeModule, stripStaticDataPrefix,
|
|
53
|
+
} from './assemble.js'
|
|
51
54
|
|
|
52
55
|
const timePhase = (profiler, name, fn) => profiler ? profiler.time(name, fn) : fn()
|
|
53
56
|
|
|
54
57
|
// Per-compile func name set + map live on ctx.func.names / ctx.func.map,
|
|
55
58
|
// populated at compile() entry. Both reset by ctx.js reset() and re-filled here.
|
|
56
59
|
|
|
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)
|
|
66
|
-
|
|
67
60
|
// Low-level IR helpers previously lived here. Pure ones moved to src/ir.js;
|
|
68
61
|
// emit-calling ones (toBool, emitTypeofCmp, emitDecl, materializeMulti,
|
|
69
62
|
// buildArrayWithSpreads) moved to src/emit.js.
|
|
@@ -145,95 +138,6 @@ const tcoTailRewrite = (ir, resultType) => {
|
|
|
145
138
|
return ir
|
|
146
139
|
}
|
|
147
140
|
|
|
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
|
-
|
|
237
141
|
// === Module compilation ===
|
|
238
142
|
|
|
239
143
|
const cloneRepMap = map => map ? new Map([...map].map(([k, v]) => [k, { ...v }])) : null
|
|
@@ -748,515 +652,6 @@ function emitClosureBody(cb) {
|
|
|
748
652
|
return fn
|
|
749
653
|
}
|
|
750
654
|
|
|
751
|
-
/**
|
|
752
|
-
* Phase: build module-init function `__start`.
|
|
753
|
-
*
|
|
754
|
-
* `__start` is the WebAssembly start function: runs once at instantiation, after
|
|
755
|
-
* imports/globals are bound but before any export is called. It threads together
|
|
756
|
-
* everything that must happen before user code observes a ready module:
|
|
757
|
-
*
|
|
758
|
-
* 1. Reset per-function emit state (locals/repByLocal/boxed/stack) — __start is
|
|
759
|
-
* a fresh function context with no params.
|
|
760
|
-
* 2. analyzeValTypes(ast) so emit sees correct ptrKind on top-level decls.
|
|
761
|
-
* 3. Sub-module init (foreign module bootstrap) emits first — its globals
|
|
762
|
-
* must be assigned before main-module code reads them.
|
|
763
|
-
* 4. emit(ast) — user top-level statements (let/const, call expressions, …).
|
|
764
|
-
* 5. boxInit — auto-boxing globals (vars with prop assignments lifted to OBJECT).
|
|
765
|
-
* 6. schemaInit — runtime schema-name table for JSON.stringify.
|
|
766
|
-
* 7. strPoolInit — copy passive string-pool segment to heap (shared memory).
|
|
767
|
-
* 8. typeofInit — preallocate typeof-result string globals.
|
|
768
|
-
*
|
|
769
|
-
* Order in the assembled body: strPool → typeof → box → schema → moduleInits → init.
|
|
770
|
-
*
|
|
771
|
-
* Late closures (those compiled during __start emit, e.g. arrows declared at
|
|
772
|
-
* module scope) are flushed via `compilePendingClosures` and prepended to
|
|
773
|
-
* `sec.funcs` so closure indices stay stable across the table.
|
|
774
|
-
*/
|
|
775
|
-
function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
776
|
-
ctx.func.locals = new Map()
|
|
777
|
-
ctx.func.repByLocal = null
|
|
778
|
-
ctx.func.boxed = new Map()
|
|
779
|
-
ctx.func.stack = []
|
|
780
|
-
ctx.func.current = { params: [], results: [] }
|
|
781
|
-
analyzeValTypes(ast)
|
|
782
|
-
const normalizeIR = ir => !ir?.length ? [] : Array.isArray(ir[0]) ? ir : [ir]
|
|
783
|
-
|
|
784
|
-
const moduleInits = []
|
|
785
|
-
if (ctx.module.moduleInits) {
|
|
786
|
-
for (const mi of ctx.module.moduleInits) {
|
|
787
|
-
analyzeValTypes(mi)
|
|
788
|
-
moduleInits.push(...normalizeIR(emit(mi)))
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
const init = emit(ast)
|
|
792
|
-
|
|
793
|
-
const boxInit = []
|
|
794
|
-
if (ctx.schema.autoBox) {
|
|
795
|
-
const bt = `${T}box`
|
|
796
|
-
ctx.func.locals.set(bt, 'i32')
|
|
797
|
-
for (const [name, { schemaId, schema }] of ctx.schema.autoBox) {
|
|
798
|
-
inc('__alloc_hdr', '__mkptr')
|
|
799
|
-
boxInit.push(
|
|
800
|
-
['local.set', `$${bt}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)], ['i32.const', 8]]],
|
|
801
|
-
['f64.store', ['local.get', `$${bt}`],
|
|
802
|
-
ctx.func.names.has(name) ? ['f64.const', 0] : ['global.get', `$${name}`]],
|
|
803
|
-
...schema.slice(1).map((_, i) =>
|
|
804
|
-
['f64.store', ['i32.add', ['local.get', `$${bt}`], ['i32.const', (i + 1) * 8]], ['f64.const', 0]]),
|
|
805
|
-
['global.set', `$${name}`, mkPtrIR(PTR.OBJECT, schemaId, ['local.get', `$${bt}`])])
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
const schemaInit = []
|
|
810
|
-
// Schema name table is needed by JSON.stringify (legacy), and by __dyn_get's
|
|
811
|
-
// OBJECT-schema fallback for polymorphic-receiver `.prop` access. Lift the
|
|
812
|
-
// gate to also populate when any __dyn_get* family helper is included so
|
|
813
|
-
// polymorphic OBJECT patterns (mismatched-schema `?:`, unknown-schema
|
|
814
|
-
// params) resolve via runtime aux→sid lookup. Direct dependents of
|
|
815
|
-
// __dyn_get (set transitively by resolveIncludes() later) are listed
|
|
816
|
-
// explicitly here because the dep graph hasn't been expanded yet at
|
|
817
|
-
// start-fn build time.
|
|
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 && (
|
|
824
|
-
ctx.core.includes.has('__stringify') ||
|
|
825
|
-
ctx.core.includes.has('__dyn_get') ||
|
|
826
|
-
ctx.core.includes.has('__dyn_get_t') ||
|
|
827
|
-
ctx.core.includes.has('__dyn_get_any') ||
|
|
828
|
-
ctx.core.includes.has('__dyn_get_any_t') ||
|
|
829
|
-
ctx.core.includes.has('__dyn_get_expr') ||
|
|
830
|
-
ctx.core.includes.has('__dyn_get_expr_t') ||
|
|
831
|
-
ctx.core.includes.has('__dyn_get_or'))) ||
|
|
832
|
-
hasJpObj
|
|
833
|
-
if (needsSchemaTbl) {
|
|
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
|
|
838
|
-
const stbl = `${T}stbl`
|
|
839
|
-
const sarr = `${T}sarr`
|
|
840
|
-
ctx.func.locals.set(stbl, 'i32')
|
|
841
|
-
ctx.func.locals.set(sarr, 'i32')
|
|
842
|
-
inc('__alloc', '__alloc_hdr', '__mkptr')
|
|
843
|
-
schemaInit.push(
|
|
844
|
-
['local.set', `$${stbl}`, ['call', '$__alloc', ['i32.const', (nSchemas + runtimeReserve) * 8]]],
|
|
845
|
-
['global.set', '$__schema_tbl', ['local.get', `$${stbl}`]])
|
|
846
|
-
if (runtimeReserve) {
|
|
847
|
-
schemaInit.push(['global.set', '$__schema_next', ['i32.const', nSchemas]])
|
|
848
|
-
}
|
|
849
|
-
for (let s = 0; s < nSchemas; s++) {
|
|
850
|
-
const keys = ctx.schema.list[s]
|
|
851
|
-
const n = keys.length
|
|
852
|
-
schemaInit.push(
|
|
853
|
-
['local.set', `$${sarr}`, ['call', '$__alloc_hdr', ['i32.const', n], ['i32.const', n], ['i32.const', 8]]])
|
|
854
|
-
for (let k = 0; k < n; k++)
|
|
855
|
-
schemaInit.push(
|
|
856
|
-
['f64.store', ['i32.add', ['local.get', `$${sarr}`], ['i32.const', k * 8]],
|
|
857
|
-
emit(['str', String(keys[k])])])
|
|
858
|
-
schemaInit.push(
|
|
859
|
-
['f64.store', ['i32.add', ['local.get', `$${stbl}`], ['i32.const', s * 8]],
|
|
860
|
-
mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${sarr}`])])
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
const strPoolInit = []
|
|
865
|
-
if (ctx.runtime.strPool) {
|
|
866
|
-
const total = ctx.runtime.strPool.length
|
|
867
|
-
strPoolInit.push(
|
|
868
|
-
['global.set', '$__strBase', ['call', '$__alloc', ['i32.const', total]]],
|
|
869
|
-
['memory.init', '$__strPool', ['global.get', '$__strBase'], ['i32.const', 0], ['i32.const', total]],
|
|
870
|
-
['data.drop', '$__strPool'],
|
|
871
|
-
)
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
const typeofInit = []
|
|
875
|
-
if (ctx.runtime.typeofStrs) {
|
|
876
|
-
for (const s of ctx.runtime.typeofStrs)
|
|
877
|
-
typeofInit.push(['global.set', `$__tof_${s}`, emit(['str', s])])
|
|
878
|
-
}
|
|
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) {
|
|
883
|
-
const initIR = normalizeIR(init)
|
|
884
|
-
const startFn = ['func', '$__start']
|
|
885
|
-
for (const [l, t] of ctx.func.locals) startFn.push(['local', `$${l}`, t])
|
|
886
|
-
startFn.push(...strPoolInit, ...typeofInit, ...boxInit, ...schemaInit,
|
|
887
|
-
...(wasiTimers ? [['call', '$__timer_init']] : []),
|
|
888
|
-
...moduleInits, ...initIR,
|
|
889
|
-
...(ctx.features.blockingTimers ? [['call', '$__timer_loop']] : []),
|
|
890
|
-
)
|
|
891
|
-
sec.start.push(startFn, ['start', '$__start'])
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
const beforeLen = closureFuncs.length
|
|
895
|
-
compilePendingClosures()
|
|
896
|
-
if (closureFuncs.length > beforeLen)
|
|
897
|
-
sec.funcs.unshift(...closureFuncs.slice(beforeLen))
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
/**
|
|
901
|
-
* Phase: closure-body dedup.
|
|
902
|
-
*
|
|
903
|
-
* Two closures with structurally-equal bodies (same shape after alpha-renaming
|
|
904
|
-
* locals/params) are emitted as a single function — duplicates redirect through
|
|
905
|
-
* the elem table to the canonical name. Closure bodies often share shape because
|
|
906
|
-
* the same inner arrow can be instantiated in many places (e.g. parser combinators).
|
|
907
|
-
*
|
|
908
|
-
* IN: closureFuncs (the WAT IR list emitted by emitClosureBody),
|
|
909
|
-
* sec.funcs (already contains closureFuncs + regular funcs),
|
|
910
|
-
* ctx.closure.table (elem-section names).
|
|
911
|
-
* OUT: sec.funcs filtered to canonical bodies, ctx.closure.table redirected.
|
|
912
|
-
*
|
|
913
|
-
* Runs AFTER all closures (including those compiled during __start emit) are
|
|
914
|
-
* collected so structural duplicates across batches collapse together.
|
|
915
|
-
*/
|
|
916
|
-
function dedupClosureBodies(closureFuncs, sec) {
|
|
917
|
-
if (closureFuncs.length <= 1) return
|
|
918
|
-
const canonicalize = (fn) => {
|
|
919
|
-
const localNames = new Set()
|
|
920
|
-
const collect = (node) => {
|
|
921
|
-
if (!Array.isArray(node)) return
|
|
922
|
-
if ((node[0] === 'local' || node[0] === 'param') && typeof node[1] === 'string' && node[1][0] === '$')
|
|
923
|
-
localNames.add(node[1])
|
|
924
|
-
for (const c of node) collect(c)
|
|
925
|
-
}
|
|
926
|
-
collect(fn)
|
|
927
|
-
let counter = 0
|
|
928
|
-
const renameMap = new Map()
|
|
929
|
-
const walk = node => {
|
|
930
|
-
if (typeof node === 'string') {
|
|
931
|
-
if (!localNames.has(node)) return node
|
|
932
|
-
let r = renameMap.get(node)
|
|
933
|
-
if (!r) { r = `$_c${counter++}`; renameMap.set(node, r) }
|
|
934
|
-
return r
|
|
935
|
-
}
|
|
936
|
-
if (!Array.isArray(node)) return node
|
|
937
|
-
return node.map(walk)
|
|
938
|
-
}
|
|
939
|
-
return JSON.stringify(['func', ...fn.slice(2).map(walk)])
|
|
940
|
-
}
|
|
941
|
-
const hashToName = new Map()
|
|
942
|
-
const redirect = new Map()
|
|
943
|
-
const keepSet = new Set()
|
|
944
|
-
for (const fn of closureFuncs) {
|
|
945
|
-
const key = canonicalize(fn)
|
|
946
|
-
const name = fn[1].slice(1)
|
|
947
|
-
const canonical = hashToName.get(key)
|
|
948
|
-
if (canonical) redirect.set(name, canonical)
|
|
949
|
-
else { hashToName.set(key, name); keepSet.add(name) }
|
|
950
|
-
}
|
|
951
|
-
if (!redirect.size) return
|
|
952
|
-
// Remove duplicate declarations BEFORE rewriting references — otherwise redirectRefs
|
|
953
|
-
// renames the declaration itself and the filter can't find the original name.
|
|
954
|
-
const kept = sec.funcs.filter(fn => {
|
|
955
|
-
if (!Array.isArray(fn) || fn[0] !== 'func') return true
|
|
956
|
-
const name = typeof fn[1] === 'string' && fn[1][0] === '$' ? fn[1].slice(1) : null
|
|
957
|
-
return !name || !redirect.has(name)
|
|
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)
|
|
967
|
-
sec.funcs.length = 0
|
|
968
|
-
sec.funcs.push(...kept)
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
/**
|
|
972
|
-
* Phase: closure-table finalize + ABI shrink.
|
|
973
|
-
*
|
|
974
|
-
* Two opportunities, both gated on a post-emit scan for `call_indirect`:
|
|
975
|
-
*
|
|
976
|
-
* 1. Drop dead $ftN type / table / elem when the scan finds zero call_indirect
|
|
977
|
-
* sites (every closure call was direct-dispatched via A3 + capture-boundary
|
|
978
|
-
* propagation, AND no top-level fn was taken as a value). Closure pointers
|
|
979
|
-
* still carry funcIdx in their NaN-box aux bits, but those bits become dead
|
|
980
|
-
* state with no reader.
|
|
981
|
-
*
|
|
982
|
-
* 2. Per-body ABI shrink: with no call_indirect, every closure is direct-only,
|
|
983
|
-
* so the uniform `(env, argc, a0..a{W-1})` ABI is no longer required.
|
|
984
|
-
* Each body sheds:
|
|
985
|
-
* • $__env when captures.length === 0
|
|
986
|
-
* • $__argc when no rest param (defaults check param value, not argc)
|
|
987
|
-
* • $__a{i} for i ≥ fixedN when no rest (caller's UNDEF padding is dead)
|
|
988
|
-
* Rest closures keep all W slots — argc + slot{fixedN..W-1} are how rest packs.
|
|
989
|
-
* Both `call` and `return_call` (tail call) sites are rewritten in the same walk.
|
|
990
|
-
*/
|
|
991
|
-
function finalizeClosureTable(sec) {
|
|
992
|
-
let indirectUsed = ctx.transform.host === 'wasi'
|
|
993
|
-
const scan = (n) => {
|
|
994
|
-
if (!Array.isArray(n) || indirectUsed) return
|
|
995
|
-
if (n[0] === 'call_indirect') { indirectUsed = true; return }
|
|
996
|
-
for (const c of n) if (Array.isArray(c)) scan(c)
|
|
997
|
-
}
|
|
998
|
-
for (const fn of sec.funcs) { scan(fn); if (indirectUsed) break }
|
|
999
|
-
if (!indirectUsed) for (const fn of sec.start) scan(fn)
|
|
1000
|
-
// Also scan raw stdlib strings (pullStdlib hasn't run yet, so stdlib funcs aren't in sec.funcs)
|
|
1001
|
-
if (!indirectUsed) for (const s of Object.keys(ctx.core.stdlib)) {
|
|
1002
|
-
if (ctx.core.stdlib[s]?.includes?.('call_indirect')) { indirectUsed = true; break }
|
|
1003
|
-
}
|
|
1004
|
-
// Keep table if call_indirect is used (closures, timer dispatch, etc.)
|
|
1005
|
-
if (indirectUsed) {
|
|
1006
|
-
if (!ctx.closure.table) ctx.closure.table = []
|
|
1007
|
-
sec.table = [['table', ['export', '"__jz_table"'], ctx.closure.table.length, 'funcref']]
|
|
1008
|
-
sec.elem = ctx.closure.table.length ? [['elem', ['i32.const', 0], 'func', ...ctx.closure.table.map(n => `$${n}`)]] : []
|
|
1009
|
-
return
|
|
1010
|
-
}
|
|
1011
|
-
sec.table = []
|
|
1012
|
-
sec.elem = []
|
|
1013
|
-
sec.types = sec.types.filter(t => !(Array.isArray(t) && t[1] === '$ftN'))
|
|
1014
|
-
const W = ctx.closure.width ?? MAX_CLOSURE_ARITY
|
|
1015
|
-
const abiOf = new Map()
|
|
1016
|
-
for (const cb of (ctx.closure.bodies || [])) {
|
|
1017
|
-
const fixedN = cb.params.length - (cb.rest ? 1 : 0)
|
|
1018
|
-
abiOf.set(cb.name, {
|
|
1019
|
-
needEnv: cb.captures.length > 0,
|
|
1020
|
-
needArgc: !!cb.rest,
|
|
1021
|
-
usedSlots: cb.rest ? W : fixedN,
|
|
1022
|
-
rest: !!cb.rest,
|
|
1023
|
-
})
|
|
1024
|
-
}
|
|
1025
|
-
for (const fn of sec.funcs) {
|
|
1026
|
-
if (!Array.isArray(fn) || fn[0] !== 'func') continue
|
|
1027
|
-
const fnName = typeof fn[1] === 'string' && fn[1][0] === '$' ? fn[1].slice(1) : null
|
|
1028
|
-
const abi = abiOf.get(fnName)
|
|
1029
|
-
if (!abi) continue
|
|
1030
|
-
for (let i = fn.length - 1; i >= 0; i--) {
|
|
1031
|
-
const node = fn[i]
|
|
1032
|
-
if (!Array.isArray(node) || node[0] !== 'param') continue
|
|
1033
|
-
const pname = node[1]
|
|
1034
|
-
if (pname === '$__env' && !abi.needEnv) fn.splice(i, 1)
|
|
1035
|
-
else if (pname === '$__argc' && !abi.needArgc) fn.splice(i, 1)
|
|
1036
|
-
else if (typeof pname === 'string' && pname.startsWith('$__a') && !abi.rest) {
|
|
1037
|
-
const idx = parseInt(pname.slice(4), 10)
|
|
1038
|
-
if (Number.isFinite(idx) && idx >= abi.usedSlots) fn.splice(i, 1)
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
const rewriteCalls = (node) => {
|
|
1043
|
-
if (!Array.isArray(node)) return
|
|
1044
|
-
for (const c of node) if (Array.isArray(c)) rewriteCalls(c)
|
|
1045
|
-
if ((node[0] === 'call' || node[0] === 'return_call') && typeof node[1] === 'string') {
|
|
1046
|
-
const callee = node[1].slice(1)
|
|
1047
|
-
const abi = abiOf.get(callee)
|
|
1048
|
-
if (!abi) return
|
|
1049
|
-
const newArgs = []
|
|
1050
|
-
if (abi.needEnv) newArgs.push(node[2])
|
|
1051
|
-
if (abi.needArgc) newArgs.push(node[3])
|
|
1052
|
-
for (let i = 0; i < abi.usedSlots; i++) newArgs.push(node[4 + i])
|
|
1053
|
-
node.splice(2, node.length - 2, ...newArgs)
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
for (const fn of sec.funcs) rewriteCalls(fn)
|
|
1057
|
-
for (const fn of sec.start) rewriteCalls(fn)
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
/**
|
|
1061
|
-
* Phase: pull stdlib + memory.
|
|
1062
|
-
*
|
|
1063
|
-
* Runs AFTER __start is built — emit calls during __start (e.g. typeofStrs,
|
|
1064
|
-
* boxInit, schemaInit) trigger `inc()` for any helpers they need, and those
|
|
1065
|
-
* additions must be observed before resolveIncludes() expands the dependency
|
|
1066
|
-
* closure.
|
|
1067
|
-
*
|
|
1068
|
-
* Steps:
|
|
1069
|
-
* 1. resolveIncludes() — close the include set under stdlib dependencies.
|
|
1070
|
-
* 2. Emit memory section ONLY when some included helper uses memory ops
|
|
1071
|
-
* (G optimization: pure scalar programs ship without memory + __heap).
|
|
1072
|
-
* When memory is needed, the allocator (__alloc + __alloc_hdr + __clear)
|
|
1073
|
-
* is force-included since stdlib funcs may call into it.
|
|
1074
|
-
* 3. Pull external (host) stdlibs into sec.extStdlib (must precede normal
|
|
1075
|
-
* imports in the module byte order).
|
|
1076
|
-
* 4. Pull resolved factory stdlibs (those keyed by feature gates) into
|
|
1077
|
-
* sec.stdlib via parseWat.
|
|
1078
|
-
*
|
|
1079
|
-
* Also reports any unresolved stdlib name (logged, not thrown — keeps test
|
|
1080
|
-
* output readable when a missing helper is the actual bug).
|
|
1081
|
-
*/
|
|
1082
|
-
function pullStdlib(sec) {
|
|
1083
|
-
resolveIncludes()
|
|
1084
|
-
|
|
1085
|
-
const needsMemory = [...ctx.core.includes].some(n => ctx.core.stdlib[n] && MEM_OPS.test(ctx.core.stdlib[n]))
|
|
1086
|
-
if (!needsMemory) ctx.scope.globals.delete('__heap')
|
|
1087
|
-
if (needsMemory && ctx.module.modules.core) {
|
|
1088
|
-
for (const fn of ['__alloc', '__alloc_hdr', '__clear']) if (!ctx.core.includes.has(fn)) ctx.core.includes.add(fn)
|
|
1089
|
-
const pages = ctx.memory.pages || 1
|
|
1090
|
-
if (ctx.memory.shared) sec.imports.push(['import', '"env"', '"memory"', ['memory', pages]])
|
|
1091
|
-
else sec.memory.push(['memory', ['export', '"memory"'], pages])
|
|
1092
|
-
if (ctx.transform.alloc !== false && ctx.core._allocRawFuncs)
|
|
1093
|
-
sec.funcs.push(...ctx.core._allocRawFuncs.map(s => parseWat(s)))
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
const stdlibStr = (name) => {
|
|
1097
|
-
const v = ctx.core.stdlib[name]
|
|
1098
|
-
return typeof v === 'function' ? v() : v
|
|
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()
|
|
1104
|
-
for (const name of Object.keys(ctx.core.stdlib)) {
|
|
1105
|
-
if (name.startsWith('__ext_') && ctx.core.includes.has(name)) {
|
|
1106
|
-
const parsed = parseWat(stdlibStr(name))
|
|
1107
|
-
sec.extStdlib.push(parsed[0] === "module" ? parsed[1] : parsed)
|
|
1108
|
-
ctx.core.extImports.add(name)
|
|
1109
|
-
ctx.core.includes.delete(name)
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
for (const n of ctx.core.includes) if (!ctx.core.stdlib[n]) console.error("MISSING stdlib:", n)
|
|
1113
|
-
sec.stdlib.push(...[...ctx.core.includes].map(n => parseWat(stdlibStr(n))))
|
|
1114
|
-
}
|
|
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
|
-
|
|
1122
|
-
/**
|
|
1123
|
-
* Phase: whole-module + per-function optimization passes.
|
|
1124
|
-
*
|
|
1125
|
-
* Order matters and is non-obvious — fixed deliberately:
|
|
1126
|
-
*
|
|
1127
|
-
* 1. specializeMkptr — replaces `call $__mkptr (T, A, off)` with `$__mkptr_T_A_d`
|
|
1128
|
-
* for known (T, A) pairs (saves ~4 B/site). Must run BEFORE per-function
|
|
1129
|
-
* passes so the new variants exist when fusedRewrite folds calls into them.
|
|
1130
|
-
* 2. specializePtrBase — folds `call F (add (global G) const)` to a `_p`
|
|
1131
|
-
* variant (saves ~3 B/site). After specializeMkptr so mkptr variants
|
|
1132
|
-
* ($__mkptr_T_A_d) are visible to it.
|
|
1133
|
-
* 3. sortStrPoolByFreq — reorders string-pool entries so hot strings get low
|
|
1134
|
-
* offsets (shrinking i32.const LEB128). Shared-memory only (passive segment).
|
|
1135
|
-
* 4. optimizeFunc per fn — hoistPtrType + fusedRewrite + sortLocalsByUse.
|
|
1136
|
-
* Must run after specializeMkptr/specializePtrBase introduce new helpers.
|
|
1137
|
-
* 5. hoistConstantPool — repeated f64 literals → mutable globals.
|
|
1138
|
-
* Last because earlier passes might fold/eliminate constants.
|
|
1139
|
-
*
|
|
1140
|
-
* Also adjusts $__heap base when data segment exceeds 1024 bytes (default
|
|
1141
|
-
* heap base) — keeps user code at offset 0 from clobbering the data segment.
|
|
1142
|
-
*/
|
|
1143
|
-
function optimizeModule(sec) {
|
|
1144
|
-
const cfg = ctx.transform.optimize // null → all on (back-compat for direct compile() callers)
|
|
1145
|
-
if (!cfg || cfg.specializeMkptr !== false)
|
|
1146
|
-
specializeMkptr([...sec.funcs, ...sec.stdlib, ...sec.start], wat => sec.stdlib.push(parseWat(wat)), parseWat)
|
|
1147
|
-
if (!cfg || cfg.specializePtrBase !== false)
|
|
1148
|
-
specializePtrBase([...sec.funcs, ...sec.stdlib, ...sec.start], wat => sec.stdlib.push(parseWat(wat)), parseWat)
|
|
1149
|
-
if (ctx.runtime.strPool && (!cfg || cfg.sortStrPoolByFreq !== false)) {
|
|
1150
|
-
const poolRef = { pool: ctx.runtime.strPool }
|
|
1151
|
-
sortStrPoolByFreq([...sec.funcs, ...sec.stdlib, ...sec.start], poolRef, ctx.runtime.strPoolDedup)
|
|
1152
|
-
ctx.runtime.strPool = poolRef.pool
|
|
1153
|
-
}
|
|
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
|
-
}
|
|
1168
|
-
if (!cfg || cfg.hoistConstantPool !== false)
|
|
1169
|
-
hoistConstantPool([...sec.funcs, ...sec.stdlib, ...sec.start], (name, wat) => ctx.scope.globals.set(name, wat))
|
|
1170
|
-
|
|
1171
|
-
const dataLen = ctx.runtime.data?.length || 0
|
|
1172
|
-
if (dataLen > 1024 && !ctx.memory.shared) {
|
|
1173
|
-
const heapBase = (dataLen + 7) & ~7
|
|
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}))`)
|
|
1177
|
-
for (const s of sec.stdlib)
|
|
1178
|
-
if (s[0] === 'func' && s[1] === '$__clear')
|
|
1179
|
-
for (let i = 2; i < s.length; i++)
|
|
1180
|
-
if (Array.isArray(s[i]) && s[i][0] === 'global.set' && Array.isArray(s[i][2]) && s[i][2][0] === 'i32.const')
|
|
1181
|
-
s[i][2][1] = `${heapBase}`
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
/**
|
|
1186
|
-
* Phase: strip static-data prefix.
|
|
1187
|
-
*
|
|
1188
|
-
* R: when `__static_str` runtime helper isn't included, the leading prefix of the
|
|
1189
|
-
* data segment (the static string-table header) is dead — strip it and shift all
|
|
1190
|
-
* pointer offsets in user code, embedded data slots, and constant-folded NaN-box
|
|
1191
|
-
* literals down by `prefix` bytes. ATOM/SSO have no offset, so they're unaffected.
|
|
1192
|
-
*
|
|
1193
|
-
* Patches both runtime-call form (`__mkptr(T, A, off)`) and the constant-folded
|
|
1194
|
-
* form (`f64.reinterpret_i64 (i64.const ...)`) when offset >= prefix.
|
|
1195
|
-
*/
|
|
1196
|
-
function stripStaticDataPrefix(sec) {
|
|
1197
|
-
if (!ctx.runtime.staticDataLen || ctx.core.includes.has('__static_str')) return
|
|
1198
|
-
const prefix = ctx.runtime.staticDataLen
|
|
1199
|
-
const SHIFTABLE = new Set([PTR.STRING, PTR.OBJECT, PTR.ARRAY, PTR.HASH, PTR.SET, PTR.MAP, PTR.BUFFER, PTR.TYPED, PTR.CLOSURE])
|
|
1200
|
-
const data = ctx.runtime.data || ''
|
|
1201
|
-
const buf = new Uint8Array(data.length)
|
|
1202
|
-
for (let i = 0; i < data.length; i++) buf[i] = data.charCodeAt(i)
|
|
1203
|
-
const dv = new DataView(buf.buffer)
|
|
1204
|
-
if (ctx.runtime.staticPtrSlots) {
|
|
1205
|
-
for (const slotOff of ctx.runtime.staticPtrSlots) {
|
|
1206
|
-
if (slotOff < prefix) continue
|
|
1207
|
-
const bits = dv.getBigUint64(slotOff, true)
|
|
1208
|
-
if (((bits >> 48n) & 0xFFF8n) !== NAN_PREFIX) continue
|
|
1209
|
-
const ty = Number((bits >> TAG_SHIFT_BIG) & TAG_MASK_BIG)
|
|
1210
|
-
if (!SHIFTABLE.has(ty)) continue
|
|
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)
|
|
1214
|
-
if (off < prefix) continue
|
|
1215
|
-
const hi = bits & ~OFFSET_MASK_BIG
|
|
1216
|
-
dv.setBigUint64(slotOff, hi | BigInt(off - prefix), true)
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
let s = ''
|
|
1220
|
-
for (let i = prefix; i < buf.length; i++) s += String.fromCharCode(buf[i])
|
|
1221
|
-
ctx.runtime.data = s
|
|
1222
|
-
if (ctx.runtime.staticPtrSlots) ctx.runtime.staticPtrSlots = ctx.runtime.staticPtrSlots
|
|
1223
|
-
.filter(o => o >= prefix).map(o => o - prefix)
|
|
1224
|
-
const shift = (node) => {
|
|
1225
|
-
if (!Array.isArray(node)) return
|
|
1226
|
-
for (let i = 0; i < node.length; i++) {
|
|
1227
|
-
const child = node[i]
|
|
1228
|
-
if (!Array.isArray(child)) continue
|
|
1229
|
-
if (child[0] === 'call' && child[1] === '$__mkptr' &&
|
|
1230
|
-
Array.isArray(child[2]) && SHIFTABLE.has(child[2][1]) &&
|
|
1231
|
-
Array.isArray(child[4]) && child[4][0] === 'i32.const' &&
|
|
1232
|
-
typeof child[4][1] === 'number' && child[4][1] >= prefix) {
|
|
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
|
|
1238
|
-
} else if (child[0] === 'f64.const' &&
|
|
1239
|
-
typeof child[1] === 'string' && child[1].startsWith('nan:0x')) {
|
|
1240
|
-
const bits = BigInt(child[1].slice(4)) | 0x7FF0000000000000n
|
|
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)
|
|
1246
|
-
if (off >= prefix) {
|
|
1247
|
-
const hi = bits & ~OFFSET_MASK_BIG
|
|
1248
|
-
const newBits = hi | BigInt(off - prefix)
|
|
1249
|
-
child[1] = 'nan:0x' + newBits.toString(16).toUpperCase().padStart(16, '0')
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
shift(child)
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
for (const s of [...sec.funcs, ...sec.stdlib, ...sec.start]) shift(s)
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
655
|
/**
|
|
1261
656
|
* Compile prepared AST to WASM module IR.
|
|
1262
657
|
* @param {import('./prepare.js').ASTNode} ast - Prepared AST
|
|
@@ -1394,20 +789,26 @@ export default function compile(ast, profiler) {
|
|
|
1394
789
|
// WASI command-mode entries (`run`, `_start`) must export as () -> ();
|
|
1395
790
|
// wasmtime/wasmer reject f64-returning functions under those names.
|
|
1396
791
|
// Parametric entries skip this — a CLI invocation has no way to supply args.
|
|
792
|
+
const wasiCommandExports = new Set()
|
|
1397
793
|
if (ctx.transform.host === 'wasi') {
|
|
1398
794
|
const WASI_ENTRIES = new Set(['run', '_start'])
|
|
1399
|
-
for (const
|
|
1400
|
-
if (!
|
|
795
|
+
for (const [exportName, val] of Object.entries(ctx.func.exports)) {
|
|
796
|
+
if (!WASI_ENTRIES.has(exportName)) continue
|
|
797
|
+
const targetName = val === true ? exportName : val
|
|
798
|
+
if (typeof targetName !== 'string') continue
|
|
799
|
+
const func = ctx.func.list.find(f => f.name === targetName)
|
|
800
|
+
if (!func) continue
|
|
1401
801
|
if (func.sig.params.length) continue
|
|
1402
|
-
const inner = isBoundaryWrapped(func) ? `$${
|
|
802
|
+
const inner = isBoundaryWrapped(func) ? `$${targetName}$exp` : `$${targetName}`
|
|
1403
803
|
for (const f of sec.funcs) {
|
|
1404
|
-
if (f[1] === inner || f[1] === `$${
|
|
804
|
+
if (f[1] === inner || f[1] === `$${targetName}`) {
|
|
1405
805
|
const expIdx = f.findIndex(n => Array.isArray(n) && n[0] === 'export')
|
|
1406
806
|
if (expIdx >= 0) f.splice(expIdx, 1)
|
|
1407
807
|
}
|
|
1408
808
|
}
|
|
1409
|
-
sec.funcs.push(['func', `$${
|
|
809
|
+
sec.funcs.push(['func', `$${exportName}$wasi`, ['export', `"${exportName}"`],
|
|
1410
810
|
['drop', ['call', inner]]])
|
|
811
|
+
wasiCommandExports.add(exportName)
|
|
1411
812
|
}
|
|
1412
813
|
}
|
|
1413
814
|
|
|
@@ -1495,6 +896,7 @@ export default function compile(ast, profiler) {
|
|
|
1495
896
|
|
|
1496
897
|
// Named export aliases: export { name } or export { source as alias }
|
|
1497
898
|
for (const [name, val] of Object.entries(ctx.func.exports)) {
|
|
899
|
+
if (wasiCommandExports.has(name)) continue
|
|
1498
900
|
if (val === true) {
|
|
1499
901
|
if (ctx.scope.userGlobals?.has(name)) sec.customs.push(['export', `"${name}"`, ['global', `$${name}`]])
|
|
1500
902
|
continue
|