jz 0.2.1 → 0.3.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 +6 -25
- package/cli.js +19 -2
- 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 +306 -7
- 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/analyze.js
CHANGED
|
@@ -33,11 +33,27 @@ export const STMT_OPS = new Set([';', 'let', 'const', 'return', 'if', 'for', 'fo
|
|
|
33
33
|
'=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??=',
|
|
34
34
|
'throw', 'try', 'catch', 'finally', '++', '--', '()'])
|
|
35
35
|
|
|
36
|
+
/** Assignment operators — shared across analyze, plan, emit. */
|
|
37
|
+
export const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
|
|
38
|
+
|
|
36
39
|
/** Distinguish a function block body `{ ... }` from an expression-bodied object literal `({a:1})`.
|
|
37
40
|
* Both share the `'{}'` op tag; blocks have a non-`':'` first child (object literals start with key:val pairs). */
|
|
38
41
|
export const isBlockBody = (body) =>
|
|
39
42
|
Array.isArray(body) && body[0] === '{}' && body[1]?.[0] !== ':'
|
|
40
43
|
|
|
44
|
+
/** Extract integer value from AST literal node. Returns null if not a 32-bit integer. */
|
|
45
|
+
export function intLiteralValue(expr) {
|
|
46
|
+
let v = null
|
|
47
|
+
if (typeof expr === 'number') v = expr
|
|
48
|
+
else if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number') v = expr[1]
|
|
49
|
+
else if (Array.isArray(expr) && expr[0] === 'u-' && typeof expr[1] === 'number') v = -expr[1]
|
|
50
|
+
else if (typeof expr === 'string') v = repOf(expr)?.intConst ?? ctx.scope.constInts?.get(expr) ?? null
|
|
51
|
+
return v != null && Number.isInteger(v) && v >= -2147483648 && v <= 2147483647 ? v : null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Non-negative integer literal — used for string/typed-array index bounds. */
|
|
55
|
+
export const nonNegIntLiteral = (node) => { const n = intLiteralValue(node); return n != null && n >= 0 ? n : null }
|
|
56
|
+
|
|
41
57
|
/** Collect all `return X` expressions (X != null) from a function body, skipping nested arrow funcs.
|
|
42
58
|
* Pushes into `out`. Non-returning paths are silently skipped — pair with `alwaysReturns` if total
|
|
43
59
|
* coverage matters, or with `hasBareReturn` to detect `return;` (undef result). */
|
|
@@ -1892,8 +1908,6 @@ export function analyzePtrUnboxable(body, locals, boxed) {
|
|
|
1892
1908
|
for (const a of args) collect(a)
|
|
1893
1909
|
}
|
|
1894
1910
|
|
|
1895
|
-
const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=',
|
|
1896
|
-
'<<=', '>>=', '>>>=', '||=', '&&=', '??='])
|
|
1897
1911
|
const NULL_CMP_OPS = new Set(['==', '!=', '===', '!=='])
|
|
1898
1912
|
|
|
1899
1913
|
function check(node) {
|
|
@@ -2035,8 +2049,6 @@ export function findFreeVars(node, bound, free, scope) {
|
|
|
2035
2049
|
for (const a of args) findFreeVars(a, bound, free, scope)
|
|
2036
2050
|
}
|
|
2037
2051
|
|
|
2038
|
-
const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
|
|
2039
|
-
|
|
2040
2052
|
/** Check if any of the given variable names are assigned anywhere in the AST. */
|
|
2041
2053
|
export function findMutations(node, names, mutated) {
|
|
2042
2054
|
if (node == null || typeof node !== 'object' || !Array.isArray(node)) return
|
package/src/assemble.js
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module assembly — WAT section construction, optimization, and finalization.
|
|
3
|
+
*
|
|
4
|
+
* # Stage contract
|
|
5
|
+
* IN: per-function WAT IR (from emit), ctx state (includes, scope, closure, etc.)
|
|
6
|
+
* OUT: assembled module sections via the `sec` object, mutated in place.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from compile.js to separate "per-function compilation" from
|
|
9
|
+
* "module assembly" concerns. All functions receive `sec` (the named-slots
|
|
10
|
+
* section accumulator) and read/write ctx state as needed.
|
|
11
|
+
*
|
|
12
|
+
* @module assemble
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { parse as parseWat } from 'watr'
|
|
16
|
+
import { ctx, inc, resolveIncludes, PTR, LAYOUT } from './ctx.js'
|
|
17
|
+
import { T, VAL, analyzeValTypes } from './analyze.js'
|
|
18
|
+
import { optimizeFunc, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, arenaRewindModule } from './optimize.js'
|
|
19
|
+
import { emit } from './emit.js'
|
|
20
|
+
import { mkPtrIR, MAX_CLOSURE_ARITY, MEM_OPS, findBodyStart } from './ir.js'
|
|
21
|
+
|
|
22
|
+
// NaN-prefix top-13-bits as BigInt — used by the static-prefix-strip pass
|
|
23
|
+
const NAN_PREFIX = BigInt(LAYOUT.NAN_PREFIX)
|
|
24
|
+
const TAG_MASK_BIG = BigInt(LAYOUT.TAG_MASK)
|
|
25
|
+
const OFFSET_MASK_BIG = BigInt(LAYOUT.OFFSET_MASK)
|
|
26
|
+
const TAG_SHIFT_BIG = BigInt(LAYOUT.TAG_SHIFT)
|
|
27
|
+
const AUX_SHIFT_BIG = BigInt(LAYOUT.AUX_SHIFT)
|
|
28
|
+
const SSO_BIT_BIG = BigInt(LAYOUT.SSO_BIT)
|
|
29
|
+
|
|
30
|
+
const heapGetIR = () => ctx.memory.shared
|
|
31
|
+
? ['i32.load', ['i32.const', 1020]]
|
|
32
|
+
: ['global.get', '$__heap']
|
|
33
|
+
|
|
34
|
+
const heapSetIR = value => ctx.memory.shared
|
|
35
|
+
? ['i32.store', ['i32.const', 1020], value]
|
|
36
|
+
: ['global.set', '$__heap', value]
|
|
37
|
+
|
|
38
|
+
const ARENA_SAFE_CALLS = new Set([
|
|
39
|
+
'$__alloc', '$__alloc_hdr', '$__mkptr',
|
|
40
|
+
'$__ptr_offset', '$__ptr_type', '$__ptr_aux',
|
|
41
|
+
'$__len', '$__cap', '$__typed_shift', '$__typed_data',
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
function applyArenaRewind(func, fn, safeCallees) {
|
|
45
|
+
if (ctx.transform.optimize?.arenaRewind === false) return false
|
|
46
|
+
if (func.raw || func.sig.params.length !== 0 || func.sig.results.length !== 1) return false
|
|
47
|
+
if (func.sig.ptrKind != null) return false
|
|
48
|
+
if (func.sig.results[0] === 'f64' && func.valResult !== VAL.NUMBER) return false
|
|
49
|
+
if (func.sig.results[0] !== 'f64' && func.sig.results[0] !== 'i32') return false
|
|
50
|
+
|
|
51
|
+
const bodyStart = findBodyStart(fn)
|
|
52
|
+
let hasAlloc = false
|
|
53
|
+
let unsafe = false
|
|
54
|
+
const scan = node => {
|
|
55
|
+
if (unsafe || !Array.isArray(node)) return
|
|
56
|
+
const op = node[0]
|
|
57
|
+
if (op === 'global.set' || op === 'return_call' || op === 'call_indirect' || op === 'call_ref') {
|
|
58
|
+
unsafe = true
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
if (op === 'call') {
|
|
62
|
+
const name = node[1]
|
|
63
|
+
if (name === '$__alloc' || name === '$__alloc_hdr') hasAlloc = true
|
|
64
|
+
if (!(safeCallees ?? ARENA_SAFE_CALLS).has(name)) {
|
|
65
|
+
unsafe = true
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (let i = 1; i < node.length; i++) scan(node[i])
|
|
70
|
+
}
|
|
71
|
+
for (let i = bodyStart; i < fn.length; i++) scan(fn[i])
|
|
72
|
+
if (unsafe || !hasAlloc) return false
|
|
73
|
+
|
|
74
|
+
let id = 0
|
|
75
|
+
const hasLocal = name => fn.some(n => Array.isArray(n) && n[0] === 'local' && n[1] === name)
|
|
76
|
+
while (hasLocal(`$${T}heap_save${id}`) || hasLocal(`$${T}arena_ret${id}`)) id++
|
|
77
|
+
const save = `$${T}heap_save${id}`
|
|
78
|
+
const ret = `$${T}arena_ret${id}`
|
|
79
|
+
const restore = () => heapSetIR(['local.get', save])
|
|
80
|
+
const resultType = func.sig.results[0]
|
|
81
|
+
|
|
82
|
+
const rewriteReturns = node => {
|
|
83
|
+
if (!Array.isArray(node)) return node
|
|
84
|
+
if (node[0] === 'return' && node.length > 1) {
|
|
85
|
+
return ['block',
|
|
86
|
+
['result', resultType],
|
|
87
|
+
['local.set', ret, node[1]],
|
|
88
|
+
restore(),
|
|
89
|
+
['return', ['local.get', ret]],
|
|
90
|
+
['unreachable']]
|
|
91
|
+
}
|
|
92
|
+
for (let i = 1; i < node.length; i++) node[i] = rewriteReturns(node[i])
|
|
93
|
+
return node
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const endsWithReturn = fn.at(-1)?.[0] === 'return' || fn.at(-1)?.[0] === 'return_call'
|
|
97
|
+
for (let i = bodyStart; i < fn.length; i++) fn[i] = rewriteReturns(fn[i])
|
|
98
|
+
const newBodyStart = findBodyStart(fn)
|
|
99
|
+
fn.splice(newBodyStart, 0,
|
|
100
|
+
['local', save, 'i32'],
|
|
101
|
+
['local', ret, resultType],
|
|
102
|
+
['local.set', save, heapGetIR()])
|
|
103
|
+
if (!endsWithReturn) {
|
|
104
|
+
const last = fn.pop()
|
|
105
|
+
fn.push(['local.set', ret, last], restore(), ['local.get', ret])
|
|
106
|
+
}
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
111
|
+
ctx.func.locals = new Map()
|
|
112
|
+
ctx.func.repByLocal = null
|
|
113
|
+
ctx.func.boxed = new Map()
|
|
114
|
+
ctx.func.stack = []
|
|
115
|
+
ctx.func.current = { params: [], results: [] }
|
|
116
|
+
analyzeValTypes(ast)
|
|
117
|
+
const normalizeIR = ir => !ir?.length ? [] : Array.isArray(ir[0]) ? ir : [ir]
|
|
118
|
+
|
|
119
|
+
const moduleInits = []
|
|
120
|
+
if (ctx.module.moduleInits) {
|
|
121
|
+
for (const mi of ctx.module.moduleInits) {
|
|
122
|
+
analyzeValTypes(mi)
|
|
123
|
+
moduleInits.push(...normalizeIR(emit(mi)))
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const init = emit(ast)
|
|
127
|
+
|
|
128
|
+
// Module-scope object literals can create closure bodies while `emit(ast)`
|
|
129
|
+
// runs. Those late closures may pull in stdlib helpers (notably JSON.parse)
|
|
130
|
+
// that affect __start setup, so flush them before deciding which runtime
|
|
131
|
+
// tables __start must initialize. Restore the start-function context after
|
|
132
|
+
// compiling closure bodies; emitClosureBody owns ctx.func.* while it runs.
|
|
133
|
+
const beforeLateClosures = closureFuncs.length
|
|
134
|
+
const startCtx = {
|
|
135
|
+
locals: ctx.func.locals,
|
|
136
|
+
repByLocal: ctx.func.repByLocal,
|
|
137
|
+
boxed: ctx.func.boxed,
|
|
138
|
+
stack: ctx.func.stack,
|
|
139
|
+
current: ctx.func.current,
|
|
140
|
+
body: ctx.func.body,
|
|
141
|
+
directClosures: ctx.func.directClosures,
|
|
142
|
+
preboxed: ctx.func.preboxed,
|
|
143
|
+
localProps: ctx.func.localProps,
|
|
144
|
+
uniq: ctx.func.uniq,
|
|
145
|
+
refinements: ctx.func.refinements,
|
|
146
|
+
}
|
|
147
|
+
compilePendingClosures()
|
|
148
|
+
Object.assign(ctx.func, startCtx)
|
|
149
|
+
|
|
150
|
+
const boxInit = []
|
|
151
|
+
if (ctx.schema.autoBox) {
|
|
152
|
+
const bt = `${T}box`
|
|
153
|
+
ctx.func.locals.set(bt, 'i32')
|
|
154
|
+
for (const [name, { schemaId, schema }] of ctx.schema.autoBox) {
|
|
155
|
+
inc('__alloc_hdr', '__mkptr')
|
|
156
|
+
boxInit.push(
|
|
157
|
+
['local.set', `$${bt}`, ['call', '$__alloc_hdr', ['i32.const', 0], ['i32.const', Math.max(1, schema.length)], ['i32.const', 8]]],
|
|
158
|
+
['f64.store', ['local.get', `$${bt}`],
|
|
159
|
+
ctx.func.names.has(name) ? ['f64.const', 0] : ['global.get', `$${name}`]],
|
|
160
|
+
...schema.slice(1).map((_, i) =>
|
|
161
|
+
['f64.store', ['i32.add', ['local.get', `$${bt}`], ['i32.const', (i + 1) * 8]], ['f64.const', 0]]),
|
|
162
|
+
['global.set', `$${name}`, mkPtrIR(PTR.OBJECT, schemaId, ['local.get', `$${bt}`])])
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const schemaInit = []
|
|
167
|
+
const hasJpObj = ctx.core.includes.has('__jp_obj') || ctx.core.includes.has('__jp')
|
|
168
|
+
const needsSchemaTbl = (ctx.schema.list.length && (
|
|
169
|
+
ctx.core.includes.has('__stringify') ||
|
|
170
|
+
ctx.core.includes.has('__dyn_get') ||
|
|
171
|
+
ctx.core.includes.has('__dyn_get_t') ||
|
|
172
|
+
ctx.core.includes.has('__dyn_get_t_h') ||
|
|
173
|
+
ctx.core.includes.has('__dyn_get_expr_t_h') ||
|
|
174
|
+
ctx.core.includes.has('__dyn_get_any') ||
|
|
175
|
+
ctx.core.includes.has('__dyn_get_any_t') ||
|
|
176
|
+
ctx.core.includes.has('__dyn_get_expr') ||
|
|
177
|
+
ctx.core.includes.has('__dyn_get_expr_t') ||
|
|
178
|
+
ctx.core.includes.has('__dyn_get_or'))) ||
|
|
179
|
+
hasJpObj
|
|
180
|
+
if (needsSchemaTbl) {
|
|
181
|
+
const nSchemas = ctx.schema.list.length
|
|
182
|
+
const runtimeReserve = hasJpObj ? 256 : 0
|
|
183
|
+
const stbl = `${T}stbl`
|
|
184
|
+
const sarr = `${T}sarr`
|
|
185
|
+
ctx.func.locals.set(stbl, 'i32')
|
|
186
|
+
ctx.func.locals.set(sarr, 'i32')
|
|
187
|
+
inc('__alloc', '__alloc_hdr', '__mkptr')
|
|
188
|
+
schemaInit.push(
|
|
189
|
+
['local.set', `$${stbl}`, ['call', '$__alloc', ['i32.const', (nSchemas + runtimeReserve) * 8]]],
|
|
190
|
+
['global.set', '$__schema_tbl', ['local.get', `$${stbl}`]])
|
|
191
|
+
if (runtimeReserve) {
|
|
192
|
+
schemaInit.push(['global.set', '$__schema_next', ['i32.const', nSchemas]])
|
|
193
|
+
}
|
|
194
|
+
for (let s = 0; s < nSchemas; s++) {
|
|
195
|
+
const keys = ctx.schema.list[s]
|
|
196
|
+
const n = keys.length
|
|
197
|
+
schemaInit.push(
|
|
198
|
+
['local.set', `$${sarr}`, ['call', '$__alloc_hdr', ['i32.const', n], ['i32.const', n], ['i32.const', 8]]])
|
|
199
|
+
for (let k = 0; k < n; k++)
|
|
200
|
+
schemaInit.push(
|
|
201
|
+
['f64.store', ['i32.add', ['local.get', `$${sarr}`], ['i32.const', k * 8]],
|
|
202
|
+
emit(['str', String(keys[k])])])
|
|
203
|
+
schemaInit.push(
|
|
204
|
+
['f64.store', ['i32.add', ['local.get', `$${stbl}`], ['i32.const', s * 8]],
|
|
205
|
+
mkPtrIR(PTR.ARRAY, 0, ['local.get', `$${sarr}`])])
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const strPoolInit = []
|
|
210
|
+
if (ctx.runtime.strPool) {
|
|
211
|
+
const total = ctx.runtime.strPool.length
|
|
212
|
+
strPoolInit.push(
|
|
213
|
+
['global.set', '$__strBase', ['call', '$__alloc', ['i32.const', total]]],
|
|
214
|
+
['memory.init', '$__strPool', ['global.get', '$__strBase'], ['i32.const', 0], ['i32.const', total]],
|
|
215
|
+
['data.drop', '$__strPool'],
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const typeofInit = []
|
|
220
|
+
if (ctx.runtime.typeofStrs) {
|
|
221
|
+
for (const s of ctx.runtime.typeofStrs)
|
|
222
|
+
typeofInit.push(['global.set', `$__tof_${s}`, emit(['str', s])])
|
|
223
|
+
}
|
|
224
|
+
const wasiTimers = ctx.features.timers && ctx.transform.host === 'wasi'
|
|
225
|
+
if (moduleInits.length || init?.length || boxInit.length || schemaInit.length || typeofInit.length || strPoolInit.length || wasiTimers) {
|
|
226
|
+
const initIR = normalizeIR(init)
|
|
227
|
+
const startFn = ['func', '$__start']
|
|
228
|
+
for (const [l, t] of ctx.func.locals) startFn.push(['local', `$${l}`, t])
|
|
229
|
+
startFn.push(...strPoolInit, ...typeofInit, ...boxInit, ...schemaInit,
|
|
230
|
+
...(wasiTimers ? [['call', '$__timer_init']] : []),
|
|
231
|
+
...moduleInits, ...initIR,
|
|
232
|
+
...(ctx.features.blockingTimers ? [['call', '$__timer_loop']] : []),
|
|
233
|
+
)
|
|
234
|
+
sec.start.push(startFn, ['start', '$__start'])
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
compilePendingClosures()
|
|
238
|
+
if (closureFuncs.length > beforeLateClosures)
|
|
239
|
+
sec.funcs.unshift(...closureFuncs.slice(beforeLateClosures))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Phase: closure-body dedup.
|
|
244
|
+
*
|
|
245
|
+
* Two closures with structurally-equal bodies (same shape after alpha-renaming
|
|
246
|
+
* locals/params) are emitted as a single function — duplicates redirect through
|
|
247
|
+
* the elem table to the canonical name. Closure bodies often share shape because
|
|
248
|
+
* the same inner arrow can be instantiated in many places (e.g. parser combinators).
|
|
249
|
+
*/
|
|
250
|
+
export function dedupClosureBodies(closureFuncs, sec) {
|
|
251
|
+
if (closureFuncs.length <= 1) return
|
|
252
|
+
const canonicalize = (fn) => {
|
|
253
|
+
const localNames = new Set()
|
|
254
|
+
const collect = (node) => {
|
|
255
|
+
if (!Array.isArray(node)) return
|
|
256
|
+
if ((node[0] === 'local' || node[0] === 'param') && typeof node[1] === 'string' && node[1][0] === '$')
|
|
257
|
+
localNames.add(node[1])
|
|
258
|
+
for (const c of node) collect(c)
|
|
259
|
+
}
|
|
260
|
+
collect(fn)
|
|
261
|
+
let counter = 0
|
|
262
|
+
const renameMap = new Map()
|
|
263
|
+
const walk = node => {
|
|
264
|
+
if (typeof node === 'string') {
|
|
265
|
+
if (!localNames.has(node)) return node
|
|
266
|
+
let r = renameMap.get(node)
|
|
267
|
+
if (!r) { r = `$_c${counter++}`; renameMap.set(node, r) }
|
|
268
|
+
return r
|
|
269
|
+
}
|
|
270
|
+
if (!Array.isArray(node)) return node
|
|
271
|
+
return node.map(walk)
|
|
272
|
+
}
|
|
273
|
+
return JSON.stringify(['func', ...fn.slice(2).map(walk)])
|
|
274
|
+
}
|
|
275
|
+
const hashToName = new Map()
|
|
276
|
+
const redirect = new Map()
|
|
277
|
+
const keepSet = new Set()
|
|
278
|
+
for (const fn of closureFuncs) {
|
|
279
|
+
const key = canonicalize(fn)
|
|
280
|
+
const name = fn[1].slice(1)
|
|
281
|
+
const canonical = hashToName.get(key)
|
|
282
|
+
if (canonical) redirect.set(name, canonical)
|
|
283
|
+
else { hashToName.set(key, name); keepSet.add(name) }
|
|
284
|
+
}
|
|
285
|
+
if (!redirect.size) return
|
|
286
|
+
const kept = sec.funcs.filter(fn => {
|
|
287
|
+
if (!Array.isArray(fn) || fn[0] !== 'func') return true
|
|
288
|
+
const name = typeof fn[1] === 'string' && fn[1][0] === '$' ? fn[1].slice(1) : null
|
|
289
|
+
return !name || !redirect.has(name)
|
|
290
|
+
})
|
|
291
|
+
const redirectRefs = node => {
|
|
292
|
+
if (typeof node === 'string') return node[0] === '$' && redirect.has(node.slice(1)) ? `$${redirect.get(node.slice(1))}` : node
|
|
293
|
+
if (!Array.isArray(node)) return node
|
|
294
|
+
for (let i = 0; i < node.length; i++) node[i] = redirectRefs(node[i])
|
|
295
|
+
return node
|
|
296
|
+
}
|
|
297
|
+
for (const fn of kept) redirectRefs(fn)
|
|
298
|
+
ctx.closure.table = ctx.closure.table.map(n => redirect.get(n) || n)
|
|
299
|
+
sec.funcs.length = 0
|
|
300
|
+
sec.funcs.push(...kept)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Phase: closure-table finalize + ABI shrink.
|
|
305
|
+
*/
|
|
306
|
+
export function finalizeClosureTable(sec) {
|
|
307
|
+
let indirectUsed = ctx.transform.host === 'wasi'
|
|
308
|
+
const scan = (n) => {
|
|
309
|
+
if (!Array.isArray(n) || indirectUsed) return
|
|
310
|
+
if (n[0] === 'call_indirect') { indirectUsed = true; return }
|
|
311
|
+
for (const c of n) if (Array.isArray(c)) scan(c)
|
|
312
|
+
}
|
|
313
|
+
for (const fn of sec.funcs) { scan(fn); if (indirectUsed) break }
|
|
314
|
+
if (!indirectUsed) for (const fn of sec.start) scan(fn)
|
|
315
|
+
if (!indirectUsed) for (const s of Object.keys(ctx.core.stdlib)) {
|
|
316
|
+
if (ctx.core.stdlib[s]?.includes?.('call_indirect')) { indirectUsed = true; break }
|
|
317
|
+
}
|
|
318
|
+
if (indirectUsed) {
|
|
319
|
+
if (!ctx.closure.table) ctx.closure.table = []
|
|
320
|
+
sec.table = [['table', ['export', '"__jz_table"'], ctx.closure.table.length, 'funcref']]
|
|
321
|
+
sec.elem = ctx.closure.table.length ? [['elem', ['i32.const', 0], 'func', ...ctx.closure.table.map(n => `$${n}`)]] : []
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
sec.table = []
|
|
325
|
+
sec.elem = []
|
|
326
|
+
sec.types = sec.types.filter(t => !(Array.isArray(t) && t[1] === '$ftN'))
|
|
327
|
+
const W = ctx.closure.width ?? MAX_CLOSURE_ARITY
|
|
328
|
+
const abiOf = new Map()
|
|
329
|
+
for (const cb of (ctx.closure.bodies || [])) {
|
|
330
|
+
const fixedN = cb.params.length - (cb.rest ? 1 : 0)
|
|
331
|
+
abiOf.set(cb.name, {
|
|
332
|
+
needEnv: cb.captures.length > 0,
|
|
333
|
+
needArgc: !!cb.rest,
|
|
334
|
+
usedSlots: cb.rest ? W : fixedN,
|
|
335
|
+
rest: !!cb.rest,
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
for (const fn of sec.funcs) {
|
|
339
|
+
if (!Array.isArray(fn) || fn[0] !== 'func') continue
|
|
340
|
+
const fnName = typeof fn[1] === 'string' && fn[1][0] === '$' ? fn[1].slice(1) : null
|
|
341
|
+
const abi = abiOf.get(fnName)
|
|
342
|
+
if (!abi) continue
|
|
343
|
+
for (let i = fn.length - 1; i >= 0; i--) {
|
|
344
|
+
const node = fn[i]
|
|
345
|
+
if (!Array.isArray(node) || node[0] !== 'param') continue
|
|
346
|
+
const pname = node[1]
|
|
347
|
+
if (pname === '$__env' && !abi.needEnv) fn.splice(i, 1)
|
|
348
|
+
else if (pname === '$__argc' && !abi.needArgc) fn.splice(i, 1)
|
|
349
|
+
else if (typeof pname === 'string' && pname.startsWith('$__a') && !abi.rest) {
|
|
350
|
+
const idx = parseInt(pname.slice(4), 10)
|
|
351
|
+
if (Number.isFinite(idx) && idx >= abi.usedSlots) fn.splice(i, 1)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const rewriteCalls = (node) => {
|
|
356
|
+
if (!Array.isArray(node)) return
|
|
357
|
+
for (const c of node) if (Array.isArray(c)) rewriteCalls(c)
|
|
358
|
+
if ((node[0] === 'call' || node[0] === 'return_call') && typeof node[1] === 'string') {
|
|
359
|
+
const callee = node[1].slice(1)
|
|
360
|
+
const abi = abiOf.get(callee)
|
|
361
|
+
if (!abi) return
|
|
362
|
+
const newArgs = []
|
|
363
|
+
if (abi.needEnv) newArgs.push(node[2])
|
|
364
|
+
if (abi.needArgc) newArgs.push(node[3])
|
|
365
|
+
for (let i = 0; i < abi.usedSlots; i++) newArgs.push(node[4 + i])
|
|
366
|
+
node.splice(2, node.length - 2, ...newArgs)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
for (const fn of sec.funcs) rewriteCalls(fn)
|
|
370
|
+
for (const fn of sec.start) rewriteCalls(fn)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Phase: pull stdlib + memory.
|
|
375
|
+
*/
|
|
376
|
+
export function pullStdlib(sec) {
|
|
377
|
+
resolveIncludes()
|
|
378
|
+
|
|
379
|
+
const needsMemory = [...ctx.core.includes].some(n => ctx.core.stdlib[n] && MEM_OPS.test(ctx.core.stdlib[n]))
|
|
380
|
+
if (!needsMemory) ctx.scope.globals.delete('__heap')
|
|
381
|
+
if (needsMemory && ctx.module.modules.core) {
|
|
382
|
+
for (const fn of ['__alloc', '__alloc_hdr', '__clear']) if (!ctx.core.includes.has(fn)) ctx.core.includes.add(fn)
|
|
383
|
+
const pages = ctx.memory.pages || 1
|
|
384
|
+
if (ctx.memory.shared) sec.imports.push(['import', '"env"', '"memory"', ['memory', pages]])
|
|
385
|
+
else sec.memory.push(['memory', ['export', '"memory"'], pages])
|
|
386
|
+
if (ctx.transform.alloc !== false && ctx.core._allocRawFuncs)
|
|
387
|
+
sec.funcs.push(...ctx.core._allocRawFuncs.map(s => parseWat(s)))
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const stdlibStr = (name) => {
|
|
391
|
+
const v = ctx.core.stdlib[name]
|
|
392
|
+
return typeof v === 'function' ? v() : v
|
|
393
|
+
}
|
|
394
|
+
ctx.core.extImports ??= new Set()
|
|
395
|
+
for (const name of Object.keys(ctx.core.stdlib)) {
|
|
396
|
+
if (name.startsWith('__ext_') && ctx.core.includes.has(name)) {
|
|
397
|
+
const parsed = parseWat(stdlibStr(name))
|
|
398
|
+
sec.extStdlib.push(parsed[0] === "module" ? parsed[1] : parsed)
|
|
399
|
+
ctx.core.extImports.add(name)
|
|
400
|
+
ctx.core.includes.delete(name)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
for (const n of ctx.core.includes) if (!ctx.core.stdlib[n]) console.error("MISSING stdlib:", n)
|
|
404
|
+
sec.stdlib.push(...[...ctx.core.includes].map(n => parseWat(stdlibStr(n))))
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function syncImports(sec) {
|
|
408
|
+
for (const imp of ctx.module.imports) {
|
|
409
|
+
if (!sec.imports.some(i => i[1] === imp[1] && i[2] === imp[2])) sec.imports.push(imp)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Phase: whole-module + per-function optimization passes.
|
|
415
|
+
*/
|
|
416
|
+
export function optimizeModule(sec) {
|
|
417
|
+
const cfg = ctx.transform.optimize
|
|
418
|
+
if (!cfg || cfg.specializeMkptr !== false)
|
|
419
|
+
specializeMkptr([...sec.funcs, ...sec.stdlib, ...sec.start], wat => sec.stdlib.push(parseWat(wat)), parseWat)
|
|
420
|
+
if (!cfg || cfg.specializePtrBase !== false)
|
|
421
|
+
specializePtrBase([...sec.funcs, ...sec.stdlib, ...sec.start], wat => sec.stdlib.push(parseWat(wat)), parseWat)
|
|
422
|
+
if (ctx.runtime.strPool && (!cfg || cfg.sortStrPoolByFreq !== false)) {
|
|
423
|
+
const poolRef = { pool: ctx.runtime.strPool }
|
|
424
|
+
sortStrPoolByFreq([...sec.funcs, ...sec.stdlib, ...sec.start], poolRef, ctx.runtime.strPoolDedup)
|
|
425
|
+
ctx.runtime.strPool = poolRef.pool
|
|
426
|
+
}
|
|
427
|
+
// Backfill globalTypes for runtime globals declared only in ctx.scope.globals
|
|
428
|
+
// (e.g., __schema_tbl, __strBase). Parses the WAT string to infer i32/f64/i64.
|
|
429
|
+
if (ctx.scope.globals) {
|
|
430
|
+
for (const [name, wat] of ctx.scope.globals) {
|
|
431
|
+
if (!wat || ctx.scope.globalTypes.has(name)) continue
|
|
432
|
+
const m = wat.match(/\(global\s+\$?\S+\s+(?:\(mut\s+)?(i32|i64|f64|f32)/)
|
|
433
|
+
if (m) ctx.scope.globalTypes.set(name, m[1])
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// Build global name→type map from ctx.scope.globalTypes (keys without $) for promoteGlobals
|
|
437
|
+
const globalTypesMap = ctx.scope.globalTypes ? new Map([...ctx.scope.globalTypes].map(([k, v]) => [`$${k}`, v])) : null
|
|
438
|
+
for (const s of [...sec.funcs, ...sec.stdlib, ...sec.start]) optimizeFunc(s, cfg, globalTypesMap)
|
|
439
|
+
if (!cfg || cfg.arenaRewind !== false) {
|
|
440
|
+
const safeCallees = arenaRewindModule([...sec.funcs, ...sec.stdlib, ...sec.start])
|
|
441
|
+
const fnByName = new Map()
|
|
442
|
+
for (const fn of sec.funcs) {
|
|
443
|
+
if (Array.isArray(fn) && fn[0] === 'func' && typeof fn[1] === 'string')
|
|
444
|
+
fnByName.set(fn[1], fn)
|
|
445
|
+
}
|
|
446
|
+
for (const func of ctx.func.list) {
|
|
447
|
+
const fn = fnByName.get(`$${func.name}`)
|
|
448
|
+
if (fn) applyArenaRewind(func, fn, safeCallees)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (!cfg || cfg.hoistConstantPool !== false)
|
|
452
|
+
hoistConstantPool([...sec.funcs, ...sec.stdlib, ...sec.start], (name, wat) => ctx.scope.globals.set(name, wat))
|
|
453
|
+
|
|
454
|
+
// Second promoteGlobals pass disabled: promoting hoistConstantPool's __fc*
|
|
455
|
+
// globals regressed the watr perf micro-pin (WASM compile time increased).
|
|
456
|
+
// The __fc* globals are typically read 3-4 times; the local setup overhead
|
|
457
|
+
// in large functions outweighs the per-read savings. Left as a no-op hook
|
|
458
|
+
// in case future analysis finds a profitable threshold or function-size gate.
|
|
459
|
+
// if (!cfg || cfg.promoteGlobals !== false) {
|
|
460
|
+
// const globalTypesMap2 = ctx.scope.globalTypes ? new Map([...ctx.scope.globalTypes].map(([k, v]) => [`$${k}`, v])) : null
|
|
461
|
+
// for (const s of [...sec.funcs, ...sec.stdlib, ...sec.start]) promoteGlobals(s, globalTypesMap2)
|
|
462
|
+
// }
|
|
463
|
+
|
|
464
|
+
const dataLen = ctx.runtime.data?.length || 0
|
|
465
|
+
if (dataLen > 1024 && !ctx.memory.shared) {
|
|
466
|
+
const heapBase = (dataLen + 7) & ~7
|
|
467
|
+
ctx.scope.globals.set('__heap', `(global $__heap (mut i32) (i32.const ${heapBase}))`)
|
|
468
|
+
ctx.scope.globalTypes.set('__heap', 'i32')
|
|
469
|
+
if (ctx.scope.globals.has('__heap_start')) {
|
|
470
|
+
ctx.scope.globals.set('__heap_start', `(global $__heap_start (mut i32) (i32.const ${heapBase}))`)
|
|
471
|
+
ctx.scope.globalTypes.set('__heap_start', 'i32')
|
|
472
|
+
}
|
|
473
|
+
for (const s of sec.stdlib)
|
|
474
|
+
if (s[0] === 'func' && s[1] === '$__clear')
|
|
475
|
+
for (let i = 2; i < s.length; i++)
|
|
476
|
+
if (Array.isArray(s[i]) && s[i][0] === 'global.set' && Array.isArray(s[i][2]) && s[i][2][0] === 'i32.const')
|
|
477
|
+
s[i][2][1] = `${heapBase}`
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Phase: strip static-data prefix.
|
|
483
|
+
*/
|
|
484
|
+
export function stripStaticDataPrefix(sec) {
|
|
485
|
+
if (!ctx.runtime.staticDataLen || ctx.core.includes.has('__static_str')) return
|
|
486
|
+
const prefix = ctx.runtime.staticDataLen
|
|
487
|
+
const SHIFTABLE = new Set([PTR.STRING, PTR.OBJECT, PTR.ARRAY, PTR.HASH, PTR.SET, PTR.MAP, PTR.BUFFER, PTR.TYPED, PTR.CLOSURE])
|
|
488
|
+
const data = ctx.runtime.data || ''
|
|
489
|
+
const buf = new Uint8Array(data.length)
|
|
490
|
+
for (let i = 0; i < data.length; i++) buf[i] = data.charCodeAt(i)
|
|
491
|
+
const dv = new DataView(buf.buffer)
|
|
492
|
+
if (ctx.runtime.staticPtrSlots) {
|
|
493
|
+
for (const slotOff of ctx.runtime.staticPtrSlots) {
|
|
494
|
+
if (slotOff < prefix) continue
|
|
495
|
+
const bits = dv.getBigUint64(slotOff, true)
|
|
496
|
+
if (((bits >> 48n) & 0xFFF8n) !== NAN_PREFIX) continue
|
|
497
|
+
const ty = Number((bits >> TAG_SHIFT_BIG) & TAG_MASK_BIG)
|
|
498
|
+
if (!SHIFTABLE.has(ty)) continue
|
|
499
|
+
if (ty === PTR.STRING && ((bits >> AUX_SHIFT_BIG) & SSO_BIT_BIG)) continue
|
|
500
|
+
const off = Number(bits & OFFSET_MASK_BIG)
|
|
501
|
+
if (off < prefix) continue
|
|
502
|
+
const hi = bits & ~OFFSET_MASK_BIG
|
|
503
|
+
dv.setBigUint64(slotOff, hi | BigInt(off - prefix), true)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
let s = ''
|
|
507
|
+
for (let i = prefix; i < buf.length; i++) s += String.fromCharCode(buf[i])
|
|
508
|
+
ctx.runtime.data = s
|
|
509
|
+
if (ctx.runtime.staticPtrSlots) ctx.runtime.staticPtrSlots = ctx.runtime.staticPtrSlots
|
|
510
|
+
.filter(o => o >= prefix).map(o => o - prefix)
|
|
511
|
+
const shift = (node) => {
|
|
512
|
+
if (!Array.isArray(node)) return
|
|
513
|
+
for (let i = 0; i < node.length; i++) {
|
|
514
|
+
const child = node[i]
|
|
515
|
+
if (!Array.isArray(child)) continue
|
|
516
|
+
if (child[0] === 'call' && child[1] === '$__mkptr' &&
|
|
517
|
+
Array.isArray(child[2]) && SHIFTABLE.has(child[2][1]) &&
|
|
518
|
+
Array.isArray(child[4]) && child[4][0] === 'i32.const' &&
|
|
519
|
+
typeof child[4][1] === 'number' && child[4][1] >= prefix) {
|
|
520
|
+
const isSsoString = child[2][1] === PTR.STRING &&
|
|
521
|
+
Array.isArray(child[3]) && child[3][0] === 'i32.const' &&
|
|
522
|
+
typeof child[3][1] === 'number' && (child[3][1] & LAYOUT.SSO_BIT)
|
|
523
|
+
if (!isSsoString) child[4][1] -= prefix
|
|
524
|
+
} else if (child[0] === 'f64.const' &&
|
|
525
|
+
typeof child[1] === 'string' && child[1].startsWith('nan:0x')) {
|
|
526
|
+
const bits = BigInt(child[1].slice(4)) | 0x7FF0000000000000n
|
|
527
|
+
if (((bits >> 48n) & 0xFFF8n) === NAN_PREFIX) {
|
|
528
|
+
const ty = Number((bits >> TAG_SHIFT_BIG) & TAG_MASK_BIG)
|
|
529
|
+
if (SHIFTABLE.has(ty) &&
|
|
530
|
+
!(ty === PTR.STRING && ((bits >> AUX_SHIFT_BIG) & SSO_BIT_BIG))) {
|
|
531
|
+
const off = Number(bits & OFFSET_MASK_BIG)
|
|
532
|
+
if (off >= prefix) {
|
|
533
|
+
const hi = bits & ~OFFSET_MASK_BIG
|
|
534
|
+
const newBits = hi | BigInt(off - prefix)
|
|
535
|
+
child[1] = 'nan:0x' + newBits.toString(16).toUpperCase().padStart(16, '0')
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
shift(child)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
for (const s of [...sec.funcs, ...sec.stdlib, ...sec.start]) shift(s)
|
|
544
|
+
}
|