jz 0.6.0 → 0.7.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 +66 -43
- package/bench/README.md +128 -78
- package/bench/bench.svg +32 -42
- package/cli.js +73 -7
- package/dist/interop.js +1 -0
- package/dist/jz.js +6024 -0
- package/index.d.ts +126 -0
- package/index.js +190 -34
- package/interop.js +71 -27
- package/layout.js +5 -0
- package/module/array.js +71 -39
- package/module/collection.js +41 -5
- package/module/core.js +2 -4
- package/module/math.js +138 -2
- package/module/number.js +21 -0
- package/module/object.js +26 -0
- package/module/simd.js +37 -5
- package/module/string.js +41 -12
- package/module/typedarray.js +415 -26
- package/package.json +21 -6
- package/src/autoload.js +3 -0
- package/src/compile/analyze-scans.js +174 -11
- package/src/compile/analyze.js +38 -3
- package/src/compile/emit-assign.js +9 -6
- package/src/compile/emit.js +347 -36
- package/src/compile/index.js +307 -29
- package/src/compile/infer.js +36 -1
- package/src/compile/loop-divmod.js +155 -0
- package/src/compile/narrow.js +108 -11
- package/src/compile/peel-stencil.js +261 -0
- package/src/compile/plan/advise.js +55 -1
- package/src/compile/plan/index.js +4 -0
- package/src/compile/plan/inline.js +5 -2
- package/src/compile/plan/literals.js +220 -5
- package/src/compile/plan/scope.js +115 -39
- package/src/compile/program-facts.js +21 -2
- package/src/ctx.js +45 -7
- package/src/ir.js +55 -7
- package/src/kind-traits.js +27 -0
- package/src/kind.js +65 -3
- package/src/optimize/index.js +344 -45
- package/src/optimize/vectorize.js +2968 -182
- package/src/prepare/index.js +64 -7
- package/src/prepare/lift-iife.js +149 -0
- package/src/reps.js +3 -2
- package/src/static.js +9 -0
- package/src/type.js +10 -6
- package/src/wat/assemble.js +195 -13
- package/src/wat/optimize.js +363 -185
package/src/prepare/index.js
CHANGED
|
@@ -44,7 +44,7 @@ import { recordGlobalRep } from '../compile/infer.js'
|
|
|
44
44
|
import { isFuncRef } from '../ir.js'
|
|
45
45
|
import {
|
|
46
46
|
CTORS, COLLECTION_CTORS, TIMER_NAMES,
|
|
47
|
-
hasModule, includeModule,
|
|
47
|
+
hasModule, includeModule, includeMods,
|
|
48
48
|
includeForArrayAccess, includeForArrayLiteral, includeForArrayPattern, includeForCallableValue,
|
|
49
49
|
includeForGenericMethod, includeForKnownKeyIteration, includeForNamedCall, includeForNumericCoercion,
|
|
50
50
|
includeForObjectLiteral, includeForObjectPattern, includeForOp, includeForProperty, includeForRuntimeCtor,
|
|
@@ -321,6 +321,30 @@ function lookupStaticStringArray(name) {
|
|
|
321
321
|
return ctx.scope.shapeStrArrays?.get(resolved) ?? null
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
/** Evaluate a constant numeric expression (number literals + basic arithmetic) for
|
|
325
|
+
* compile-time string/template folding. Returns null when it isn't a pure-number
|
|
326
|
+
* constant — string `+` and dynamic parts fall through to the caller's runtime path. */
|
|
327
|
+
function constNum(node) {
|
|
328
|
+
if (Array.isArray(node) && node[0] == null && typeof node[1] === 'number') return node[1]
|
|
329
|
+
if (!Array.isArray(node)) return null
|
|
330
|
+
const [op, a, b] = node
|
|
331
|
+
if ((op === 'u-' || op === '-' || op === '+') && b === undefined) {
|
|
332
|
+
const x = constNum(a)
|
|
333
|
+
return x == null ? null : op === 'u-' || op === '-' ? -x : +x
|
|
334
|
+
}
|
|
335
|
+
const x = constNum(a), y = constNum(b)
|
|
336
|
+
if (x == null || y == null) return null
|
|
337
|
+
switch (op) {
|
|
338
|
+
case '+': return x + y
|
|
339
|
+
case '-': return x - y
|
|
340
|
+
case '*': return x * y
|
|
341
|
+
case '/': return y === 0 ? null : x / y
|
|
342
|
+
case '%': return y === 0 ? null : x % y
|
|
343
|
+
case '**': return x ** y
|
|
344
|
+
}
|
|
345
|
+
return null
|
|
346
|
+
}
|
|
347
|
+
|
|
324
348
|
function staticStringExpr(node) {
|
|
325
349
|
const lit = stringValue(node)
|
|
326
350
|
if (lit != null) return lit
|
|
@@ -341,7 +365,11 @@ function staticStringExpr(node) {
|
|
|
341
365
|
if (op === '`') {
|
|
342
366
|
let out = ''
|
|
343
367
|
for (const part of args) {
|
|
344
|
-
|
|
368
|
+
let s = staticStringExpr(part)
|
|
369
|
+
// A numeric interpolation (`${123}`, `${1+2}`) is a constant in string context —
|
|
370
|
+
// ToString it so a fully-static template folds to one literal instead of a runtime
|
|
371
|
+
// concat. (Only the template case stringifies numbers; `+` stays polymorphic.)
|
|
372
|
+
if (s == null) { const n = constNum(part); if (n != null) s = String(n) }
|
|
345
373
|
if (s == null) return null
|
|
346
374
|
out += s
|
|
347
375
|
}
|
|
@@ -383,7 +411,7 @@ function resolveImportMeta(spec) {
|
|
|
383
411
|
|
|
384
412
|
function recordModuleInitFacts(root) {
|
|
385
413
|
const facts = ctx.module.initFacts ||= {
|
|
386
|
-
dynVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
414
|
+
dynVars: new Set(), dynWriteVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
387
415
|
hasFuncValue: false, timerNames: new Set(),
|
|
388
416
|
maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false,
|
|
389
417
|
writtenProps: new Set(),
|
|
@@ -458,6 +486,11 @@ export default function prepare(node) {
|
|
|
458
486
|
// import would cycle (autoload imports every module via module/index.js).
|
|
459
487
|
ctx.module.include = includeModule
|
|
460
488
|
includeModule('core')
|
|
489
|
+
// Empty or whitespace-only source parses to a bare '' — an empty program, not an
|
|
490
|
+
// identifier reference. Normalize to an empty statement so it compiles to a bare
|
|
491
|
+
// `(module)` instead of a `(local.get $)` against a zero-length name. (A non-empty
|
|
492
|
+
// bare identifier like `foo` parses to `'foo'` and stays a real reference.)
|
|
493
|
+
if (node === '') node = [';']
|
|
461
494
|
validateCoalesceMixing(node) // ES2020: reject unparenthesized `??` mixed with `||`/`&&`
|
|
462
495
|
normalizeIdents(node)
|
|
463
496
|
fuseSparseMapReads(node) // AST-level fusion; needs pre-resolution shape — defined at end of file
|
|
@@ -1246,7 +1279,7 @@ function foldNamespaceIntrospection(callee, args) {
|
|
|
1246
1279
|
// Compiler-internal synthetic callees: emit-handled intrinsics, never user
|
|
1247
1280
|
// function values — so a bare reference must not pull in the callable-value
|
|
1248
1281
|
// (function table / closure) machinery.
|
|
1249
|
-
const INTRINSIC_CALLEES = new Set(['__iter_arr'])
|
|
1282
|
+
const INTRINSIC_CALLEES = new Set(['__iter_arr', '__keys_ro'])
|
|
1250
1283
|
|
|
1251
1284
|
function resolveCallee(callee, args) {
|
|
1252
1285
|
if (typeof callee === 'string') {
|
|
@@ -1427,6 +1460,10 @@ const handlers = {
|
|
|
1427
1460
|
|
|
1428
1461
|
// Template literal: [``, part, ...] → fused single-allocation string concat.
|
|
1429
1462
|
'`'(...parts) {
|
|
1463
|
+
// Fully-static template (`a${123}b`, `hello ${1+2} world`) folds to a single string
|
|
1464
|
+
// literal — a static data segment / SSO box, no runtime concat and no heap machinery.
|
|
1465
|
+
const folded = staticStringExpr(['`', ...parts])
|
|
1466
|
+
if (folded != null) return staticString(folded)
|
|
1430
1467
|
includeForStringValue()
|
|
1431
1468
|
const nodes = parts.map(p =>
|
|
1432
1469
|
Array.isArray(p) && p[0] == null && typeof p[1] === 'string' ? ['str', p[1]] : prep(p))
|
|
@@ -1628,6 +1665,15 @@ const handlers = {
|
|
|
1628
1665
|
for (const i of decl.slice(1))
|
|
1629
1666
|
if (Array.isArray(i) && i[0] === '=' && typeof i[1] === 'string')
|
|
1630
1667
|
ctx.func.exports[i[1]] = true
|
|
1668
|
+
// export name → bare-identifier re-export (shorthand for `export { name }`).
|
|
1669
|
+
// Register the binding and emit nothing; without this the name falls through
|
|
1670
|
+
// to `prep(decl)` below and compiles as a dead `global.get; drop` statement
|
|
1671
|
+
// while the export itself is silently lost.
|
|
1672
|
+
if (typeof decl === 'string') {
|
|
1673
|
+
const resolved = ctx.scope.chain[decl]
|
|
1674
|
+
ctx.func.exports[decl] = (resolved && resolved !== decl) ? resolved : decl
|
|
1675
|
+
return null
|
|
1676
|
+
}
|
|
1631
1677
|
// export { name, name as alias } from './mod' or export * from './mod'
|
|
1632
1678
|
if (Array.isArray(decl) && decl[0] === 'from') {
|
|
1633
1679
|
const mod = decl[2]?.[1]
|
|
@@ -1821,7 +1867,11 @@ const handlers = {
|
|
|
1821
1867
|
// alone wrongly folds `-5n`, and negating the bigint here yields garbage (-2^63+5).
|
|
1822
1868
|
// `typeof !== 'bigint'` excludes it in both engines (real JS: 'bigint'; jz: matches
|
|
1823
1869
|
// 'bigint'). Bigint negation then flows to emit's i64.sub(0,·) path correctly.
|
|
1824
|
-
|
|
1870
|
+
// `-0` is NOT folded: the self-host kernel evaluates the constant `-na[1]` with
|
|
1871
|
+
// i32 negation (i32 has no signed zero), collapsing -0→+0 — observable via sort's
|
|
1872
|
+
// -0<+0 tiebreak, Object.is, and 1/x. Leaving it as a runtime `u-` emits f64.neg,
|
|
1873
|
+
// which preserves the sign in both engines; V8 re-folds it, so no native cost.
|
|
1874
|
+
if (b === undefined) { const na = prep(a); return isLit(na) && typeof na[1] === 'number' && typeof na[1] !== 'bigint' && na[1] !== 0 ? [, -na[1]] : ['u-', na] }
|
|
1825
1875
|
return ['-', prep(a), prep(b)]
|
|
1826
1876
|
},
|
|
1827
1877
|
|
|
@@ -2123,10 +2173,17 @@ const handlers = {
|
|
|
2123
2173
|
// `[null, null]`, `undefined` is `[null]` (empty value slot). A numeric/string literal
|
|
2124
2174
|
// `[null, v]` has a non-nullish value slot, so `src[1] == null` discriminates them.
|
|
2125
2175
|
const nullish = Array.isArray(src) && src[0] == null && src[1] == null
|
|
2176
|
+
// `__keys_ro` is for-in's read-only key list: identical to Object.keys, but
|
|
2177
|
+
// when the receiver has a complete static schema the keys are a compile-time
|
|
2178
|
+
// constant, so it pools ONE static-data array instead of allocating a fresh
|
|
2179
|
+
// one each evaluation — the per-iteration heap-growth cliff (jz#deopt-forin).
|
|
2180
|
+
// Sound only because for-in reads ks[i]/ks.length and never mutates (unlike
|
|
2181
|
+
// user Object.keys, which permits in-place `.sort()`/`.reverse()`).
|
|
2182
|
+
includeMods('core', 'object', 'string')
|
|
2126
2183
|
const keysExpr = nullish ? ['[]', null]
|
|
2127
2184
|
: typeof src === 'string'
|
|
2128
|
-
? ['?', ['==', src, [null, null]], ['[]', null], ['()',
|
|
2129
|
-
: ['()',
|
|
2185
|
+
? ['?', ['==', src, [null, null]], ['[]', null], ['()', '__keys_ro', src]]
|
|
2186
|
+
: ['()', '__keys_ro', src]
|
|
2130
2187
|
const ks = `${T}fik${ctx.func.uniq++}`, ix = `${T}fii${ctx.func.uniq++}`, lenV = `${T}fil${ctx.func.uniq++}`
|
|
2131
2188
|
const decls = ['let',
|
|
2132
2189
|
['=', ks, keysExpr],
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lambda-lift immediately-invoked arrow literals (IIFEs).
|
|
3
|
+
*
|
|
4
|
+
* `(params => body)(args)` is lowered by jz's default path as a CLOSURE value invoked
|
|
5
|
+
* via `call_indirect` through the uniform-f64 closure ABI. That ABI can't carry a
|
|
6
|
+
* `v128`, so a SIMD IIFE — `(() => f32x4.…)()` — emits invalid wasm; it also pays a
|
|
7
|
+
* closure alloc + indirect call for what is really a direct, single call.
|
|
8
|
+
*
|
|
9
|
+
* This pass rewrites each such IIFE into a TOP-LEVEL function whose free variables are
|
|
10
|
+
* appended as parameters, and replaces the call with a direct call passing those vars:
|
|
11
|
+
*
|
|
12
|
+
* let r = (() => { let t = f32x4.mul(a, a); return f32x4.add(t, …) })() // captures a
|
|
13
|
+
* ⇓
|
|
14
|
+
* let ⟨lift⟩ = (a) => { let t = f32x4.mul(a, a); return f32x4.add(t, …) } // hoisted to top
|
|
15
|
+
* let r = ⟨lift⟩(a) // direct call
|
|
16
|
+
*
|
|
17
|
+
* The lifted function flows through jz's monomorphic typed-function path (like any
|
|
18
|
+
* `let f = (x) => …; f(v)`), so a captured/returned `v128` is typed `v128` from the
|
|
19
|
+
* single call site — and `inlineOnce` then folds the single-caller body back in, so
|
|
20
|
+
* there's no residual call. Capture is BY VALUE, which is exact for a synchronous
|
|
21
|
+
* immediate invocation (the value at the call instant == what the closure would read);
|
|
22
|
+
* the one divergence — a capture MUTATED inside the body, which a closure writes back
|
|
23
|
+
* through its cell — is the bail below.
|
|
24
|
+
*
|
|
25
|
+
* Bails (leaving the closure path untouched) on: non-plain params (rest / default /
|
|
26
|
+
* destructured), or a body that assigns any captured variable. Everything else folds.
|
|
27
|
+
*
|
|
28
|
+
* @module prepare/lift-iife
|
|
29
|
+
*/
|
|
30
|
+
import { T, extractParams, classifyParam } from '../ast.js'
|
|
31
|
+
import { findFreeVars, findMutations } from '../compile/analyze-scans.js'
|
|
32
|
+
|
|
33
|
+
// Build a comma-list operand node (the parser's shape) from an array of nodes.
|
|
34
|
+
const commaList = (items) =>
|
|
35
|
+
items.length === 0 ? null : items.length === 1 ? items[0] : [',', ...items]
|
|
36
|
+
|
|
37
|
+
// Unwrap parenthesis nodes (`['()', x]`, length 2) to reach the inner expression.
|
|
38
|
+
const unwrapParens = (n) => {
|
|
39
|
+
while (Array.isArray(n) && n[0] === '()' && n.length === 2) n = n[1]
|
|
40
|
+
return n
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// `['()', callee, args]` (length 3) whose callee unwraps to an arrow literal → that arrow.
|
|
44
|
+
const iifeArrow = (n) => {
|
|
45
|
+
if (!Array.isArray(n) || n[0] !== '()' || n.length !== 3) return null
|
|
46
|
+
const callee = unwrapParens(n[1])
|
|
47
|
+
return Array.isArray(callee) && callee[0] === '=>' ? callee : null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Plain-name params only; anything else (rest/default/destructure) → null (bail).
|
|
51
|
+
const plainParamNames = (arrow) => {
|
|
52
|
+
const out = []
|
|
53
|
+
for (const p of extractParams(arrow[1])) {
|
|
54
|
+
const c = classifyParam(p)
|
|
55
|
+
if (c.kind !== 'plain' || typeof c.name !== 'string') return null
|
|
56
|
+
out.push(c.name)
|
|
57
|
+
}
|
|
58
|
+
return out
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Names a function binds: its plain params + every let/const name in its body, NOT
|
|
62
|
+
// descending into nested arrows (their locals are their own). Over-approximating
|
|
63
|
+
// within a frame is safe — a free ident only resolves to one of these if it's
|
|
64
|
+
// actually in scope at the reference. Used to scope captures to enclosing LOCALS
|
|
65
|
+
// (module-level names stay global refs in the lifted body, never captured).
|
|
66
|
+
const collectDeclNames = (decl, out) => {
|
|
67
|
+
if (!Array.isArray(decl)) return
|
|
68
|
+
if (decl[0] === '=' && typeof decl[1] === 'string') out.add(decl[1])
|
|
69
|
+
else if (decl[0] === '=' && Array.isArray(decl[1])) collectPatternNames(decl[1], out)
|
|
70
|
+
else if (typeof decl[1] === 'string') out.add(decl[1])
|
|
71
|
+
}
|
|
72
|
+
const collectPatternNames = (pat, out) => {
|
|
73
|
+
if (typeof pat === 'string') { out.add(pat); return }
|
|
74
|
+
if (!Array.isArray(pat)) return
|
|
75
|
+
for (let i = 1; i < pat.length; i++) collectPatternNames(pat[i], out)
|
|
76
|
+
}
|
|
77
|
+
function functionLocals(paramNodes, body) {
|
|
78
|
+
const names = new Set()
|
|
79
|
+
for (const p of paramNodes) { const c = classifyParam(p); if (typeof c.name === 'string') names.add(c.name); else if (c.pattern) collectPatternNames(c.pattern, names) }
|
|
80
|
+
const scan = (n) => {
|
|
81
|
+
if (!Array.isArray(n)) return
|
|
82
|
+
if (n[0] === '=>') return
|
|
83
|
+
if (n[0] === 'let' || n[0] === 'const') for (const d of n.slice(1)) collectDeclNames(d, names)
|
|
84
|
+
for (let i = 1; i < n.length; i++) scan(n[i])
|
|
85
|
+
}
|
|
86
|
+
scan(body)
|
|
87
|
+
return names
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function liftIIFEs(ast) {
|
|
91
|
+
if (!Array.isArray(ast)) return ast
|
|
92
|
+
const lifted = [] // hoisted `['let', ['=', name, arrow]]` decls
|
|
93
|
+
let uid = 0
|
|
94
|
+
|
|
95
|
+
// Copy non-index node metadata (parser `.loc`, etc.) onto a rebuilt node so error
|
|
96
|
+
// source-locations survive the transform.
|
|
97
|
+
const copyMeta = (to, from) => { for (const k of Object.keys(from)) if (isNaN(+k)) to[k] = from[k]; return to }
|
|
98
|
+
|
|
99
|
+
// `locals` = union of every enclosing FUNCTION frame's bound names. The transform
|
|
100
|
+
// is post-order (inner IIFEs fold first) so an outer lift sees already-direct inner
|
|
101
|
+
// calls; scope tracking is top-down so each IIFE sees the right enclosing locals.
|
|
102
|
+
// Identity-preserving: a node with no lifted descendant is returned unchanged.
|
|
103
|
+
const visit = (node, locals) => {
|
|
104
|
+
if (!Array.isArray(node)) return node
|
|
105
|
+
|
|
106
|
+
// Descend into an arrow body with the frame extended by this arrow's locals.
|
|
107
|
+
if (node[0] === '=>') {
|
|
108
|
+
const inner = new Set(locals)
|
|
109
|
+
for (const n of functionLocals(extractParams(node[1]), node[2])) inner.add(n)
|
|
110
|
+
let changed = false
|
|
111
|
+
const out = node.map((c, i) => { if (i === 0) return c; const v = visit(c, inner); if (v !== c) changed = true; return v })
|
|
112
|
+
return changed ? copyMeta(out, node) : node
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Transform children first (post-order).
|
|
116
|
+
let changed = false
|
|
117
|
+
const mapped = node.map((c, i) => { if (i === 0) return c; const v = visit(c, locals); if (v !== c) changed = true; return v })
|
|
118
|
+
const base = changed ? copyMeta(mapped, node) : node
|
|
119
|
+
|
|
120
|
+
const arrow = iifeArrow(base)
|
|
121
|
+
if (!arrow) return base
|
|
122
|
+
|
|
123
|
+
const paramNames = plainParamNames(arrow)
|
|
124
|
+
if (paramNames === null) return base // bail: non-plain params
|
|
125
|
+
|
|
126
|
+
const callArgs = base[2] == null ? [] : (Array.isArray(base[2]) && base[2][0] === ',') ? base[2].slice(1) : [base[2]]
|
|
127
|
+
if (callArgs.some(a => Array.isArray(a) && a[0] === '...')) return base // bail: spread into a fixed-arity direct call
|
|
128
|
+
|
|
129
|
+
const body = arrow[2]
|
|
130
|
+
const captures = []
|
|
131
|
+
findFreeVars(body, new Set(paramNames), captures, locals)
|
|
132
|
+
|
|
133
|
+
const mutated = new Set()
|
|
134
|
+
findMutations(body, new Set(captures), mutated)
|
|
135
|
+
if (mutated.size) return base // bail: a captured var is written
|
|
136
|
+
|
|
137
|
+
// Lift: `(…params, …captures) => body`, hoisted to module top; call passes captures.
|
|
138
|
+
const name = `${T}lift${uid++}`
|
|
139
|
+
lifted.push(['let', ['=', name, ['=>', ['()', commaList([...paramNames, ...captures])], body]]])
|
|
140
|
+
return copyMeta(['()', name, commaList([...callArgs, ...captures])], base)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const result = visit(ast, new Set())
|
|
144
|
+
if (!lifted.length) return result
|
|
145
|
+
|
|
146
|
+
// Prepend the lifted decls to the module body (`[';', …stmts]`).
|
|
147
|
+
if (Array.isArray(result) && result[0] === ';') return [';', ...lifted, ...result.slice(1)]
|
|
148
|
+
return [';', ...lifted, result]
|
|
149
|
+
}
|
package/src/reps.js
CHANGED
|
@@ -56,6 +56,7 @@ export const VAL = {
|
|
|
56
56
|
* @property {boolean} [notString] proven not a string (skips string-path guards).
|
|
57
57
|
* @property {number} [arrayElemSchema] element object-schema id for arrays.
|
|
58
58
|
* @property {string} [arrayElemValType] element VAL.* kind for arrays.
|
|
59
|
+
* @property {string} [arrayElemElemValType] nested element VAL.* kind (`X[i][j]`) for arrays of arrays.
|
|
59
60
|
* @property {string} [carrier] abi carrier id override (e.g. 'jsstring').
|
|
60
61
|
* @property {boolean} [unsigned] i32 carries an unsigned value (`>>>` result).
|
|
61
62
|
* @property {*} [jsonShape] inferred shape for the JSON.stringify fast path.
|
|
@@ -67,8 +68,8 @@ export const VAL = {
|
|
|
67
68
|
*/
|
|
68
69
|
export const REP_FIELDS = new Set([
|
|
69
70
|
'val', 'ptrKind', 'ptrAux', 'schemaId', 'intConst', 'intCertain', 'notString',
|
|
70
|
-
'arrayElemSchema', 'arrayElemValType', 'carrier', 'unsigned', 'jsonShape',
|
|
71
|
-
'typedCtor', 'wasm', 'nullable',
|
|
71
|
+
'arrayElemSchema', 'arrayElemValType', 'arrayElemElemValType', 'carrier', 'unsigned', 'jsonShape',
|
|
72
|
+
'typedCtor', 'wasm', 'nullable', 'neverGrown',
|
|
72
73
|
])
|
|
73
74
|
|
|
74
75
|
const DBG_REPS = typeof process !== 'undefined' && process.env?.JZ_DEBUG_INVARIANTS === '1'
|
package/src/static.js
CHANGED
|
@@ -19,6 +19,15 @@ export function intLiteralValue(expr) {
|
|
|
19
19
|
/** Non-negative integer literal — used for string/typed-array index bounds. */
|
|
20
20
|
export const nonNegIntLiteral = (node) => { const n = intLiteralValue(node); return n != null && n >= 0 ? n : null }
|
|
21
21
|
|
|
22
|
+
/** Flat-array slot key for a *bare* non-negative integer index literal `[null, k]`
|
|
23
|
+
* — returns the stringified index ("0","1",…) so an array `a[k]` resolves through
|
|
24
|
+
* the same SRoA `name#i` machinery as an object `o.key`. Only a literal index
|
|
25
|
+
* qualifies (not a const-folded identifier): the key must be unambiguous at scan
|
|
26
|
+
* time, before any rep is known. Null for dynamic / non-integer / huge indices. */
|
|
27
|
+
export const staticIndexKey = (node) =>
|
|
28
|
+
Array.isArray(node) && node[0] == null && Number.isInteger(node[1]) && node[1] >= 0 && node[1] < 0x100000000
|
|
29
|
+
? String(node[1]) : null
|
|
30
|
+
|
|
22
31
|
/** Fold compile-time integer expressions (literals, const bindings, + - * <<). */
|
|
23
32
|
export function constIntExpr(node) {
|
|
24
33
|
let lit = intLiteralValue(node)
|
package/src/type.js
CHANGED
|
@@ -401,16 +401,20 @@ export function exprType(expr, locals) {
|
|
|
401
401
|
|
|
402
402
|
// Always f64
|
|
403
403
|
if (op === '/' || op === '**' || op === '[' || op === '{}' || op === 'str') return 'f64'
|
|
404
|
-
// arr[i] — typed
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
404
|
+
// arr[i] — integer typed arrays (Int8/Uint8/Int16/Uint16/Int32/Uint32, aux 0..5) read as i32:
|
|
405
|
+
// the element IS a 32-bit machine integer, so a binding used in integer/bitwise ops stays i32
|
|
406
|
+
// instead of round-tripping i32.load → f64 → trunc back (the deopt that made packed-pixel fade
|
|
407
|
+
// loops like lorenz slow). Uint32 reads carry the full 0..2^32-1 range as the i32 bit-pattern;
|
|
408
|
+
// ToInt32-coercing uses (& | ^ << >> >>>, i32.store) are bit-exact, and value uses that need the
|
|
409
|
+
// unsigned magnitude (compare, f64 convert) go through the elem-aux's unsigned path. Floats
|
|
410
|
+
// (Float32/Float64, aux 6/7) genuinely yield f64. typedElems: in-progress reads come from
|
|
411
|
+
// localTypedElemsOverlay during analyzeBody; post-analyze passes read ctx.types.typedElem.
|
|
408
412
|
if (op === '[]') {
|
|
409
413
|
if (typeof args[0] === 'string') {
|
|
410
414
|
const ctor = ctx.func.localTypedElemsOverlay?.get(args[0]) ?? ctx.types.typedElem?.get(args[0])
|
|
411
415
|
if (ctor) {
|
|
412
416
|
const aux = typedElemAux(ctor)
|
|
413
|
-
if (aux != null && (aux & 7) <=
|
|
417
|
+
if (aux != null && (aux & 7) <= 5) return 'i32'
|
|
414
418
|
}
|
|
415
419
|
}
|
|
416
420
|
return 'f64'
|
|
@@ -501,7 +505,7 @@ export function exprType(expr, locals) {
|
|
|
501
505
|
// hand a scalar back (i32x4.lane / v128.anyTrue / v128.allTrue → i32;
|
|
502
506
|
// f32x4.lane → f64). See module/simd.js.
|
|
503
507
|
if (typeof args[0] === 'string' && (args[0].startsWith('f32x4.') || args[0].startsWith('i32x4.') || args[0].startsWith('f64x2.') || args[0].startsWith('v128.'))) {
|
|
504
|
-
if (args[0] === 'f32x4.lane') return 'f64'
|
|
508
|
+
if (args[0] === 'f32x4.lane' || args[0] === 'f64x2.lane') return 'f64'
|
|
505
509
|
if (args[0] === 'i32x4.lane' || args[0] === 'v128.anyTrue' || args[0] === 'v128.allTrue') return 'i32'
|
|
506
510
|
return 'v128'
|
|
507
511
|
}
|
package/src/wat/assemble.js
CHANGED
|
@@ -37,7 +37,7 @@ import { T } from '../ast.js'
|
|
|
37
37
|
import { analyzeValTypes, analyzeBody } from '../compile/analyze.js'
|
|
38
38
|
import { VAL } from '../reps.js'
|
|
39
39
|
import { optimizeFunc, collectVolatileGlobals, collectReachableGlobalWrites, hoistGlobalPtrOffset, stablePtrGlobalNames, hoistConstantPool, specializeMkptr, specializePtrBase, sortStrPoolByFreq, arenaRewindModule } from '../optimize/index.js'
|
|
40
|
-
import { emit } from '../compile/emit.js'
|
|
40
|
+
import { emit, emitVoid } from '../compile/emit.js'
|
|
41
41
|
import { mkPtrIR, MAX_CLOSURE_ARITY, MEM_OPS, findBodyStart } from '../ir.js'
|
|
42
42
|
|
|
43
43
|
// NaN-prefix top-13-bits as BigInt — used by the static-prefix-strip pass
|
|
@@ -153,6 +153,11 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
153
153
|
analyzeValTypes(ast)
|
|
154
154
|
const normalizeIR = ir => !ir?.length ? [] : Array.isArray(ir[0]) ? ir : [ir]
|
|
155
155
|
|
|
156
|
+
// Mark module-scope emission: top-level statements run exactly once, so a constant
|
|
157
|
+
// array/object literal here is a single instance that can safely live in a static
|
|
158
|
+
// data segment (no per-call freshness to violate). Function bodies — compiled
|
|
159
|
+
// separately, and the late closures below — leave this unset and alloc fresh.
|
|
160
|
+
ctx.func.atModuleScope = true
|
|
156
161
|
const moduleInits = []
|
|
157
162
|
if (ctx.module.moduleInits) {
|
|
158
163
|
for (const mi of ctx.module.moduleInits) {
|
|
@@ -162,7 +167,13 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
162
167
|
}
|
|
163
168
|
}
|
|
164
169
|
seedGeneratedLocals(ast)
|
|
165
|
-
|
|
170
|
+
// __start has no result: emit the top-level program in void context so a stray
|
|
171
|
+
// value is dropped. `ast` is normally a `;` statement-sequence (each statement
|
|
172
|
+
// already void-dropped), but jzify unwraps a single-statement program to its
|
|
173
|
+
// bare expression — emitting that in value context leaves a value on the stack
|
|
174
|
+
// and the start function fails validation. emitVoid handles both shapes.
|
|
175
|
+
const init = emitVoid(ast)
|
|
176
|
+
ctx.func.atModuleScope = false
|
|
166
177
|
|
|
167
178
|
// Module-scope object literals can create closure bodies while `emit(ast)`
|
|
168
179
|
// runs. Those late closures may pull in stdlib helpers (notably JSON.parse)
|
|
@@ -285,6 +296,47 @@ export function buildStartFn(ast, sec, closureFuncs, compilePendingClosures) {
|
|
|
285
296
|
sec.funcs.unshift(...closureFuncs.slice(beforeLateClosures))
|
|
286
297
|
}
|
|
287
298
|
|
|
299
|
+
/**
|
|
300
|
+
* Hoist constant global initializers out of `__start` into immutable inline decls.
|
|
301
|
+
*
|
|
302
|
+
* A top-level `const x = <constant>` for a non-numeric value (atom `true`/`null`/
|
|
303
|
+
* `undefined`/`NaN`, an SSO or static-string NaN-box, a folded pointer) emits a
|
|
304
|
+
* `(global.set $x (f64.const …))` into `__start`, because only *numeric* consts are
|
|
305
|
+
* folded ahead of emit. But the value is a compile-time constant, so it belongs in
|
|
306
|
+
* the decl itself — `(global $x f64 (f64.const …))` — exactly like the numeric path.
|
|
307
|
+
* That drops the store, and when it empties `__start` the start function and its
|
|
308
|
+
* directive go too. Gated to single-assignment user `const`s so we never freeze a
|
|
309
|
+
* binding something else writes.
|
|
310
|
+
*/
|
|
311
|
+
export function hoistConstGlobalInits(sec) {
|
|
312
|
+
const startFn = sec.start.find(n => Array.isArray(n) && n[0] === 'func' && n[1] === '$__start')
|
|
313
|
+
if (!startFn) return
|
|
314
|
+
const writes = new Map()
|
|
315
|
+
const scan = (node) => {
|
|
316
|
+
if (!Array.isArray(node)) return
|
|
317
|
+
if (node[0] === 'global.set' && typeof node[1] === 'string') writes.set(node[1], (writes.get(node[1]) || 0) + 1)
|
|
318
|
+
for (const c of node) scan(c)
|
|
319
|
+
}
|
|
320
|
+
for (const arr of [sec.funcs, sec.stdlib, sec.start]) for (const fn of arr) scan(fn)
|
|
321
|
+
for (let i = startFn.length - 1; i >= findBodyStart(startFn); i--) {
|
|
322
|
+
const stmt = startFn[i]
|
|
323
|
+
if (!Array.isArray(stmt) || stmt[0] !== 'global.set' || writes.get(stmt[1]) !== 1) continue
|
|
324
|
+
const name = typeof stmt[1] === 'string' && stmt[1][0] === '$' ? stmt[1].slice(1) : null
|
|
325
|
+
const g = name && ctx.scope.globals.get(name)
|
|
326
|
+
const c = stmt[2]
|
|
327
|
+
if (!g || !g.mut || !ctx.scope.consts?.has(name) || !ctx.scope.userGlobals?.has(name)) continue
|
|
328
|
+
if (!Array.isArray(c) || c[0] !== `${g.type}.const`) continue
|
|
329
|
+
ctx.scope.globals.set(name, { ...g, mut: false, init: c[1] })
|
|
330
|
+
startFn.splice(i, 1)
|
|
331
|
+
}
|
|
332
|
+
// Hoisting can empty `__start`. The O2 watr pass prunes a bodyless start, but at
|
|
333
|
+
// O0/O1 nothing else does — drop it (func + directive) here so a const-only module
|
|
334
|
+
// carries no start at all.
|
|
335
|
+
if (findBodyStart(startFn) >= startFn.length)
|
|
336
|
+
for (let j = sec.start.length - 1; j >= 0; j--)
|
|
337
|
+
if (Array.isArray(sec.start[j]) && sec.start[j][1] === '$__start') sec.start.splice(j, 1)
|
|
338
|
+
}
|
|
339
|
+
|
|
288
340
|
/**
|
|
289
341
|
* Phase: closure-body dedup.
|
|
290
342
|
*
|
|
@@ -420,24 +472,154 @@ export function finalizeClosureTable(sec) {
|
|
|
420
472
|
for (const fn of sec.start) rewriteCalls(fn)
|
|
421
473
|
}
|
|
422
474
|
|
|
475
|
+
/**
|
|
476
|
+
* Stdlib funcs actually reachable from the emitted program. Seeds from real
|
|
477
|
+
* `call`/`return_call`/`ref.func` sites in the user funcs, `__start`, and the elem
|
|
478
|
+
* table, then closes transitively over the stdlib call graph (each reached helper's
|
|
479
|
+
* template references). Conservative by construction — a template `$__foo` in a
|
|
480
|
+
* feature-dead branch is kept, never dropped — so it's safe to gate inclusion and the
|
|
481
|
+
* memory/allocator decision on it. An eagerly-`inc`'d helper that nothing calls is
|
|
482
|
+
* absent, which is the whole point.
|
|
483
|
+
*/
|
|
484
|
+
function reachableStdlib(sec) {
|
|
485
|
+
const stdlib = ctx.core.stdlib
|
|
486
|
+
const reach = new Set(), stack = []
|
|
487
|
+
// Track every reached name (module-namespace `math.sin` included), but only follow
|
|
488
|
+
// those with a stdlib template. Names match `$foo`, `$__foo`, `$math.sin_core` — the
|
|
489
|
+
// dotted module funcs are the ones the `$__`-only regex used to miss, pruning live code.
|
|
490
|
+
const add = (name) => { if (!reach.has(name)) { reach.add(name); if (stdlib[name] != null) stack.push(name) } }
|
|
491
|
+
const scanIR = (node) => {
|
|
492
|
+
if (!Array.isArray(node)) return
|
|
493
|
+
if ((node[0] === 'call' || node[0] === 'return_call' || node[0] === 'ref.func') &&
|
|
494
|
+
typeof node[1] === 'string' && node[1][0] === '$') add(node[1].slice(1))
|
|
495
|
+
for (const c of node) scanIR(c)
|
|
496
|
+
}
|
|
497
|
+
for (const fn of sec.funcs) scanIR(fn)
|
|
498
|
+
for (const fn of sec.start) scanIR(fn)
|
|
499
|
+
for (const e of sec.elem) // closure table: bare `$fn` func refs
|
|
500
|
+
if (Array.isArray(e)) for (const c of e) if (typeof c === 'string' && c[0] === '$') add(c.slice(1))
|
|
501
|
+
// A stdlib func that self-exports (`(export "__invoke_closure")`) is a host-facing
|
|
502
|
+
// entry point — the JS host calls it directly, so it's a root even when nothing in
|
|
503
|
+
// the wasm calls it. Mirrors treeshake's inline-export rooting.
|
|
504
|
+
for (const n of ctx.core.includes) {
|
|
505
|
+
const v = stdlib[n]
|
|
506
|
+
let t = ''
|
|
507
|
+
try { t = typeof v === 'function' ? v() : v } catch { t = '' }
|
|
508
|
+
if (typeof t === 'string' && t.includes('(export "')) add(n)
|
|
509
|
+
}
|
|
510
|
+
while (stack.length) {
|
|
511
|
+
const v = stdlib[stack.pop()]
|
|
512
|
+
let text = ''
|
|
513
|
+
try { text = typeof v === 'function' ? v() : v } catch { text = '' }
|
|
514
|
+
if (typeof text === 'string') for (const m of text.matchAll(/\$([A-Za-z_][A-Za-z0-9_.]*)/g)) add(m[1])
|
|
515
|
+
}
|
|
516
|
+
return reach
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// The f64x2 stdlib mirrors the lane vectorizer (optimize/vectorize.js) injects in the LATE 'post'
|
|
520
|
+
// pass — after the stdlib was pulled + treeshaken. Keep in sync with that pass's call-rewrite map
|
|
521
|
+
// (PPC_CALL2). These are the ONLY helpers appendLateStdlib may add; restricting to them avoids
|
|
522
|
+
// touching helpers that live in other module sections (ext-stdlib, imports) where a blind
|
|
523
|
+
// referenced-but-absent scan would wrongly re-append and duplicate them.
|
|
524
|
+
const LATE_VEC_HELPERS = new Set(['math.sin2', 'math.cos2', 'math.pow2', 'math.atan2_2', 'math.hypot_2', 'math.log_v', 'math.exp_v', 'math.exp2_v'])
|
|
525
|
+
|
|
526
|
+
// A late pass can reference one of the f64x2 mirrors that wasn't present when the stdlib was first
|
|
527
|
+
// assembled. Append any referenced-but-missing mirror body (fixpoint over their own calls, though
|
|
528
|
+
// the trig mirrors call nothing). moduleArr is mutated in place; non-mirror references are left for
|
|
529
|
+
// watr to resolve (a genuine missing helper is the kernel's own pull, already satisfied).
|
|
530
|
+
export function appendLateStdlib(moduleArr) {
|
|
531
|
+
const stdlib = ctx.core.stdlib
|
|
532
|
+
const have = new Set()
|
|
533
|
+
for (const n of moduleArr) if (Array.isArray(n) && n[0] === 'func' && typeof n[1] === 'string') have.add(n[1])
|
|
534
|
+
let added = true
|
|
535
|
+
while (added) {
|
|
536
|
+
added = false
|
|
537
|
+
const refs = new Set()
|
|
538
|
+
const scan = (n) => { if (!Array.isArray(n)) return; if ((n[0] === 'call' || n[0] === 'return_call' || n[0] === 'ref.func') && typeof n[1] === 'string' && n[1][0] === '$') refs.add(n[1]); for (const c of n) scan(c) }
|
|
539
|
+
for (const n of moduleArr) scan(n)
|
|
540
|
+
for (const ref of refs) {
|
|
541
|
+
const name = ref.slice(1)
|
|
542
|
+
if (have.has(ref) || !LATE_VEC_HELPERS.has(name) || stdlib[name] == null) continue
|
|
543
|
+
const node = parseTemplate(typeof stdlib[name] === 'function' ? stdlib[name]() : stdlib[name])
|
|
544
|
+
moduleArr.push(node[0] === 'module' ? node[1] : node)
|
|
545
|
+
have.add(ref)
|
|
546
|
+
added = true
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
423
551
|
/**
|
|
424
552
|
* Phase: pull stdlib + memory.
|
|
425
553
|
*/
|
|
426
554
|
export function pullStdlib(sec) {
|
|
427
555
|
resolveIncludes()
|
|
428
556
|
|
|
429
|
-
|
|
430
|
-
|
|
557
|
+
// Reachability, not inclusion, decides what the output needs. `ctx.core.includes`
|
|
558
|
+
// accumulates everything a module *might* use (eager module-load `inc`s + transitive
|
|
559
|
+
// deps), but a const array / static string literal calls none of it. So we seed from
|
|
560
|
+
// the actual call sites in the emitted funcs + __start (+ elem table) and close
|
|
561
|
+
// transitively over the stdlib call graph. An eagerly-included helper that nothing
|
|
562
|
+
// calls never enters this set — so allocator, memory, and exports reflect real use.
|
|
563
|
+
const reachable = reachableStdlib(sec)
|
|
564
|
+
const realize = (n) => { const v = ctx.core.stdlib[n]; try { return typeof v === 'function' ? v() : v } catch { return '' } }
|
|
565
|
+
|
|
566
|
+
// Two distinct needs, kept separate:
|
|
567
|
+
// · needsAlloc — the program allocates at runtime: an allocator func is reachable,
|
|
568
|
+
// or shared-mem string literals seed a pool __start allocs. Drives the bump
|
|
569
|
+
// allocator (`__alloc`/`__alloc_hdr`/`__clear`), the `__heap` pointer, and the
|
|
570
|
+
// `_alloc`/`_clear` marshalling exports.
|
|
571
|
+
// · needsMemory — linear memory must merely *exist*: we allocate, OR a literal lives
|
|
572
|
+
// in a static data segment (a const pointer, no allocator behind it), OR a reached
|
|
573
|
+
// helper / inline body does a load/store, OR `__ptr_type` is reached (the module
|
|
574
|
+
// discriminates heap tags — an `instanceof`/`typeof x==='object'` whose argument the
|
|
575
|
+
// host marshals across the boundary). A data segment with no memory is invalid wasm,
|
|
576
|
+
// so memory can't be gated on allocation alone.
|
|
577
|
+
const ALLOC_FUNCS = ['__alloc', '__alloc_hdr', '__alloc_hdr_n']
|
|
578
|
+
const needsAlloc = !!ctx.runtime.strPool || ALLOC_FUNCS.some(a => reachable.has(a))
|
|
579
|
+
// Memory ops can be emitted *inline* into user/start funcs (a heap-path char read
|
|
580
|
+
// loads without calling a stdlib helper), so scan the emitted bodies too.
|
|
581
|
+
const hasMemOp = (node) => Array.isArray(node) &&
|
|
582
|
+
((typeof node[0] === 'string' && MEM_OPS.test(node[0])) || node.some(hasMemOp))
|
|
583
|
+
// `ctx.runtime.data` is never empty here — the number module seeds a static stringify
|
|
584
|
+
// prefix (`NaNInfinity…`) at offset 0; stripStaticDataPrefix removes it when unused, so
|
|
585
|
+
// the real question is whether any data lives *beyond* that strippable prefix.
|
|
586
|
+
// An explicit `{ memory: pages }` / shared-memory option is a caller request to own
|
|
587
|
+
// linear memory (e.g. to marshal host values in), independent of what the wasm itself
|
|
588
|
+
// reaches — honour it even for an otherwise-memoryless program.
|
|
589
|
+
const explicitMemory = ctx.memory.pages > 0 || !!ctx.memory.shared
|
|
590
|
+
const needsMemory = needsAlloc || explicitMemory ||
|
|
591
|
+
(ctx.runtime.data?.length || 0) > (ctx.runtime.staticDataLen || 0) ||
|
|
592
|
+
reachable.has('__ptr_type') ||
|
|
593
|
+
[...reachable].some(n => MEM_OPS.test(realize(n))) ||
|
|
594
|
+
sec.funcs.some(hasMemOp) || sec.start.some(hasMemOp)
|
|
595
|
+
// Emit only what's reachable: drop every eagerly-`inc`'d *internal* helper the program
|
|
596
|
+
// never calls. This is what lets a const-array / static-string / atom module shed the
|
|
597
|
+
// allocator, pointer dispatchers, and length helpers that an array/object module load
|
|
598
|
+
// pulled in wholesale — and it keeps the dead allocator from dangling on the `$__heap`
|
|
599
|
+
// we delete below. Scoped to `__`-prefixed names: module-namespace funcs (`math.sin`)
|
|
600
|
+
// are pulled in on demand, never eagerly, so they're already minimal and never pruned
|
|
601
|
+
// here (guarding against any reachability blind spot in a dotted-name template).
|
|
602
|
+
for (const n of [...ctx.core.includes]) if (n.startsWith('__') && !reachable.has(n)) ctx.core.includes.delete(n)
|
|
603
|
+
if (!needsAlloc) ctx.scope.globals.delete('__heap')
|
|
431
604
|
if (needsMemory && ctx.module.modules.core) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
605
|
+
if (needsAlloc) {
|
|
606
|
+
for (const fn of ['__alloc', '__alloc_hdr', '__clear']) ctx.core.includes.add(fn)
|
|
607
|
+
// Late-add of allocators may pull in transitive deps (__alloc → __memgrow,
|
|
608
|
+
// etc.) that the initial resolveIncludes did not yet see; re-resolve.
|
|
609
|
+
// No-op when the alloc trio was already present.
|
|
610
|
+
resolveIncludes()
|
|
611
|
+
}
|
|
612
|
+
// Initial pages must cover the static data segment (it loads at instantiation), not
|
|
613
|
+
// just the default 1 — otherwise a module whose constants exceed 64 KiB emits a data
|
|
614
|
+
// segment that overflows its own memory. The heap grows past this on demand via
|
|
615
|
+
// __memgrow. (Shared memory loads literals via memory.init into allocated space, so
|
|
616
|
+
// its initial size isn't pinned by the data length.)
|
|
617
|
+
const dataPages = ctx.memory.shared ? 0 : Math.ceil((ctx.runtime.data?.length || 0) / 65536)
|
|
618
|
+
const pages = Math.max(ctx.memory.pages || 1, dataPages)
|
|
619
|
+
const max = ctx.memory.max || 0 // 0 = no maximum (unbounded growth)
|
|
620
|
+
if (ctx.memory.shared) sec.imports.push(['import', '"env"', '"memory"', max ? ['memory', pages, max] : ['memory', pages]])
|
|
621
|
+
else sec.memory.push(max ? ['memory', ['export', '"memory"'], pages, max] : ['memory', ['export', '"memory"'], pages])
|
|
622
|
+
if (needsAlloc && ctx.transform.alloc !== false && ctx.core._allocRawFuncs)
|
|
441
623
|
sec.funcs.push(...ctx.core._allocRawFuncs.map(parseTemplate))
|
|
442
624
|
}
|
|
443
625
|
|