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/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, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, treeshake, arenaRewindModule } from './optimize.js'
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, MEM_OPS, WASM_OPS, SPREAD_MUTATORS, BOXED_MUTATORS,
41
- mkPtrIR, ptrOffsetIR, ptrTypeIR, extractF64Bits, appendStaticSlots,
42
- isLit, litVal, isNullishLit, isPureIR, isPostfix, emitNum,
43
- temp, tempI32, tempI64, f64rem, toNumF64, truthyIR, toBoolFromEmitted,
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 func of ctx.func.list) {
1400
- if (!func.exported || !WASI_ENTRIES.has(func.name)) continue
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) ? `$${func.name}$exp` : `$${func.name}`
802
+ const inner = isBoundaryWrapped(func) ? `$${targetName}$exp` : `$${targetName}`
1403
803
  for (const f of sec.funcs) {
1404
- if (f[1] === inner || f[1] === `$${func.name}`) {
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', `$${func.name}$wasi`, ['export', `"${func.name}"`],
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