jz 0.8.0 → 0.9.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 +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +27 -27
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6178
- package/index.js +165 -74
- package/interop.js +189 -17
- package/jzify/arguments.js +32 -1
- package/jzify/async.js +409 -0
- package/jzify/classes.js +136 -18
- package/jzify/generators.js +639 -0
- package/jzify/hoist-vars.js +73 -1
- package/jzify/index.js +123 -4
- package/jzify/names.js +1 -0
- package/jzify/transform.js +239 -1
- package/layout.js +48 -3
- package/module/array.js +318 -43
- package/module/atomics.js +144 -0
- package/module/collection.js +1429 -155
- package/module/core.js +431 -40
- package/module/date.js +142 -120
- package/module/fs.js +144 -0
- package/module/function.js +6 -3
- package/module/index.js +4 -1
- package/module/json.js +270 -49
- package/module/math.js +1032 -145
- package/module/number.js +532 -163
- package/module/object.js +353 -95
- package/module/regex.js +157 -7
- package/module/schema.js +100 -2
- package/module/string.js +428 -93
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +82 -11
- package/src/ast.js +11 -5
- package/src/autoload.js +28 -1
- package/src/bridge.js +7 -2
- package/src/compile/analyze-scans.js +22 -7
- package/src/compile/analyze.js +136 -30
- package/src/compile/dyn-closure-tables.js +277 -0
- package/src/compile/emit-assign.js +281 -15
- package/src/compile/emit.js +1055 -70
- package/src/compile/index.js +277 -29
- package/src/compile/infer.js +42 -4
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/loop-recurrence.js +167 -0
- package/src/compile/narrow.js +555 -18
- package/src/compile/peel-stencil.js +5 -0
- package/src/compile/plan/index.js +45 -3
- package/src/compile/plan/inline.js +8 -1
- package/src/compile/plan/literals.js +39 -0
- package/src/compile/plan/scope.js +430 -23
- package/src/compile/program-facts.js +732 -38
- package/src/ctx.js +64 -9
- package/src/helper-counters.js +7 -1
- package/src/ir.js +113 -5
- package/src/kind-traits.js +66 -3
- package/src/kind.js +84 -12
- package/src/op-policy.js +7 -4
- package/src/optimize/index.js +1179 -704
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +982 -67
- package/src/prepare/index.js +809 -67
- package/src/prepare/math-kernel.js +331 -0
- package/src/prepare/pre-eval.js +714 -0
- package/src/snapshot.js +194 -0
- package/src/type.js +1170 -56
- package/src/wat/assemble.js +403 -65
- package/src/wat/codegen.js +74 -14
- package/transform.js +113 -4
- package/wasi.js +3 -0
package/src/prepare/index.js
CHANGED
|
@@ -74,6 +74,18 @@ let funcLocalNames
|
|
|
74
74
|
// Lets the `.`-handler tell a function receiver — where `.caller`/`.callee` are
|
|
75
75
|
// prohibited introspection — from a data object that merely has such a field.
|
|
76
76
|
let funcValueNames
|
|
77
|
+
// Per-module set of top-level names WRITTEN beyond their declaration (bare-name
|
|
78
|
+
// assign/compound/++ anywhere in the module, locals-shadowed writes excluded).
|
|
79
|
+
// Gates defFunc: a depth-0 `let g = (…) => …` lifts into a fixed NAMED FUNCTION,
|
|
80
|
+
// sound only while the binding is immutable — JS lets a `let`/`var` function
|
|
81
|
+
// binding be reassigned (even from inside a function), and lifting such a
|
|
82
|
+
// binding froze callers onto the first value (reads resolved to the minted
|
|
83
|
+
// function; the write targeted a binding that no longer existed — "'g' is not
|
|
84
|
+
// in scope" / silently-stale first arrow). Mirror of fn-namespace's multiProp
|
|
85
|
+
// demotion: a reassigned name stays an ordinary closure-valued global
|
|
86
|
+
// (writable, indirect-callable); devirtGlobalCalls re-devirts the init-order-
|
|
87
|
+
// resolvable cases afterward. Stacked per module (recursive imports swap it).
|
|
88
|
+
let reassignedTopLevel
|
|
77
89
|
|
|
78
90
|
const resetPrepState = () => {
|
|
79
91
|
depth = 0
|
|
@@ -82,6 +94,63 @@ const resetPrepState = () => {
|
|
|
82
94
|
assignedStaticGlobals = new Set()
|
|
83
95
|
funcLocalNames = [new Set()]
|
|
84
96
|
funcValueNames = [new Set()]
|
|
97
|
+
reassignedTopLevel = new Set()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Bare-name write targets across a module root, scope-tracked: a write to a
|
|
101
|
+
// same-named LOCAL (arrow param, or a let/const anywhere in the enclosing
|
|
102
|
+
// function body — the function-scope approximation the sibling scans use)
|
|
103
|
+
// does not count. Over-demotion is sound but taxes a lifted function with the
|
|
104
|
+
// closure convention for nothing, so shadowed writes are excluded.
|
|
105
|
+
const scanReassignedTopLevel = (root) => {
|
|
106
|
+
const out = new Set()
|
|
107
|
+
const isWriteOp = (op) => op === '++' || op === '--' ||
|
|
108
|
+
(typeof op === 'string' && op.endsWith('=') && ASSIGN_OPS.has(op))
|
|
109
|
+
const declaredIn = (body, bound) => {
|
|
110
|
+
const walk = (n) => {
|
|
111
|
+
if (!Array.isArray(n)) return
|
|
112
|
+
if (n[0] === '=>') return
|
|
113
|
+
if ((n[0] === 'let' || n[0] === 'const' || n[0] === 'var') && n.length >= 2) {
|
|
114
|
+
for (let i = 1; i < n.length; i++) {
|
|
115
|
+
const d = n[i]
|
|
116
|
+
if (typeof d === 'string') bound.add(d)
|
|
117
|
+
else if (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string') bound.add(d[1])
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (n[0] === 'catch' && typeof n[1] === 'string') bound.add(n[1])
|
|
121
|
+
for (let i = 1; i < n.length; i++) walk(n[i])
|
|
122
|
+
}
|
|
123
|
+
walk(body)
|
|
124
|
+
}
|
|
125
|
+
const walk = (n, bound) => {
|
|
126
|
+
if (!Array.isArray(n)) return
|
|
127
|
+
if (n[0] === '=>') {
|
|
128
|
+
const inner = new Set(bound)
|
|
129
|
+
for (const p of extractParams(n[1])) {
|
|
130
|
+
const c = classifyParam(p)
|
|
131
|
+
if (c?.name) inner.add(c.name)
|
|
132
|
+
}
|
|
133
|
+
declaredIn(n[2], inner)
|
|
134
|
+
walk(n[2], inner)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
// A declarator's own `=` is the DECLARATION, not a reassignment — descend
|
|
138
|
+
// only into each declarator's init expression.
|
|
139
|
+
if ((n[0] === 'let' || n[0] === 'const' || n[0] === 'var') && n.length >= 2) {
|
|
140
|
+
for (let i = 1; i < n.length; i++) {
|
|
141
|
+
const d = n[i]
|
|
142
|
+
if (Array.isArray(d) && d[0] === '=') walk(d[2], bound)
|
|
143
|
+
else if (Array.isArray(d)) walk(d, bound)
|
|
144
|
+
}
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
if (isWriteOp(n[0]) && typeof n[1] === 'string' && !bound.has(n[1])) out.add(n[1])
|
|
148
|
+
for (let i = 1; i < n.length; i++) walk(n[i], bound)
|
|
149
|
+
}
|
|
150
|
+
// Top-level declarations don't shadow — they ARE the bindings being tested;
|
|
151
|
+
// a top-level `g = …` after `let g = …` is exactly the reassignment case.
|
|
152
|
+
walk(root, new Set())
|
|
153
|
+
return out
|
|
85
154
|
}
|
|
86
155
|
|
|
87
156
|
// ES spec: identifier with \uHHHH or \u{...} escape is equivalent to the decoded
|
|
@@ -196,6 +265,19 @@ const stripBoolNot = c => {
|
|
|
196
265
|
while (Array.isArray(c) && c[0] === '!' && Array.isArray(c[1]) && c[1][0] === '!') c = c[1][1]
|
|
197
266
|
return c
|
|
198
267
|
}
|
|
268
|
+
// In a statement (value-discarded) position, postfix `x++`/`x--` is lowered to `(++x) − 1` /
|
|
269
|
+
// `(--x) + 1` to recover the old value — but nobody reads it, so drop the ∓1 and keep the bare
|
|
270
|
+
// increment. (`obj.p++` lowers via `obj.p = obj.p + 1`, also wrapped.) Cleaner AST for the loop/
|
|
271
|
+
// recurrence passes; codegen already discarded the ∓1, so this is purely canonicalization.
|
|
272
|
+
const isOne = n => Array.isArray(n) && n[0] == null && n[1] === 1
|
|
273
|
+
const dropDeadPostfix = s => {
|
|
274
|
+
if (Array.isArray(s) && s.length === 3 && isOne(s[2]) && Array.isArray(s[1])) {
|
|
275
|
+
const inner = s[1][0]
|
|
276
|
+
if ((s[0] === '-' && (inner === '++' || inner === '=')) ||
|
|
277
|
+
(s[0] === '+' && (inner === '--' || inner === '='))) return s[1]
|
|
278
|
+
}
|
|
279
|
+
return s
|
|
280
|
+
}
|
|
199
281
|
const stringValue = node => Array.isArray(node) && node[0] == null && typeof node[1] === 'string' ? node[1] : null
|
|
200
282
|
const MUTATING_ARRAY_METHODS = new Set(['copyWithin', 'fill', 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'])
|
|
201
283
|
|
|
@@ -274,6 +356,67 @@ function collectTopLevelStaticAssignments(node, facts) {
|
|
|
274
356
|
if (str != null || arr) facts.set(node[1], { str, arr })
|
|
275
357
|
}
|
|
276
358
|
|
|
359
|
+
/** `[c0,c1,…][i]` inside a function body allocates the literal PER EVALUATION —
|
|
360
|
+
* the '[' static-data lowering is module-scope-gated because a NAMED local
|
|
361
|
+
* literal could leak per-instance mutations across calls. A literal in the
|
|
362
|
+
* RECEIVER position of its own read can neither escape nor be written, so
|
|
363
|
+
* hoist it to a synthetic module-level const: one shared data segment + the
|
|
364
|
+
* staticArrs base/len fold, with duplicates interned by content (beat-style
|
|
365
|
+
* samplers read several such tables per sample — 3×144 B allocs/sample in the
|
|
366
|
+
* Sierpinski floatbeat; a const index rides the same path and folds all the
|
|
367
|
+
* way to a constant). Elements: number literals (incl. unary minus) only —
|
|
368
|
+
* exactly the static-extractable set the '[' lowering takes. */
|
|
369
|
+
function hoistIndexedConstLiterals(root) {
|
|
370
|
+
const lits = new Map() // content key → synthetic const name
|
|
371
|
+
const decls = []
|
|
372
|
+
// Parse shapes: number literal = [null, n]; unary minus = ['-', lit];
|
|
373
|
+
// array literal = ['[]', elems] (unary '[]'), elems = [',', ...] | one lit | undefined;
|
|
374
|
+
// subscript = ['[]', receiver, index] (binary '[]').
|
|
375
|
+
const litVal = (e) => Array.isArray(e) && e.length === 2 && e[0] == null && typeof e[1] === 'number' ? e[1]
|
|
376
|
+
: Array.isArray(e) && e[0] === '-' && e.length === 2 ? (v => v === null ? null : -v)(litVal(e[1]))
|
|
377
|
+
: null
|
|
378
|
+
// A literal read in WRITE position (`[1,2][0] = 5`, `[1,2][k]++`, `delete [1,2][0]`,
|
|
379
|
+
// destructuring targets) must keep its fresh per-evaluation array — rewriting it
|
|
380
|
+
// would mutate the shared segment under every other read interned to the same
|
|
381
|
+
// content. Post-order rewrites children before the parent assign is visible, so
|
|
382
|
+
// collect banned '[]' nodes in a first pass over every assignment-target subtree.
|
|
383
|
+
const banned = new Set()
|
|
384
|
+
const banIn = (t) => { if (!Array.isArray(t)) return; if (t[0] === '[]') banned.add(t); for (let i = 1; i < t.length; i++) banIn(t[i]) }
|
|
385
|
+
const collectBans = (node) => {
|
|
386
|
+
if (!Array.isArray(node)) return
|
|
387
|
+
const op = node[0]
|
|
388
|
+
if (typeof op === 'string' && (op === '++' || op === '--' || op === 'delete' || op === '=' ||
|
|
389
|
+
(op.length >= 2 && op.endsWith('=') && !['==', '===', '!=', '!==', '<=', '>='].includes(op))))
|
|
390
|
+
banIn(node[1])
|
|
391
|
+
for (let i = 1; i < node.length; i++) collectBans(node[i])
|
|
392
|
+
}
|
|
393
|
+
collectBans(root)
|
|
394
|
+
const walk = (node) => {
|
|
395
|
+
if (!Array.isArray(node)) return
|
|
396
|
+
for (let i = 1; i < node.length; i++) walk(node[i])
|
|
397
|
+
if (node[0] !== '[]' || node.length !== 3 || banned.has(node)) return
|
|
398
|
+
const lit = node[1]
|
|
399
|
+
if (!Array.isArray(lit) || lit[0] !== '[]' || lit.length !== 2) return
|
|
400
|
+
const inner = lit[1]
|
|
401
|
+
const elems = Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : inner === undefined ? [] : [inner]
|
|
402
|
+
if (!elems.length) return
|
|
403
|
+
const vals = elems.map(litVal)
|
|
404
|
+
if (vals.some(v => v === null)) return
|
|
405
|
+
const key = vals.join(',')
|
|
406
|
+
let name = lits.get(key)
|
|
407
|
+
if (name == null) {
|
|
408
|
+
name = `__salit${lits.size}`
|
|
409
|
+
lits.set(key, name)
|
|
410
|
+
decls.push(['const', ['=', name, lit]])
|
|
411
|
+
}
|
|
412
|
+
node[1] = name
|
|
413
|
+
}
|
|
414
|
+
walk(root)
|
|
415
|
+
if (!decls.length) return root
|
|
416
|
+
if (Array.isArray(root) && root[0] === ';') { root.splice(1, 0, ...decls); return root }
|
|
417
|
+
return [';', ...decls, root]
|
|
418
|
+
}
|
|
419
|
+
|
|
277
420
|
function seedStaticGlobalAssignments(node) {
|
|
278
421
|
// jzify hoists function declarations ahead of `var` initializer assignments.
|
|
279
422
|
// Seed one-write static globals before preparing those function bodies so
|
|
@@ -375,15 +518,6 @@ function staticStringExpr(node) {
|
|
|
375
518
|
}
|
|
376
519
|
return out
|
|
377
520
|
}
|
|
378
|
-
if (op === '``' && Array.isArray(args[0]) && args[0][0] === '.' && args[0][1] === 'String' && args[0][2] === 'raw') {
|
|
379
|
-
let out = ''
|
|
380
|
-
for (const part of args.slice(1)) {
|
|
381
|
-
const s = staticStringExpr(part)
|
|
382
|
-
if (s == null) return null
|
|
383
|
-
out += s
|
|
384
|
-
}
|
|
385
|
-
return out
|
|
386
|
-
}
|
|
387
521
|
if (op === '()' && Array.isArray(args[0]) && args[0][0] === '.' && args[0][2] === 'join' && typeof args[0][1] === 'string') {
|
|
388
522
|
const arr = lookupStaticStringArray(args[0][1])
|
|
389
523
|
if (!arr) return null
|
|
@@ -414,7 +548,8 @@ function recordModuleInitFacts(root) {
|
|
|
414
548
|
dynVars: new Set(), dynWriteVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
415
549
|
hasFuncValue: false, timerNames: new Set(),
|
|
416
550
|
maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false,
|
|
417
|
-
writtenProps: new Set(),
|
|
551
|
+
writtenProps: new Set(), literalWriteKeys: new Map(),
|
|
552
|
+
arrResized: new Set(), nameEscapes: new Set(),
|
|
418
553
|
}
|
|
419
554
|
const visitFuncValue = (node) => {
|
|
420
555
|
if (facts.hasFuncValue || !Array.isArray(node)) return
|
|
@@ -495,6 +630,8 @@ export default function prepare(node) {
|
|
|
495
630
|
normalizeIdents(node)
|
|
496
631
|
fuseSparseMapReads(node) // AST-level fusion; needs pre-resolution shape — defined at end of file
|
|
497
632
|
seedStaticGlobalAssignments(node)
|
|
633
|
+
node = hoistIndexedConstLiterals(node)
|
|
634
|
+
reassignedTopLevel = scanReassignedTopLevel(node)
|
|
498
635
|
const ast = prep(node)
|
|
499
636
|
// Top-level functions referenced as first-class values (e.g. `let o = { fn: g }`,
|
|
500
637
|
// `arr.push(g)`, `return g`) need trampoline emission, which depends on the fn
|
|
@@ -612,7 +749,7 @@ function isDeclared(name) {
|
|
|
612
749
|
|
|
613
750
|
function pushScope(scope = new Map()) {
|
|
614
751
|
scopes.push(scope)
|
|
615
|
-
staticConstScopes.push({ strings: new Map(), arrays: new Map() })
|
|
752
|
+
staticConstScopes.push({ strings: new Map(), arrays: new Map(), consts: new Set() })
|
|
616
753
|
}
|
|
617
754
|
|
|
618
755
|
function popScope() {
|
|
@@ -670,8 +807,17 @@ const hasFunc = name => ctx.func.names.has(name)
|
|
|
670
807
|
// fast-paths bail and fall through to `resolveCallee`, which already routes a
|
|
671
808
|
// declared name to its local value. Mirrors the guard in
|
|
672
809
|
// `foldNamespaceIntrospection`.
|
|
810
|
+
// …EXCEPT a namespace alias (`const M = Math` at any depth): registerBuiltinAlias
|
|
811
|
+
// maps the name to the MODULE ITSELF in the block scope — that's the namespace,
|
|
812
|
+
// not a shadow of it. An ordinary local can never carry that resolution (only the
|
|
813
|
+
// hasModule-gated alias branch writes module names into scope maps).
|
|
814
|
+
const isNamespaceAliasScoped = name => {
|
|
815
|
+
if (!scopes.length || !isDeclared(name)) return false
|
|
816
|
+
const key = resolveScope(name)
|
|
817
|
+
return typeof key === 'string' && key !== name && (hasModule(key) || !!builtinMemberKey(key))
|
|
818
|
+
}
|
|
673
819
|
const shadowsBuiltin = name => typeof name === 'string' &&
|
|
674
|
-
((scopes.length && isDeclared(name)) || hasFunc(name) || ctx.scope.userGlobals?.has?.(name))
|
|
820
|
+
((scopes.length && isDeclared(name) && !isNamespaceAliasScoped(name)) || hasFunc(name) || ctx.scope.userGlobals?.has?.(name))
|
|
675
821
|
// A local bound to a function literal in any active arrow scope (the nested-
|
|
676
822
|
// closure counterpart to `hasFunc`, which only knows depth-0 lifted functions).
|
|
677
823
|
const isFuncValueLocal = name => typeof name === 'string' && funcValueNames.some(s => s.has(name))
|
|
@@ -749,6 +895,25 @@ function resolveTypeof(node) {
|
|
|
749
895
|
return node
|
|
750
896
|
}
|
|
751
897
|
|
|
898
|
+
// Always-truthy / always-falsy over PREPPED IR: literals plus the short-circuit
|
|
899
|
+
// lattice — `a || b` is always-truthy when either arm always is, `a && b` when
|
|
900
|
+
// both are; duals for falsy. Powers dead-arm elimination in the '||'/'&&'
|
|
901
|
+
// handlers: resolveTypeof folds a guard arm to a literal mid-chain
|
|
902
|
+
// (`x || typeof g === 'undefined' || g.member`) and left-associativity buries
|
|
903
|
+
// it one level deep, where emit's literal-LHS fold never looks. Dropping the
|
|
904
|
+
// dead tail at prep keeps its host-global reads out of the import section.
|
|
905
|
+
const litTruth = n => Array.isArray(n) && n.length === 2 && n[0] == null ? !!n[1]
|
|
906
|
+
: Array.isArray(n) && n[0] === 'str' && typeof n[1] === 'string' ? !!n[1] : null
|
|
907
|
+
const alwaysTruthy = (n) => litTruth(n) ?? (Array.isArray(n) &&
|
|
908
|
+
(n[0] === '||' ? alwaysTruthy(n[1]) || alwaysTruthy(n[2])
|
|
909
|
+
: n[0] === '&&' && alwaysTruthy(n[1]) && alwaysTruthy(n[2])))
|
|
910
|
+
const alwaysFalsy = (n) => {
|
|
911
|
+
const l = litTruth(n)
|
|
912
|
+
return l != null ? !l : Array.isArray(n) &&
|
|
913
|
+
(n[0] === '&&' ? alwaysFalsy(n[1]) || alwaysFalsy(n[2])
|
|
914
|
+
: n[0] === '||' && alwaysFalsy(n[1]) && alwaysFalsy(n[2]))
|
|
915
|
+
}
|
|
916
|
+
|
|
752
917
|
// Prepare a strict `===`/`!==`. resolveTypeof may fold `typeof x === 'type'` to a
|
|
753
918
|
// literal or rewrite it to a numeric-code compare; either way we prep the result's
|
|
754
919
|
// operands directly. The strict op stays intact (no collapse to loose `==`) so
|
|
@@ -805,6 +970,9 @@ function prep(node) {
|
|
|
805
970
|
if (node in CONSTANTS) return [, CONSTANTS[node]]
|
|
806
971
|
if (node in F64_CONSTANTS) return [, F64_CONSTANTS[node]]
|
|
807
972
|
if (REJECT_IDENTS[node]) err(REJECT_IDENTS[node])
|
|
973
|
+
// A bare #name ident outside its class body: the `#field in obj` brand check
|
|
974
|
+
// (or a leaked private name). Reject with intent, not "not in scope".
|
|
975
|
+
if (node[0] === '#') err(`private name '${node}' — \`#field in obj\` brand checks are not supported`)
|
|
808
976
|
// Boolean/Number as value → identity arrow (for .filter(Boolean), .map(Number) etc.)
|
|
809
977
|
if (node === 'Boolean' || node === 'Number') { includeForCallableValue(); return ['=>', 'x', 'x'] }
|
|
810
978
|
// Block locals shadow module imports/globals, even when the local keeps the same name.
|
|
@@ -828,11 +996,26 @@ function prep(node) {
|
|
|
828
996
|
}
|
|
829
997
|
|
|
830
998
|
const [op, ...args] = node
|
|
831
|
-
if (op === 'void' && ctx.transform.strict) err('strict mode: `void` is prohibited
|
|
832
|
-
// jz's `==`/`!=`
|
|
833
|
-
//
|
|
999
|
+
if (op === 'void' && ctx.transform.strict) err('strict mode: `void` is prohibited — write `undefined`.')
|
|
1000
|
+
// jz's `==`/`!=` follow JS loose equality (statically-known mixed types coerce:
|
|
1001
|
+
// `1 == "1"` is true), so default mode accepts them for JS parity. strict enforces
|
|
1002
|
+
// the canonical subset, where `===`/`!==` are the one spelling — reject the loose form.
|
|
834
1003
|
if ((op === '==' || op === '!=') && ctx.transform.strict)
|
|
835
|
-
err(`strict mode: \`${op}\` is prohibited — use \`${op}
|
|
1004
|
+
err(`strict mode: \`${op}\` is prohibited — use \`${op}=\` (\`jz --jzify\` converts). jz's \`${op}\` follows JS loose equality; the canonical subset spells equality \`===\`/\`!==\` only.`)
|
|
1005
|
+
// A builtin-namespace member alias (`let sin = Math.sin`, `let {sin} = Math`)
|
|
1006
|
+
// carries no storage — writing through it would silently target nothing.
|
|
1007
|
+
// Catch every write form (`=`, compound `+=`-family, `++`/`--`) here, ahead
|
|
1008
|
+
// of per-op handlers, so none of them need their own copy of this check.
|
|
1009
|
+
if ((ASSIGN_OPS.has(op) || op === '++' || op === '--') && typeof args[0] === 'string') {
|
|
1010
|
+
const aliasKey = builtinAliasKeyOf(args[0])
|
|
1011
|
+
if (aliasKey) err(`Cannot reassign '${args[0]}' — bound to builtin '${aliasKey}' via alias/destructuring; builtin-namespace bindings are compile-time only, not writable storage`)
|
|
1012
|
+
// Assignment to a const binding is a compile error (ES: runtime TypeError).
|
|
1013
|
+
// Resolve through the live block scopes so a shadowing `let` of the same
|
|
1014
|
+
// name stays writable; module-level consts are guarded by emit's isConst.
|
|
1015
|
+
const target = scopes.length && isDeclared(args[0]) ? resolveScope(args[0]) : args[0]
|
|
1016
|
+
if (typeof target === 'string' && staticConstScopes.some(f => f.consts?.has(target)))
|
|
1017
|
+
err(`Assignment to constant '${args[0]}' (TypeError in JS)`)
|
|
1018
|
+
}
|
|
836
1019
|
if (op == null) {
|
|
837
1020
|
if (typeof args[0] === 'string') {
|
|
838
1021
|
includeForStringValue()
|
|
@@ -854,6 +1037,8 @@ function prep(node) {
|
|
|
854
1037
|
// need a list of these names to know what to emit, so the table lives here either way.
|
|
855
1038
|
export const GLOBALS = Object.assign(Object.create(null), {
|
|
856
1039
|
Math: 'math',
|
|
1040
|
+
fs: 'fs',
|
|
1041
|
+
fetch: 'web',
|
|
857
1042
|
Number: 'Number',
|
|
858
1043
|
Array: 'Array',
|
|
859
1044
|
Object: 'Object',
|
|
@@ -884,7 +1069,10 @@ export const GLOBALS = Object.assign(Object.create(null), {
|
|
|
884
1069
|
TextDecoder: 'TextDecoder',
|
|
885
1070
|
})
|
|
886
1071
|
|
|
887
|
-
|
|
1072
|
+
// `,` is the ordinary pattern separator; `;` appears when a `{…}` pattern parsed
|
|
1073
|
+
// in STATEMENT position (for-of head cover grammar: `for ({ x = 1 } of …)`) —
|
|
1074
|
+
// same items, block-shaped node.
|
|
1075
|
+
const patternItems = (node) => (node?.[0] === ',' || node?.[0] === ';') ? node.slice(1) : [node]
|
|
888
1076
|
const isDestructPattern = (node) => Array.isArray(node) && (node[0] === '[]' || node[0] === '{}')
|
|
889
1077
|
|
|
890
1078
|
// Element count of a prepared inline array literal `['[', e0, e1, …]` with no
|
|
@@ -943,9 +1131,37 @@ function bindingNames(pattern, out = new Set()) {
|
|
|
943
1131
|
return out
|
|
944
1132
|
}
|
|
945
1133
|
|
|
1134
|
+
/** Does any arrow inside `node` reference `name`? The capture test for the
|
|
1135
|
+
* per-iteration for-head `let` lowering (pay only when actually captured). */
|
|
1136
|
+
function bodyCapturesName(node, name) {
|
|
1137
|
+
if (!Array.isArray(node)) return false
|
|
1138
|
+
if (node[0] === '=>') return refsName(node[2], name, { skipArrow: false })
|
|
1139
|
+
for (let i = 1; i < node.length; i++) if (bodyCapturesName(node[i], name)) return true
|
|
1140
|
+
return false
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/** Rename bare identifiers per `map` — literal nodes and non-computed property
|
|
1144
|
+
* keys stay untouched. Used to point a for-head's cond/step at the carrier. */
|
|
1145
|
+
function substIdents(node, map) {
|
|
1146
|
+
if (typeof node === 'string') return map.get(node) ?? node
|
|
1147
|
+
if (!Array.isArray(node) || node[0] == null) return node
|
|
1148
|
+
if (node[0] === 'str') return node
|
|
1149
|
+
if (node[0] === '.') return ['.', substIdents(node[1], map), node[2]]
|
|
1150
|
+
// Property/label key position is not an identifier read (`{ i: i }` in a
|
|
1151
|
+
// for-head cond must rename only the VALUE side).
|
|
1152
|
+
if (node[0] === ':' && typeof node[1] === 'string') return [':', node[1], ...node.slice(2).map(n => substIdents(n, map))]
|
|
1153
|
+
return [node[0], ...node.slice(1).map(n => substIdents(n, map))]
|
|
1154
|
+
}
|
|
1155
|
+
|
|
946
1156
|
function pushPatternAssign(target, valueExpr, out, decls = null) {
|
|
947
1157
|
if (Array.isArray(target) && target[0] === '=') {
|
|
948
|
-
|
|
1158
|
+
// Destructuring default fires ONLY on undefined (ES §13.15.5.3) — `??` would
|
|
1159
|
+
// also fire on null (`[a = 1] = [null]` must leave a null). Spill the read
|
|
1160
|
+
// once, test against undefined, keep the default lazily evaluated.
|
|
1161
|
+
const tmp = `${T}d${ctx.func.uniq++}`
|
|
1162
|
+
if (decls) decls.push(['=', tmp, valueExpr])
|
|
1163
|
+
else out.push(['=', tmp, valueExpr])
|
|
1164
|
+
pushPatternAssign(target[1], ['?:', ['===', tmp, [, JZ_UNDEF]], prep(target[2]), tmp], out, decls)
|
|
949
1165
|
return
|
|
950
1166
|
}
|
|
951
1167
|
|
|
@@ -1013,8 +1229,9 @@ function expandDestruct(pattern, source, out, decls = null, srcLen = null) {
|
|
|
1013
1229
|
}
|
|
1014
1230
|
|
|
1015
1231
|
if (Array.isArray(item) && item[0] === '=') {
|
|
1232
|
+
// Route through pushPatternAssign's `=` case: undefined-only default.
|
|
1016
1233
|
if (typeof item[1] === 'string')
|
|
1017
|
-
pushPatternAssign(item
|
|
1234
|
+
pushPatternAssign(item, ['.', source, item[1]], out, decls)
|
|
1018
1235
|
continue
|
|
1019
1236
|
}
|
|
1020
1237
|
|
|
@@ -1022,7 +1239,17 @@ function expandDestruct(pattern, source, out, decls = null, srcLen = null) {
|
|
|
1022
1239
|
const key = item[1]
|
|
1023
1240
|
const computedKey = Array.isArray(key) && key[0] === '[]' && key.length === 2 ? key[1] : null
|
|
1024
1241
|
if (computedKey) includeForArrayAccess()
|
|
1025
|
-
|
|
1242
|
+
// Numeric key (`{ 0: v, length: z } = arr`) — an index read, not a dot-key:
|
|
1243
|
+
// the static-key path hashes STRING keys only (and arrays index natively).
|
|
1244
|
+
// The parser yields the key as a literal node `[null, 0]` (raw number in
|
|
1245
|
+
// synthesized shapes).
|
|
1246
|
+
const numKey = typeof key === 'number' ? key
|
|
1247
|
+
: Array.isArray(key) && key.length === 2 && key[0] == null && typeof key[1] === 'number' ? key[1]
|
|
1248
|
+
: null
|
|
1249
|
+
const read = computedKey ? ['[]', source, computedKey]
|
|
1250
|
+
: numKey != null ? (includeForArrayAccess(), ['[]', source, [, numKey]])
|
|
1251
|
+
: ['.', source, key]
|
|
1252
|
+
pushPatternAssign(item[2], read, out, decls)
|
|
1026
1253
|
continue
|
|
1027
1254
|
}
|
|
1028
1255
|
}
|
|
@@ -1047,6 +1274,196 @@ function expandDestruct(pattern, source, out, decls = null, srcLen = null) {
|
|
|
1047
1274
|
}
|
|
1048
1275
|
}
|
|
1049
1276
|
|
|
1277
|
+
// --- Builtin-namespace member aliasing --------------------------------------
|
|
1278
|
+
// `let/const name = NS.member` (`let sin = Math.sin`) and destructuring
|
|
1279
|
+
// (`let { sin, PI } = Math`, incl. rename `{ pow: myPow }`) bind straight to
|
|
1280
|
+
// the resolved emit key (`math.sin`) instead of materializing a real global —
|
|
1281
|
+
// there's no first-class "Math.sin" runtime value, only the compiler's own
|
|
1282
|
+
// dispatch table, so the alias makes every later reference to `name` behave
|
|
1283
|
+
// exactly as if the source had written `Math.sin` there directly:
|
|
1284
|
+
// - `name(x)` — the bare-identifier branch in `prep()` (and `resolveCallee`)
|
|
1285
|
+
// already returns a dotted `scope.chain`/block-scope entry bare, so the
|
|
1286
|
+
// call lowers straight to `$math.sin`, no boxing, no arity ceiling (the
|
|
1287
|
+
// general shape behind the `const alias = fn` fast path above).
|
|
1288
|
+
// - a bare non-call reference falls through to the SAME first-class-value /
|
|
1289
|
+
// constant-fold path a literal `Math.sin` reference hits at emit time
|
|
1290
|
+
// (`builtinFunctionValue` / arity-0 constant fold) — succeeds or fails
|
|
1291
|
+
// identically to the dotted form; never silently wrong.
|
|
1292
|
+
// Exports and reassignment are rejected with a clear error (see `registerBuiltinAlias`
|
|
1293
|
+
// and the reassignment guard in the main `prep()` dispatch) rather than
|
|
1294
|
+
// silently targeting no storage.
|
|
1295
|
+
|
|
1296
|
+
/** `node` is the flat dotted emit key prep's own `.` handler would produce for
|
|
1297
|
+
* `NS.member` (e.g. `'math.sin'`) — i.e. a real, already-resolved builtin
|
|
1298
|
+
* reference, not an ordinary value/expression. */
|
|
1299
|
+
function builtinMemberKey(node) {
|
|
1300
|
+
return typeof node === 'string' && node.includes('.') && ctx.core.emit[node] != null ? node : null
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/** Pure syntactic extraction of `{ a, b: c }` → `[[target, member], …]` (handles
|
|
1304
|
+
* rename). Returns null for any shape a plain namespace has no notion of: rest,
|
|
1305
|
+
* defaults, computed keys, nested patterns. Shared by the declaration-form
|
|
1306
|
+
* alias path (`namespaceMemberAliases`) and the assignment-form path
|
|
1307
|
+
* (`namespaceMemberAssigns`) below — they differ only in what they do with
|
|
1308
|
+
* each [target, member] pair. */
|
|
1309
|
+
function namespaceObjectPatternPairs(pattern) {
|
|
1310
|
+
if (!Array.isArray(pattern) || pattern[0] !== '{}' || pattern.length !== 2) return null
|
|
1311
|
+
const items = patternItems(pattern[1])
|
|
1312
|
+
const pairs = []
|
|
1313
|
+
for (const item of items) {
|
|
1314
|
+
if (typeof item === 'string') pairs.push([item, item])
|
|
1315
|
+
else if (Array.isArray(item) && item[0] === ':' && typeof item[1] === 'string' && typeof item[2] === 'string')
|
|
1316
|
+
pairs.push([item[2], item[1]])
|
|
1317
|
+
else return null
|
|
1318
|
+
}
|
|
1319
|
+
return pairs
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/** `let { a, b: c } = NS` where NS is a known builtin module — expand to one
|
|
1323
|
+
* alias per key (handles rename). Returns null (falls through to the generic
|
|
1324
|
+
* runtime-destructure path) for any shape `namespaceObjectPatternPairs` rejects,
|
|
1325
|
+
* or an unknown member. */
|
|
1326
|
+
function namespaceMemberAliases(pattern, mod) {
|
|
1327
|
+
const pairs = namespaceObjectPatternPairs(pattern)
|
|
1328
|
+
if (!pairs) return null
|
|
1329
|
+
// Module init (registers the mod's ctx.core.emit['mod.member'] handlers) is
|
|
1330
|
+
// lazy — same as the '.' handler's own `includeModule(mod)` call — so it must
|
|
1331
|
+
// run BEFORE the emit-key lookups below, not after.
|
|
1332
|
+
includeModule(mod)
|
|
1333
|
+
const aliases = []
|
|
1334
|
+
for (const [target, member] of pairs) {
|
|
1335
|
+
const key = `${mod}.${member}`
|
|
1336
|
+
if (ctx.core.emit[key] == null) return null
|
|
1337
|
+
aliases.push([target, key])
|
|
1338
|
+
}
|
|
1339
|
+
return aliases
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/** `({ a, b: c } = NS)` — assignment-form namespace destructure. Unlike the
|
|
1343
|
+
* declaration form above, each target is a PRE-EXISTING binding (a real local/
|
|
1344
|
+
* global, or itself another alias), not a fresh one — so it can't be resolved
|
|
1345
|
+
* to a compile-time-only alias; it needs a real assignment. Lower to one plain
|
|
1346
|
+
* `target = NS.member` per key, reusing the raw (unprepped) `NS` node so the
|
|
1347
|
+
* ordinary `.` handler does the module-include/arity/shadow work, exactly as
|
|
1348
|
+
* it would for a literal `target = NS.member` written by hand — proven to
|
|
1349
|
+
* compile and run correctly (see the reassignment-into-a-real-binding case
|
|
1350
|
+
* the `.` handler already supports). Returns null for the same unsupported
|
|
1351
|
+
* shapes `namespaceObjectPatternPairs` rejects. */
|
|
1352
|
+
function namespaceMemberAssigns(pattern, rhsRaw) {
|
|
1353
|
+
const pairs = namespaceObjectPatternPairs(pattern)
|
|
1354
|
+
if (!pairs) return null
|
|
1355
|
+
return pairs.map(([target, member]) => ['=', target, ['.', rhsRaw, member]])
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/** Bind `name` to builtin emit key `key` at the current scope (module
|
|
1359
|
+
* `scope.chain` at depth 0, block scope otherwise) instead of declaring a
|
|
1360
|
+
* real global/local — mirrors the `const alias = fn` function-alias fast
|
|
1361
|
+
* path in `prepDecl`. `includeForCallableValue` is pre-armed exactly when the
|
|
1362
|
+
* '.' handler would arm it (arity > 0), so an incidental first-class use
|
|
1363
|
+
* (`let g = sin` elsewhere) still finds closure support wired up. */
|
|
1364
|
+
function registerBuiltinAlias(name, key) {
|
|
1365
|
+
if (ctx.func.exports[name]) {
|
|
1366
|
+
// A CONSTANT member (Math.PI — an arity-0 value emitter) exported by name
|
|
1367
|
+
// needs real storage, not a wrapper function: `Math.max(1, …)` used to
|
|
1368
|
+
// synthesize `(a) => math.PI(a)` here, so importers doing arithmetic on PI
|
|
1369
|
+
// got a closure — NaN (the window-function taylor memo died on A = …/PI).
|
|
1370
|
+
// Return false: the caller falls through to an ordinary global declaration
|
|
1371
|
+
// whose init emits the constant.
|
|
1372
|
+
if ((emitArity(ctx.core.emit[key]) || 0) === 0) return false
|
|
1373
|
+
// An alias carries no runtime storage, but an EXPORT needs some — synthesize
|
|
1374
|
+
// the wrapping function the old error told users to write by hand
|
|
1375
|
+
// (`export let { sin, cos } = Math` — window-function's util.js — must just
|
|
1376
|
+
// work). Arity from the emitter; in-module calls direct-call the wrapper,
|
|
1377
|
+
// which inlines back to the builtin under watr.
|
|
1378
|
+
const arity = Math.max(1, emitArity(ctx.core.emit[key]) || 1)
|
|
1379
|
+
const params = Array.from({ length: arity }, (_, i) => `${T}ba${i}`)
|
|
1380
|
+
const paramsNode = params.length === 1 ? params[0] : [',', ...params]
|
|
1381
|
+
const wrapped = prep(['=>', paramsNode, ['()', key, params.length === 1 ? params[0] : [',', ...params]]])
|
|
1382
|
+
if (defFunc(name, wrapped)) return true
|
|
1383
|
+
err(`'${name}' aliases builtin '${key}' and cannot be exported directly — export a wrapping function instead`)
|
|
1384
|
+
}
|
|
1385
|
+
if (emitArity(ctx.core.emit[key]) > 0) includeForCallableValue()
|
|
1386
|
+
if (depth === 0) {
|
|
1387
|
+
ctx.scope.chain[name] = key
|
|
1388
|
+
} else {
|
|
1389
|
+
const fnNames = funcLocalNames[funcLocalNames.length - 1]
|
|
1390
|
+
if (fnNames) fnNames.add(name)
|
|
1391
|
+
if (scopes.length > 0) scopes[scopes.length - 1].set(name, key)
|
|
1392
|
+
}
|
|
1393
|
+
return true
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/** True (returning the key) iff bare identifier `name` currently resolves — via
|
|
1397
|
+
* block scope or module `scope.chain` — to a builtin-member alias. Pure read,
|
|
1398
|
+
* no side effects; mirrors the resolution order of the bare-identifier branch
|
|
1399
|
+
* in `prep()` (block scope first, chain otherwise). Used by the reassignment
|
|
1400
|
+
* guard: an alias carries no storage, so `name = …` must error, not miscompile. */
|
|
1401
|
+
function builtinAliasKeyOf(name) {
|
|
1402
|
+
if (typeof name !== 'string') return null
|
|
1403
|
+
const key = scopes.length && isDeclared(name) ? resolveScope(name) : ctx.scope.chain[name]
|
|
1404
|
+
return builtinMemberKey(key)
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// jzify hoists top-level `function` declarations to the front of their
|
|
1408
|
+
// enclosing `;` block (mirroring JS function-hoisting — see jzify/transform.js
|
|
1409
|
+
// `transformScope`), so a hoisted function's body can be PREPPED — and any
|
|
1410
|
+
// builtin-namespace alias it references resolved — before a SIBLING
|
|
1411
|
+
// `let {sin} = Math` / `let sin = Math.sin` the function calls appears in the
|
|
1412
|
+
// statement list. Real JS gets away with this because the function isn't
|
|
1413
|
+
// CALLED until the whole block has finished initializing; jz's prepare pass
|
|
1414
|
+
// resolves each reference eagerly in one linear walk, so without this the
|
|
1415
|
+
// alias isn't registered yet and the reference falls through unresolved (a
|
|
1416
|
+
// dangling local at watr assembly, not a caught compile error). Scanning every
|
|
1417
|
+
// sibling `let`/`const` up front and registering any alias-shaped one makes
|
|
1418
|
+
// alias resolution order-independent within the block — matching how a REAL
|
|
1419
|
+
// global (declareGlobal) already resolves order-independently, since compile
|
|
1420
|
+
// (not prepare) looks those up by name after the whole module has been prepped.
|
|
1421
|
+
function preRegisterBuiltinAliases(stmts) {
|
|
1422
|
+
// A sibling `let Math = {…}` in this SAME block shadows the builtin even
|
|
1423
|
+
// though — being an unordered pre-scan — it hasn't been individually
|
|
1424
|
+
// prepped yet (so `shadowsBuiltin`/`userGlobals` don't know about it yet
|
|
1425
|
+
// either). Collect every name this block itself declares up front so the
|
|
1426
|
+
// scan below can treat it exactly like an outer-scope shadow.
|
|
1427
|
+
const blockDeclared = new Set()
|
|
1428
|
+
for (const stmt of stmts) {
|
|
1429
|
+
if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const')) continue
|
|
1430
|
+
for (const i of stmt.slice(1)) {
|
|
1431
|
+
const target = Array.isArray(i) && i[0] === '=' ? i[1] : i
|
|
1432
|
+
bindingNames(target, blockDeclared)
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
// Bare identifier `name` names an as-yet-unshadowed builtin module — null
|
|
1436
|
+
// when `name` is shadowed (by this block, an outer scope, a function, or a
|
|
1437
|
+
// user global) or simply isn't a known module name.
|
|
1438
|
+
const builtinModOf = (name) => {
|
|
1439
|
+
if (typeof name !== 'string' || blockDeclared.has(name) || shadowsBuiltin(name)) return null
|
|
1440
|
+
const mod = ctx.scope.chain[name]
|
|
1441
|
+
return mod && !mod.includes('.') && hasModule(mod) ? mod : null
|
|
1442
|
+
}
|
|
1443
|
+
for (const stmt of stmts) {
|
|
1444
|
+
if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const')) continue
|
|
1445
|
+
for (const i of stmt.slice(1)) {
|
|
1446
|
+
if (!Array.isArray(i) || i[0] !== '=') continue
|
|
1447
|
+
const [, name, init] = i
|
|
1448
|
+
if (isDestructPattern(name) && typeof init === 'string') {
|
|
1449
|
+
const mod = builtinModOf(init)
|
|
1450
|
+
if (mod) {
|
|
1451
|
+
const aliases = namespaceMemberAliases(name, mod)
|
|
1452
|
+
if (aliases) for (const [target, key] of aliases) registerBuiltinAlias(target, key)
|
|
1453
|
+
}
|
|
1454
|
+
} else if (!isDestructPattern(name) && typeof name === 'string' && Array.isArray(init) &&
|
|
1455
|
+
init[0] === '.' && typeof init[1] === 'string' && typeof init[2] === 'string') {
|
|
1456
|
+
const mod = builtinModOf(init[1])
|
|
1457
|
+
if (mod) {
|
|
1458
|
+
includeModule(mod)
|
|
1459
|
+
const key = `${mod}.${init[2]}`
|
|
1460
|
+
if (ctx.core.emit[key] != null) registerBuiltinAlias(name, key)
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1050
1467
|
/** Prepare let/const declaration. */
|
|
1051
1468
|
function prepDecl(op, ...inits) {
|
|
1052
1469
|
const rest = []
|
|
@@ -1100,7 +1517,67 @@ function prepDecl(op, ...inits) {
|
|
|
1100
1517
|
const staticArr = op === 'const' ? staticStringArrayValues(init) : null
|
|
1101
1518
|
const normed = prep(init)
|
|
1102
1519
|
|
|
1520
|
+
// `let/const name = NS.member` (`let sin = Math.sin`) — prep's `.` handler
|
|
1521
|
+
// already resolved this to the flat dotted emit key; alias `name` to it
|
|
1522
|
+
// (see registerBuiltinAlias) instead of declaring a real global/local that
|
|
1523
|
+
// would box the builtin as a first-class value on every reference.
|
|
1524
|
+
if (!isDestructPattern(name) && typeof name === 'string') {
|
|
1525
|
+
const memberKey = builtinMemberKey(normed)
|
|
1526
|
+
if (memberKey && registerBuiltinAlias(name, memberKey)) continue
|
|
1527
|
+
// `const M = Math` at module top level — a bare reference to a whole
|
|
1528
|
+
// builtin namespace (no member, no dot). Same reasoning as above: there's
|
|
1529
|
+
// no runtime namespace object to box, so alias `name` straight to the
|
|
1530
|
+
// module name in `scope.chain` instead of declaring a real global — the
|
|
1531
|
+
// existing `mod = ctx.scope.chain[obj]` check in the '.' handler (the
|
|
1532
|
+
// SAME table `Math` itself resolves through) then resolves `M.sqrt`
|
|
1533
|
+
// exactly like a direct `Math.sqrt` reference would, with no further
|
|
1534
|
+
// changes needed there.
|
|
1535
|
+
// Any depth: registerBuiltinAlias scope-routes (chain at module level, the
|
|
1536
|
+
// block-scoped `scopes` stack inside functions), and the consumers — the
|
|
1537
|
+
// '.' handler and resolveCallee's `.`-callee branch — resolve the receiver
|
|
1538
|
+
// through the function scope FIRST (namespaceModOf below). The genuine-
|
|
1539
|
+
// alias-vs-ordinary-local ambiguity is settled by the discriminator here,
|
|
1540
|
+
// not at the read site: only an RHS that RESOLVED to a module name
|
|
1541
|
+
// registers (an ordinary local named 'json'/'fn' never does — its RHS is
|
|
1542
|
+
// a value expression, and a user shadow of the namespace makes prep
|
|
1543
|
+
// resolve the RHS through the shadow instead). `normed !== name` guards
|
|
1544
|
+
// the identity-self-map false positive (e.g. a cross-module host-import
|
|
1545
|
+
// alias that happens to be named after a module).
|
|
1546
|
+
// `!shadowsBuiltin(init)`: the RHS must be the NAMESPACE ITSELF, not a
|
|
1547
|
+
// declared VALUE binding that merely resolves to a module-shaped name —
|
|
1548
|
+
// `let object = {…}; let alias = object` chains normed==='object'
|
|
1549
|
+
// (identity self-map through the shadow path) and must stay a value copy.
|
|
1550
|
+
if (typeof normed === 'string' && normed !== name && hasModule(normed)
|
|
1551
|
+
&& typeof init === 'string' && !shadowsBuiltin(init)) {
|
|
1552
|
+
registerBuiltinAlias(name, normed); continue
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1103
1556
|
if (isDestructPattern(name)) {
|
|
1557
|
+
// `let/const {a, b: c} = NS` where NS resolved (above) to a known builtin
|
|
1558
|
+
// module — alias each key directly (see namespaceMemberAliases) instead
|
|
1559
|
+
// of running the generic runtime object-destructure below, which has no
|
|
1560
|
+
// way to read a property off a namespace that isn't a real heap object.
|
|
1561
|
+
if (typeof normed === 'string' && hasModule(normed)) {
|
|
1562
|
+
const aliases = namespaceMemberAliases(name, normed)
|
|
1563
|
+
if (aliases) {
|
|
1564
|
+
for (const [target, key] of aliases) {
|
|
1565
|
+
if (registerBuiltinAlias(target, key)) continue
|
|
1566
|
+
// Exported CONSTANT member (export let { PI } = Math): real storage,
|
|
1567
|
+
// mirroring the normal decl path's depth-0 prefix/chain wiring; the
|
|
1568
|
+
// init assignment rides `rest` into module init like any destructure.
|
|
1569
|
+
declareGlobal(target)
|
|
1570
|
+
let declName = target
|
|
1571
|
+
if (depth === 0 && ctx.module.currentPrefix) {
|
|
1572
|
+
declName = `${ctx.module.currentPrefix}$${target}`
|
|
1573
|
+
ctx.scope.chain[target] = declName
|
|
1574
|
+
}
|
|
1575
|
+
rest.push(['=', declName, key])
|
|
1576
|
+
recordGlobalRep(declName, key)
|
|
1577
|
+
}
|
|
1578
|
+
continue
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1104
1581
|
// Register each binding both as a module global (depth 0) and in the
|
|
1105
1582
|
// current arrow's local scope (depth ≠ 0). Without the local registration
|
|
1106
1583
|
// the name is invisible to `isUnresolvableBareIdent`, so a later
|
|
@@ -1154,6 +1631,11 @@ function prepDecl(op, ...inits) {
|
|
|
1154
1631
|
if (typeof declName === 'string' && Array.isArray(normed) && normed[0] === '=>')
|
|
1155
1632
|
funcValueNames[funcValueNames.length - 1]?.add(declName)
|
|
1156
1633
|
if (op === 'const') bindStaticConst(declName, staticStr, staticArr)
|
|
1634
|
+
// Local const: record the (post-rename) name for the assignment guard —
|
|
1635
|
+
// isConst covers only module scope, so `const c = 2; c = 3` inside a
|
|
1636
|
+
// function used to compile and mutate silently.
|
|
1637
|
+
if (op === 'const' && typeof declName === 'string' && scopes.length)
|
|
1638
|
+
staticConstScopes[staticConstScopes.length - 1]?.consts?.add(declName)
|
|
1157
1639
|
// Track const for reassignment checks — only module-scope consts (depth 0)
|
|
1158
1640
|
if (typeof declName === 'string' && depth === 0) {
|
|
1159
1641
|
if (ctx.module.currentPrefix) {
|
|
@@ -1249,6 +1731,87 @@ function dispatchConstructorCall(callee, args) {
|
|
|
1249
1731
|
return undefined
|
|
1250
1732
|
}
|
|
1251
1733
|
|
|
1734
|
+
// `f.call/apply/bind` on a PROVEN function binding lowers statically: jz
|
|
1735
|
+
// functions cannot observe `this` (rejected outside the class lowering), so
|
|
1736
|
+
// the thisArg is dead weight — kept only for its side effects via a comma
|
|
1737
|
+
// sequence. Anything not provably a function keeps the runtime path (a user
|
|
1738
|
+
// object may legitimately carry its own `call` property). Previously these
|
|
1739
|
+
// silently returned undefined (.call/.apply) or trapped (table OOB, .bind).
|
|
1740
|
+
function foldFnCallApplyBind(callee, args) {
|
|
1741
|
+
if (!Array.isArray(callee) || callee[0] !== '.') return undefined
|
|
1742
|
+
const [, name, meth] = callee
|
|
1743
|
+
if (typeof name !== 'string' || (meth !== 'call' && meth !== 'apply' && meth !== 'bind')) return undefined
|
|
1744
|
+
if (!hasFunc(name) && !isFuncValueLocal(name)) return undefined
|
|
1745
|
+
const [thisArg, ...rest] = handlerArgs(args)
|
|
1746
|
+
const trivialThis = thisArg == null || typeof thisArg === 'string' ||
|
|
1747
|
+
(Array.isArray(thisArg) && thisArg[0] == null)
|
|
1748
|
+
const seq = (node) => trivialThis ? prep(node) : prep([',', thisArg, node])
|
|
1749
|
+
const argsSlot = (list) => list.length === 0 ? null : list.length === 1 ? list[0] : [',', ...list]
|
|
1750
|
+
if (meth === 'call') return seq(['()', name, argsSlot(rest)])
|
|
1751
|
+
if (meth === 'apply') {
|
|
1752
|
+
if (rest.length > 1) err('`.apply` takes (thisArg, argsArray)')
|
|
1753
|
+
// A literal args array expands statically — fixed-arity callees accept it
|
|
1754
|
+
// where a runtime spread could not.
|
|
1755
|
+
const arr = rest[0]
|
|
1756
|
+
if (Array.isArray(arr) && arr[0] === '[]' && arr.length <= 2) {
|
|
1757
|
+
const elems = arr.length === 1 ? [] : (Array.isArray(arr[1]) && arr[1][0] === ',') ? arr[1].slice(1) : [arr[1]]
|
|
1758
|
+
if (!elems.some(e => Array.isArray(e) && e[0] === '...')) return seq(['()', name, argsSlot(elems)])
|
|
1759
|
+
}
|
|
1760
|
+
return seq(['()', name, rest.length ? ['...', rest[0]] : null])
|
|
1761
|
+
}
|
|
1762
|
+
// bind(thisArg, ...pre) → an arrow closing over the pre-bound args. When the
|
|
1763
|
+
// callee's arity is known (a lifted top-level fn), mint EXPLICIT remaining
|
|
1764
|
+
// params — a rest+spread arrow would hit the non-variadic spread-call limit.
|
|
1765
|
+
const f = ctx.func.list.find(fn => fn.name === name)
|
|
1766
|
+
if (f && !f.rest) {
|
|
1767
|
+
const remaining = Math.max(0, f.sig.params.length - rest.length)
|
|
1768
|
+
const ps = Array.from({ length: remaining }, () => `${T}b${ctx.func.uniq++}`)
|
|
1769
|
+
return seq(['=>', ps.length ? ['()', argsSlot(ps)] : ['()', null],
|
|
1770
|
+
['()', name, argsSlot([...rest, ...ps])]])
|
|
1771
|
+
}
|
|
1772
|
+
const r = `${T}b${ctx.func.uniq++}`
|
|
1773
|
+
return seq(['=>', ['()', ['...', r]], ['()', name, argsSlot([...rest, ['...', r]])]])
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// `JSON.parse(src, reviver)` — the reviver argument was silently DROPPED
|
|
1777
|
+
// (module/json.js parses single-arg). Lower the two-arg form to an inline
|
|
1778
|
+
// IIFE that parses, then walks the result bottom-up applying the reviver
|
|
1779
|
+
// (ES §25.5.1 InternalizeJSONProperty). One divergence, documented: a
|
|
1780
|
+
// reviver returning undefined ASSIGNS undefined instead of deleting the
|
|
1781
|
+
// property (jz fixed-shape objects delete only dictionary keys).
|
|
1782
|
+
let jsonReviveTemplate = null
|
|
1783
|
+
function foldJsonReviver(callee, args) {
|
|
1784
|
+
const isParse = callee === 'JSON.parse' ||
|
|
1785
|
+
(Array.isArray(callee) && callee[0] === '.' && callee[1] === 'JSON' && callee[2] === 'parse')
|
|
1786
|
+
if (!isParse) return undefined
|
|
1787
|
+
const list = handlerArgs(args)
|
|
1788
|
+
if (list.length < 2 || list[1] == null) return undefined
|
|
1789
|
+
// A literal null/undefined reviver is spec-ignored — keep the plain parse
|
|
1790
|
+
// (the walk would otherwise closure-call a nullish value at runtime).
|
|
1791
|
+
if (Array.isArray(list[1]) && list[1][0] == null && list[1][1] == null) return undefined
|
|
1792
|
+
if (!ctx.transform.parse) err('JSON.parse with a reviver needs the jz pipeline (ctx.transform.parse)')
|
|
1793
|
+
jsonReviveTemplate ??= ctx.transform.parse(`((s, r) => {
|
|
1794
|
+
let walk
|
|
1795
|
+
walk = (val) => {
|
|
1796
|
+
if (Array.isArray(val)) {
|
|
1797
|
+
for (let i = 0; i < val.length; i++) val[i] = r(String(i), walk(val[i]))
|
|
1798
|
+
} else if (val !== null && typeof val === 'object') {
|
|
1799
|
+
let ks = Object.keys(val)
|
|
1800
|
+
for (let i = 0; i < ks.length; i++) { let k = ks[i]; val[k] = r(k, walk(val[k])) }
|
|
1801
|
+
}
|
|
1802
|
+
return val
|
|
1803
|
+
}
|
|
1804
|
+
return r("", walk(JSON.parse(s)))
|
|
1805
|
+
})`)
|
|
1806
|
+
// Fresh structural copy per site — prep mutates/renames in place.
|
|
1807
|
+
// (Recursive copy, not structuredClone: the self-host kernel compiles this
|
|
1808
|
+
// file and structuredClone is not a jz builtin.)
|
|
1809
|
+
const cloneNode = (n) => Array.isArray(n) ? n.map(cloneNode) : n
|
|
1810
|
+
const iife = cloneNode(jsonReviveTemplate)
|
|
1811
|
+
const arrow = Array.isArray(iife) && iife[0] === '()' && iife.length === 2 ? iife[1] : iife
|
|
1812
|
+
return prep(['()', arrow, [',', list[0], list[1]]])
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1252
1815
|
// Compile-time namespace introspection on a `obj.prop(...)` callee:
|
|
1253
1816
|
// `Array.isArray(NS)` on a bare builtin global folds to `false` (a namespace
|
|
1254
1817
|
// value is never an array); `NS.hasOwnProperty("member")` on a builtin
|
|
@@ -1260,8 +1823,11 @@ function foldNamespaceIntrospection(callee, args) {
|
|
|
1260
1823
|
if (obj === 'Array' && prop === 'isArray') {
|
|
1261
1824
|
const cargs = handlerArgs(args)
|
|
1262
1825
|
const a0 = cargs.length === 1 ? cargs[0] : null
|
|
1826
|
+
// Fold to boolean `false`, not number 0 — `Array.isArray(Math) === false`
|
|
1827
|
+
// must be true, and prepare keeps boolean identity (see the true/false
|
|
1828
|
+
// literal notes at prep()).
|
|
1263
1829
|
if (typeof a0 === 'string' && GLOBALS[a0] && !(scopes.length && isDeclared(a0)) && !hasFunc(a0))
|
|
1264
|
-
return [,
|
|
1830
|
+
return [, false]
|
|
1265
1831
|
}
|
|
1266
1832
|
if (prop === 'hasOwnProperty' && typeof obj === 'string' && !(scopes.length && isDeclared(obj))) {
|
|
1267
1833
|
const mod = ctx.scope.chain[obj]
|
|
@@ -1285,6 +1851,15 @@ function foldNamespaceIntrospection(callee, args) {
|
|
|
1285
1851
|
// (function table / closure) machinery.
|
|
1286
1852
|
const INTRINSIC_CALLEES = new Set(['__iter_arr', '__keys_ro'])
|
|
1287
1853
|
|
|
1854
|
+
// Resolve a member-receiver to a builtin module name, honoring FUNCTION-SCOPED
|
|
1855
|
+
// namespace aliases (`const M = Math` inside a body registers M → 'math' in the
|
|
1856
|
+
// block scope; resolveScope surfaces it) ahead of the module-level chain.
|
|
1857
|
+
function namespaceModOf(obj) {
|
|
1858
|
+
if (typeof obj !== 'string') return null
|
|
1859
|
+
const key = scopes.length && isDeclared(obj) ? resolveScope(obj) : ctx.scope.chain[obj]
|
|
1860
|
+
return typeof key === 'string' && !key.includes('.') && hasModule(key) ? key : null
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1288
1863
|
function resolveCallee(callee, args) {
|
|
1289
1864
|
if (typeof callee === 'string') {
|
|
1290
1865
|
const local = scopes.length && isDeclared(callee)
|
|
@@ -1292,6 +1867,14 @@ function resolveCallee(callee, args) {
|
|
|
1292
1867
|
if (local) return resolveScope(callee)
|
|
1293
1868
|
if (resolved?.includes('.')) return resolved
|
|
1294
1869
|
if (resolved && hasFunc(resolved)) return resolved
|
|
1870
|
+
// Chain-resolved VALUE GLOBAL — a default-imported factory product
|
|
1871
|
+
// (`export default make(...)` → module global `__dep$default`;
|
|
1872
|
+
// `import thing …; thing(x)` must closure-call that global, not fall
|
|
1873
|
+
// through to the bare unresolvable name).
|
|
1874
|
+
if (resolved && (ctx.scope.globals.has(resolved) || ctx.scope.userGlobals?.has?.(resolved))) {
|
|
1875
|
+
includeForCallableValue()
|
|
1876
|
+
return resolved
|
|
1877
|
+
}
|
|
1295
1878
|
if (resolved && !resolved.includes('.')) {
|
|
1296
1879
|
if (hasModule(resolved) && !ctx.module.imports.some(i => i[3]?.[1] === `$${resolved}`)) includeModule(resolved)
|
|
1297
1880
|
return callee
|
|
@@ -1322,8 +1905,8 @@ function resolveCallee(callee, args) {
|
|
|
1322
1905
|
}
|
|
1323
1906
|
if (key && includeForNamedCall(key)) return key
|
|
1324
1907
|
if (includeForGenericMethod(prop)) return prep(callee)
|
|
1325
|
-
const mod =
|
|
1326
|
-
if (
|
|
1908
|
+
const mod = namespaceModOf(obj)
|
|
1909
|
+
if (mod)
|
|
1327
1910
|
return (includeModule(mod), mod + '.' + prop)
|
|
1328
1911
|
return prep(callee)
|
|
1329
1912
|
}
|
|
@@ -1375,6 +1958,20 @@ const handlers = {
|
|
|
1375
1958
|
// Destructuring assignment: [a, ...r] = expr or ({x: a} = expr)
|
|
1376
1959
|
// Distinguishing from index assignment: destructuring patterns have exactly one payload node.
|
|
1377
1960
|
if (isDestructPattern(lhs) && lhs.length === 2) {
|
|
1961
|
+
// `({sqrt, abs} = Math)` — see namespaceMemberAssigns. Checked ahead of the
|
|
1962
|
+
// generic runtime-destructure path below, which has no way to read a
|
|
1963
|
+
// property off a namespace that isn't a real heap object.
|
|
1964
|
+
if (lhs[0] === '{}' && typeof rhs === 'string' && !shadowsBuiltin(rhs)) {
|
|
1965
|
+
const mod = ctx.scope.chain[rhs]
|
|
1966
|
+
// `mod !== rhs` excludes an identity self-map (an ordinary host-import
|
|
1967
|
+
// alias or un-renamed binding resolves to its OWN name) — see the same
|
|
1968
|
+
// guard's rationale in the '.' handler and prepDecl's namespace-value alias.
|
|
1969
|
+
if (mod && mod !== rhs && !mod.includes('.') && hasModule(mod)) {
|
|
1970
|
+
const assigns = namespaceMemberAssigns(lhs, rhs)
|
|
1971
|
+
if (assigns) return prep([';', ...assigns])
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1378
1975
|
const scalar = scalarArrayDestruct(lhs, rhs)
|
|
1379
1976
|
if (scalar) return scalar
|
|
1380
1977
|
|
|
@@ -1411,7 +2008,24 @@ const handlers = {
|
|
|
1411
2008
|
// Build the target `.` node directly from the resolved base — re-`prep`ing
|
|
1412
2009
|
// the lhs would resolve a multiProp `fn.prop` to an rvalue (closure
|
|
1413
2010
|
// materialization block), which is not a valid assignment target.
|
|
1414
|
-
|
|
2011
|
+
// Cross-module lift: the lifted func belongs to the BASE function's
|
|
2012
|
+
// OWNING module (fnBase's mangled prefix), not the module that textually
|
|
2013
|
+
// contains the write. Untagged, the writing module's end-of-prep rename
|
|
2014
|
+
// sweep double-prefixes it (`__B$__A$lex$next`) and the owner's call
|
|
2015
|
+
// sites never direct-resolve — every read stays on the dyn path forever
|
|
2016
|
+
// (the hot tokenizer probes test/closures.js's cross-module pin catches).
|
|
2017
|
+
if (defFunc(name, prep(rhs))) {
|
|
2018
|
+
const ownerEnd = fnBase.lastIndexOf('$')
|
|
2019
|
+
if (ownerEnd > 0) {
|
|
2020
|
+
const fn = ctx.func.list.find(f => f.name === name)
|
|
2021
|
+
// _ownerPrefix exempts the lift from the writing module's NAME
|
|
2022
|
+
// mangling only — its BODY is this module's text and must still get
|
|
2023
|
+
// this module's reference-renaming walk (unlike _modulePrefix, which
|
|
2024
|
+
// marks sub-module funcs already walked with their own rename map).
|
|
2025
|
+
if (fn && !fn._ownerPrefix) fn._ownerPrefix = fnBase.slice(0, ownerEnd)
|
|
2026
|
+
}
|
|
2027
|
+
return ['=', ['.', fnBase, lhs[2]], name]
|
|
2028
|
+
}
|
|
1415
2029
|
}
|
|
1416
2030
|
}
|
|
1417
2031
|
const staticStr = staticStringExpr(rhs)
|
|
@@ -1453,10 +2067,24 @@ const handlers = {
|
|
|
1453
2067
|
const catchClause = clauses.find(c => Array.isArray(c) && c[0] === 'catch')
|
|
1454
2068
|
const finallyClause = clauses.find(c => Array.isArray(c) && c[0] === 'finally')
|
|
1455
2069
|
const tryBody = prep(body)
|
|
2070
|
+
// A pattern catch param (`catch ({ x })`) binds via a minted temp + a
|
|
2071
|
+
// destructuring decl prepended to the handler (mirrors defFunc's param
|
|
2072
|
+
// patterns) — the raw pattern node is not a bindable catch local.
|
|
2073
|
+
let cParam = catchClause?.[1], cHandler = catchClause?.[2]
|
|
2074
|
+
if (catchClause && isDestructPattern(cParam)) {
|
|
2075
|
+
const tmp = `${T}cp${ctx.func.uniq++}`
|
|
2076
|
+
const declStmt = ['let', ['=', cParam, tmp]]
|
|
2077
|
+
cHandler = Array.isArray(cHandler) && cHandler[0] === '{}'
|
|
2078
|
+
? (Array.isArray(cHandler[1]) && cHandler[1][0] === ';'
|
|
2079
|
+
? ['{}', [';', declStmt, ...cHandler[1].slice(1)]]
|
|
2080
|
+
: ['{}', [';', declStmt, ...(cHandler[1] == null ? [] : [cHandler[1]])]])
|
|
2081
|
+
: ['{}', [';', declStmt, cHandler]]
|
|
2082
|
+
cParam = tmp
|
|
2083
|
+
}
|
|
1456
2084
|
// prep(handler) ONCE — it has side effects (uniq++, scope pushes, includes), so
|
|
1457
2085
|
// the no-finally catch branch must reuse `caught`, not re-prep (FE-3 fix).
|
|
1458
2086
|
const caught = catchClause
|
|
1459
|
-
? ['catch', tryBody,
|
|
2087
|
+
? ['catch', tryBody, cParam, prep(cHandler)]
|
|
1460
2088
|
: tryBody
|
|
1461
2089
|
return finallyClause ? ['finally', caught, prep(finallyClause[1])] : caught
|
|
1462
2090
|
},
|
|
@@ -1476,6 +2104,13 @@ const handlers = {
|
|
|
1476
2104
|
|
|
1477
2105
|
// Tagged template: tag`a${x}b` → tag(['a','b'], x)
|
|
1478
2106
|
'``'(tag, ...parts) {
|
|
2107
|
+
// String.raw needs the RAW source slices, but subscript's template node
|
|
2108
|
+
// carries only cooked strings (escapes already applied) — raw text is
|
|
2109
|
+
// unrecoverable post-parse, and folding cooked-as-raw is silently wrong
|
|
2110
|
+
// for any template containing an escape. Reject until the parser keeps
|
|
2111
|
+
// raw slices (upstream subscript; same for `.raw` inside custom tags).
|
|
2112
|
+
if (Array.isArray(tag) && tag[0] === '.' && tag[1] === 'String' && tag[2] === 'raw')
|
|
2113
|
+
err('String.raw not supported: the parser keeps only cooked template strings')
|
|
1479
2114
|
const raw = staticStringExpr(['``', tag, ...parts])
|
|
1480
2115
|
if (raw != null) return staticString(raw)
|
|
1481
2116
|
const strs = [], exprs = []
|
|
@@ -1635,21 +2270,31 @@ const handlers = {
|
|
|
1635
2270
|
'==='(a, b) { return prepStrictEq('===', a, b) },
|
|
1636
2271
|
'!=='(a, b) { return prepStrictEq('!==', a, b) },
|
|
1637
2272
|
|
|
2273
|
+
// Short-circuit dead-arm elimination, value-exact: `A || B` with A never-falsy
|
|
2274
|
+
// IS A — B is unreachable; dual for `&&`. Both operands are prepped first so
|
|
2275
|
+
// policy checks still fire (same discipline as emit's literal-LHS fold, which
|
|
2276
|
+
// preps-then-skips); only the dead subtree is dropped from the program.
|
|
2277
|
+
'||'(a, b) { const pa = prep(a), pb = prep(b); return alwaysTruthy(pa) ? pa : ['||', pa, pb] },
|
|
2278
|
+
'&&'(a, b) { const pa = prep(a), pb = prep(b); return alwaysFalsy(pa) ? pa : ['&&', pa, pb] },
|
|
2279
|
+
|
|
1638
2280
|
// Statements
|
|
1639
|
-
';': (...stmts) =>
|
|
2281
|
+
';': (...stmts) => {
|
|
2282
|
+
preRegisterBuiltinAliases(stmts)
|
|
2283
|
+
return [';', ...stmts.map(prep).filter(x => x != null).map(dropDeadPostfix)]
|
|
2284
|
+
},
|
|
1640
2285
|
'let': (...inits) => prepDecl('let', ...inits),
|
|
1641
2286
|
'const': (...inits) => prepDecl('const', ...inits),
|
|
1642
2287
|
|
|
1643
2288
|
// Block-scoped control flow: push scope for bodies so inner let/const shadows correctly
|
|
1644
2289
|
'if': (cond, then, els) => {
|
|
1645
2290
|
const c = prep(stripBoolNot(cond))
|
|
1646
|
-
pushScope(); const t = prep(then); popScope()
|
|
1647
|
-
if (els != null) { pushScope(); const e = prep(els); popScope(); return ['if', c, t, e] }
|
|
2291
|
+
pushScope(); const t = dropDeadPostfix(prep(then)); popScope()
|
|
2292
|
+
if (els != null) { pushScope(); const e = dropDeadPostfix(prep(els)); popScope(); return ['if', c, t, e] }
|
|
1648
2293
|
return ['if', c, t]
|
|
1649
2294
|
},
|
|
1650
2295
|
'while': (cond, body) => {
|
|
1651
2296
|
const c = prep(stripBoolNot(cond))
|
|
1652
|
-
pushScope(); const b = prep(body); popScope()
|
|
2297
|
+
pushScope(); const b = dropDeadPostfix(prep(body)); popScope()
|
|
1653
2298
|
return ['while', c, b]
|
|
1654
2299
|
},
|
|
1655
2300
|
// do { body } while (cond) → flag-guarded while: `flag=true; while (flag||cond) { flag=false; body }`.
|
|
@@ -1667,8 +2312,13 @@ const handlers = {
|
|
|
1667
2312
|
'export': decl => {
|
|
1668
2313
|
if (Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const'))
|
|
1669
2314
|
for (const i of decl.slice(1))
|
|
1670
|
-
if (Array.isArray(i) && i[0] === '='
|
|
1671
|
-
ctx.func.exports[i[1]] = true
|
|
2315
|
+
if (Array.isArray(i) && i[0] === '=') {
|
|
2316
|
+
if (typeof i[1] === 'string') ctx.func.exports[i[1]] = true
|
|
2317
|
+
// `export let { a, b: c } = …` / `export let [x, y] = …` — every
|
|
2318
|
+
// BoundName of the declaration is an export (ES §16.2.3.2). Surfaced
|
|
2319
|
+
// by window-function's `export let { cos, sin, abs } = Math`.
|
|
2320
|
+
else if (isDestructPattern(i[1])) for (const n of bindingNames(i[1])) ctx.func.exports[n] = true
|
|
2321
|
+
}
|
|
1672
2322
|
// export name → bare-identifier re-export (shorthand for `export { name }`).
|
|
1673
2323
|
// Register the binding and emit nothing; without this the name falls through
|
|
1674
2324
|
// to `prep(decl)` below and compiles as a dead `global.get; drop` statement
|
|
@@ -1806,19 +2456,12 @@ const handlers = {
|
|
|
1806
2456
|
return result
|
|
1807
2457
|
},
|
|
1808
2458
|
|
|
1809
|
-
// Switch
|
|
1810
|
-
//
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
return prep(body)
|
|
1816
|
-
}
|
|
1817
|
-
return ['switch', prep(discriminant), ...cases.map(c => {
|
|
1818
|
-
if (c[0] === 'case') return ['case', prep(c[1]), prepCase(c[2])]
|
|
1819
|
-
if (c[0] === 'default') return ['default', prep(c[1])]
|
|
1820
|
-
return prep(c)
|
|
1821
|
-
})]
|
|
2459
|
+
// Switch reaches prepare only when jzify was skipped (strict / .jz): default
|
|
2460
|
+
// mode lowers every switch to the entry-index if-chain (jzify/switch.js). The
|
|
2461
|
+
// language table keeps `switch` in the jzify ring, not the strict canonical
|
|
2462
|
+
// subset — and the old native twin here mis-compiled `break` (no loop frame).
|
|
2463
|
+
'switch'() {
|
|
2464
|
+
return err('strict mode: `switch` is not in the canonical subset — use if/else chains (default mode lowers switch)')
|
|
1822
2465
|
},
|
|
1823
2466
|
|
|
1824
2467
|
// Optional chaining / typeof — need ptr module. Optional member access pulls
|
|
@@ -1925,6 +2568,8 @@ const handlers = {
|
|
|
1925
2568
|
const folded = foldImportMetaResolve(callee, args)
|
|
1926
2569
|
?? dispatchConstructorCall(callee, args)
|
|
1927
2570
|
?? foldNamespaceIntrospection(callee, args)
|
|
2571
|
+
?? foldFnCallApplyBind(callee, args)
|
|
2572
|
+
?? foldJsonReviver(callee, args)
|
|
1928
2573
|
if (folded !== undefined) return folded
|
|
1929
2574
|
|
|
1930
2575
|
callee = resolveCallee(callee, args)
|
|
@@ -1943,6 +2588,20 @@ const handlers = {
|
|
|
1943
2588
|
// `(x.pop)` and drop the call. Keeping the slot makes `prep` idempotent for
|
|
1944
2589
|
// calls and matches `setCallArgs`'s canonical shape; `commaList(node[2])`
|
|
1945
2590
|
// reads it back as zero args everywhere downstream.
|
|
2591
|
+
// Object.freeze is identity in jz (frozenness is not modeled — the emitter
|
|
2592
|
+
// returns its operand unchanged, module/object.js). Fold the CALL away so
|
|
2593
|
+
// the operand's static knowledge survives the wrapper: a frozen literal
|
|
2594
|
+
// binding keeps its schema (slot dispatch), and `TABLE[2]` on a frozen
|
|
2595
|
+
// preset table resolves statically instead of falling to the untyped
|
|
2596
|
+
// element dispatch. `Object.freeze` as a value (`arr.map(Object.freeze)`)
|
|
2597
|
+
// is not a call form and keeps the runtime emitter.
|
|
2598
|
+
if (callee === 'Object.freeze' && preppedArgs.length === 1 && preppedArgs[0] != null) {
|
|
2599
|
+
// Record the (prepared, post-rename) binding so Object.isFrozen answers
|
|
2600
|
+
// true for it — consistency, not enforcement (writes are not trapped).
|
|
2601
|
+
if (typeof preppedArgs[0] === 'string') (ctx.runtime.frozenVars ??= new Set()).add(preppedArgs[0])
|
|
2602
|
+
return preppedArgs[0]
|
|
2603
|
+
}
|
|
2604
|
+
|
|
1946
2605
|
const result = preppedArgs.length ? ['()', callee, ...preppedArgs] : ['()', callee, null]
|
|
1947
2606
|
|
|
1948
2607
|
if (callee === 'Object.assign' && ctx.schema.register) inferAssignSchema(result)
|
|
@@ -2074,13 +2733,46 @@ const handlers = {
|
|
|
2074
2733
|
|
|
2075
2734
|
// For loop
|
|
2076
2735
|
'for'(head, body) {
|
|
2736
|
+
// ES §14.7.4.7 CreatePerIterationEnvironment: a `let` declared in a classic
|
|
2737
|
+
// for-HEAD gets a FRESH binding each iteration when closures capture it —
|
|
2738
|
+
// `for (let i…) fns.push(() => i)` must capture 0,1,2, not the final value.
|
|
2739
|
+
// Lower to the copy-in/copy-out shape (only when a body arrow actually
|
|
2740
|
+
// references the head var — pay-per-capture):
|
|
2741
|
+
// for (let __i = 0; __i < n; __i++) { let i = __i; …body…; __i = i }
|
|
2742
|
+
// The body-`let` then rides the existing per-iteration fresh-cell machinery
|
|
2743
|
+
// (emitLoopFreshBoxed). Known edge, accepted: a closure inside the COND or
|
|
2744
|
+
// STEP itself captures the carrier, not the per-iteration binding.
|
|
2745
|
+
if (Array.isArray(head) && head[0] === ';' && Array.isArray(head[1]) && head[1][0] === 'let') {
|
|
2746
|
+
const captured = []
|
|
2747
|
+
for (let i = 1; i < head[1].length; i++) {
|
|
2748
|
+
const d = head[1][i]
|
|
2749
|
+
const nm = typeof d === 'string' ? d : (Array.isArray(d) && d[0] === '=' && typeof d[1] === 'string' ? d[1] : null)
|
|
2750
|
+
if (nm && bodyCapturesName(body, nm)) captured.push(nm)
|
|
2751
|
+
}
|
|
2752
|
+
if (captured.length) {
|
|
2753
|
+
const carrier = new Map(captured.map(n => [n, `${n}${T}pi${ctx.func.uniq++}`]))
|
|
2754
|
+
const renamed = (n) => substIdents(n, carrier)
|
|
2755
|
+
const decl = ['let', ...head[1].slice(1).map(d => {
|
|
2756
|
+
if (typeof d === 'string') return carrier.get(d) ?? d
|
|
2757
|
+
if (Array.isArray(d) && d[0] === '=' && carrier.has(d[1])) return ['=', carrier.get(d[1]), d[2]]
|
|
2758
|
+
return d
|
|
2759
|
+
})]
|
|
2760
|
+
const newHead = [';', decl, renamed(head[2]), renamed(head[3]), ...head.slice(4).map(renamed)]
|
|
2761
|
+
const copyIn = ['let', ...captured.map(n => ['=', n, carrier.get(n)])]
|
|
2762
|
+
const copyOut = captured.map(n => ['=', carrier.get(n), n])
|
|
2763
|
+
const newBody = ['{}', [';', copyIn, body, ...copyOut]]
|
|
2764
|
+
return handlers['for'](newHead, newBody)
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2077
2767
|
pushScope()
|
|
2078
2768
|
// A comma/sequence Expression in a for-IN head RHS — `for (x in a, b)` — is valid (the RHS is
|
|
2079
2769
|
// an Expression): evaluate left-to-right for side effects, value as the last element. (for-OF's
|
|
2080
|
-
// RHS is an AssignmentExpression — no comma — so it is left alone.)
|
|
2081
|
-
// node in the source slot. Don't wrap it in `()`:
|
|
2082
|
-
//
|
|
2083
|
-
//
|
|
2770
|
+
// RHS is an AssignmentExpression — no comma — so it is left alone.) subscript ≥10.5.1 parses
|
|
2771
|
+
// the head re-associated, landing a bare `,` node in the source slot. Don't wrap it in `()`:
|
|
2772
|
+
// Object.keys((a, obj))
|
|
2773
|
+
// hides `obj` behind the sequence and loses its static schema (a non-escaping literal
|
|
2774
|
+
// scalarizes → 0 keys). Instead take the LAST element as the (direct) iteration source and run
|
|
2775
|
+
// the earlier elements once first.
|
|
2084
2776
|
let forInSeqPre = null
|
|
2085
2777
|
if (Array.isArray(head) && head[0] === 'in' && Array.isArray(head[2]) && head[2][0] === ',') {
|
|
2086
2778
|
const parts = head[2].slice(1)
|
|
@@ -2127,13 +2819,16 @@ const handlers = {
|
|
|
2127
2819
|
}
|
|
2128
2820
|
}
|
|
2129
2821
|
}
|
|
2130
|
-
r = ['for', init ? prep(init) : null, cond ? prep(cond) : null, step ? prep(step) : null, prep(body)]
|
|
2822
|
+
r = ['for', init ? prep(init) : null, cond ? prep(cond) : null, step ? dropDeadPostfix(prep(step)) : null, dropDeadPostfix(prep(body))]
|
|
2131
2823
|
} else if (Array.isArray(head) && head[0] === 'of') {
|
|
2132
2824
|
// for (let x of arr) → hoist arr (if non-trivial) and arr.length once, iterate by index.
|
|
2133
2825
|
// Divergence from JS: mutating arr during iteration won't extend/shorten the loop.
|
|
2134
2826
|
// jz philosophy: explicit > implicit; mutation during iteration is a code smell.
|
|
2135
2827
|
const [, decl, src] = head
|
|
2136
|
-
const
|
|
2828
|
+
const isDeclHead = Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const')
|
|
2829
|
+
// `for ((x) of …)` — unwrap a cover-parenthesized target (mirrors for-in).
|
|
2830
|
+
let ofLhs = decl; while (Array.isArray(ofLhs) && ofLhs[0] === '()' && ofLhs.length === 2) ofLhs = ofLhs[1]
|
|
2831
|
+
const varName = isDeclHead ? decl[1] : ofLhs
|
|
2137
2832
|
const idx = `${T}i${ctx.func.uniq++}`
|
|
2138
2833
|
const lenVar = `${T}len${ctx.func.uniq++}`
|
|
2139
2834
|
const arrVar = `${T}arr${ctx.func.uniq++}`
|
|
@@ -2147,7 +2842,14 @@ const handlers = {
|
|
|
2147
2842
|
const decls = ['let', ['=', arrVar, ['()', '__iter_arr', src]], ['=', idx, [, 0]], ['=', lenVar, lenE]]
|
|
2148
2843
|
const cond = ['<', idx, lenVar]
|
|
2149
2844
|
const step = ['++', idx]
|
|
2150
|
-
|
|
2845
|
+
// Decl head (`for (let x of …)`) takes a fresh per-iteration binding;
|
|
2846
|
+
// ASSIGNMENT head (`for (x of …)`, `for ([a] of …)`, `for (o.x of …)`,
|
|
2847
|
+
// var-hoisted heads) must assign the EXISTING target — a `let` wrap
|
|
2848
|
+
// shadowed it, so after-loop reads saw the stale outer value.
|
|
2849
|
+
const bindStmt = isDeclHead
|
|
2850
|
+
? ['let', ['=', varName, ['[]', arrVar, idx]]]
|
|
2851
|
+
: ['=', varName, ['[]', arrVar, idx]]
|
|
2852
|
+
const inner = [';', bindStmt, body]
|
|
2151
2853
|
r = prep(['for', [';', decls, cond, step], inner])
|
|
2152
2854
|
} else if (Array.isArray(head) && head[0] === 'in') {
|
|
2153
2855
|
// `for…in` relies on runtime key enumeration — outside the pure canonical subset. strict
|
|
@@ -2193,8 +2895,22 @@ const handlers = {
|
|
|
2193
2895
|
['=', ks, keysExpr],
|
|
2194
2896
|
['=', ix, [, 0]],
|
|
2195
2897
|
['=', lenV, ['|', ['.', ks, 'length'], [, 0]]]]
|
|
2196
|
-
|
|
2197
|
-
|
|
2898
|
+
// Assignment-form bare name that resolves NOWHERE (`for (k in o)` /
|
|
2899
|
+
// `for (let in {})` with k undeclared — sloppy JS mints an implicit
|
|
2900
|
+
// global): declare it in the loop's own decls so the binding exists at
|
|
2901
|
+
// every opt level (emit otherwise leaks watr's "Unknown local $k"; O2
|
|
2902
|
+
// only masked it by constant-propagating the name away). Loop-scoped
|
|
2903
|
+
// rather than JS's implicit global (documented subset divergence). Only
|
|
2904
|
+
// this structural write-only binder mints — a general write-legalization
|
|
2905
|
+
// in emit let undeclared READS resolve (test262 ReferenceError pins).
|
|
2906
|
+
if (!isMemberTarget && !isDecl && typeof target === 'string'
|
|
2907
|
+
&& !isDeclared(target) && !hasFunc(target) && !ctx.scope.userGlobals?.has?.(target))
|
|
2908
|
+
decls.push(['=', target, [null]])
|
|
2909
|
+
// Member targets AND assignment-form bare names (`for (k in o)`) assign
|
|
2910
|
+
// the existing binding — a `let` wrap shadowed the outer k, so after-loop
|
|
2911
|
+
// reads saw the stale value. Only decl heads take a fresh binding.
|
|
2912
|
+
const bindEach = isMemberTarget || !isDecl
|
|
2913
|
+
? ['=', target, ['[]', ks, ix]] // x.y = key / k = key (existing binding)
|
|
2198
2914
|
: ['let', ['=', target, ['[]', ks, ix]]] // let k = key (fresh per-iteration binding)
|
|
2199
2915
|
const forNode = ['for', [';', decls, ['<', ix, lenV], ['++', ix]],
|
|
2200
2916
|
[';', bindEach, body]]
|
|
@@ -2225,9 +2941,11 @@ const handlers = {
|
|
|
2225
2941
|
// A user binding named like a builtin namespace (`let Math = {…}`) shadows it
|
|
2226
2942
|
// — read the property off the local value, not the builtin namespace table.
|
|
2227
2943
|
if (shadowsBuiltin(obj)) { includeForProperty(prop); return ['.', prep(obj), prop] }
|
|
2228
|
-
|
|
2944
|
+
// Function-scoped namespace aliases resolve here too (namespaceModOf) — the
|
|
2945
|
+
// module-level chain alone missed `const M = Math; M.sqrt` inside a body.
|
|
2946
|
+
const mod = namespaceModOf(obj)
|
|
2229
2947
|
// Only treat as module namespace if it's a known built-in module (not a mangled import name)
|
|
2230
|
-
if (
|
|
2948
|
+
if (mod) {
|
|
2231
2949
|
includeModule(mod)
|
|
2232
2950
|
const key = mod + '.' + prop
|
|
2233
2951
|
if (emitArity(ctx.core.emit[key]) > 0) includeForCallableValue()
|
|
@@ -2333,6 +3051,10 @@ function defFunc(name, node) {
|
|
|
2333
3051
|
if (!Array.isArray(node) || node[0] !== '=>') return false
|
|
2334
3052
|
// Only extract top-level functions, not nested (closures stay as values)
|
|
2335
3053
|
if (depth > 0) return false
|
|
3054
|
+
// A reassigned binding must stay a mutable closure-valued global — lifting it
|
|
3055
|
+
// into a fixed named function froze callers onto the first value (see
|
|
3056
|
+
// reassignedTopLevel). 'default' can't be reassigned (export default).
|
|
3057
|
+
if (name !== 'default' && reassignedTopLevel?.has(name)) return false
|
|
2336
3058
|
let [, rawParams, body] = node
|
|
2337
3059
|
const raw = extractParams(rawParams)
|
|
2338
3060
|
|
|
@@ -2345,7 +3067,12 @@ function defFunc(name, node) {
|
|
|
2345
3067
|
else if (c.kind === 'plain') params.push({ name: c.name, type: 'f64' })
|
|
2346
3068
|
else if (c.kind === 'default') {
|
|
2347
3069
|
params.push({ name: c.name, type: 'f64' })
|
|
2348
|
-
|
|
3070
|
+
// defFunc's node arrives PREPPED (every caller passes prep(rhs); the body is
|
|
3071
|
+
// consumed as-is below) — so the default value is prepped too. Re-prepping it
|
|
3072
|
+
// here double-lowered an arrow default's body: its prepared 5-ary 'for' nodes
|
|
3073
|
+
// re-entered the 2-ary 'for' handler, shifting init/cond/step into the wrong
|
|
3074
|
+
// slots (surfaced by subscript 10.5.0's dispatch(ops, tail, fn = (…) => {for…}) ).
|
|
3075
|
+
const defVal = c.defValue
|
|
2349
3076
|
defaults[c.name] = defVal
|
|
2350
3077
|
if (Array.isArray(defVal) && defVal[0] === '{}' && defVal.length > 1 && ctx.schema.register) {
|
|
2351
3078
|
const props = defVal.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
|
|
@@ -2354,7 +3081,7 @@ function defFunc(name, node) {
|
|
|
2354
3081
|
} else {
|
|
2355
3082
|
const tmp = `${T}p${ctx.func.uniq++}`
|
|
2356
3083
|
params.push({ name: tmp, type: 'f64' })
|
|
2357
|
-
if (c.kind === 'destruct-default') defaults[tmp] =
|
|
3084
|
+
if (c.kind === 'destruct-default') defaults[tmp] = c.defValue // prepped (see 'default' above)
|
|
2358
3085
|
bodyPrefix.push(['let', ['=', c.pattern, tmp]])
|
|
2359
3086
|
}
|
|
2360
3087
|
}
|
|
@@ -2486,8 +3213,12 @@ function prepareModule(specifier, source) {
|
|
|
2486
3213
|
ast = ctx.transform.parse(source)
|
|
2487
3214
|
}
|
|
2488
3215
|
if (ctx.transform.jzify) ast = ctx.transform.jzify(ast)
|
|
3216
|
+
ast = hoistIndexedConstLiterals(ast)
|
|
2489
3217
|
const savedDepth = depth; depth = 0
|
|
3218
|
+
const savedReassigned = reassignedTopLevel
|
|
3219
|
+
reassignedTopLevel = scanReassignedTopLevel(ast)
|
|
2490
3220
|
const moduleInit = prep(ast)
|
|
3221
|
+
reassignedTopLevel = savedReassigned
|
|
2491
3222
|
depth = savedDepth
|
|
2492
3223
|
|
|
2493
3224
|
// Collect exports: rename exported funcs with prefix
|
|
@@ -2495,6 +3226,16 @@ function prepareModule(specifier, source) {
|
|
|
2495
3226
|
const exportLocal = (exportName, localName) => {
|
|
2496
3227
|
const mangled = `${prefix}$${localName}`
|
|
2497
3228
|
moduleExports.set(exportName, mangled)
|
|
3229
|
+
// Aliased export (`export { helper as poles }`, `export default helper`):
|
|
3230
|
+
// exportName ('poles'/'default') is what IMPORTERS see, but in-module call
|
|
3231
|
+
// sites still reference the ORIGINAL local name ('helper') verbatim — the
|
|
3232
|
+
// walk below rewrites references by exact string match against this same
|
|
3233
|
+
// map, so without a second entry keyed on localName it never finds them and
|
|
3234
|
+
// they dangle as a call to a function that no longer exists post-rename
|
|
3235
|
+
// ("'helper' is not in scope"). Un-aliased exports (`export {helper}`,
|
|
3236
|
+
// `exportLocal(name, name)`) already have exportName === localName, so this
|
|
3237
|
+
// is a no-op there.
|
|
3238
|
+
if (localName !== exportName) moduleExports.set(localName, mangled)
|
|
2498
3239
|
const func = ctx.func.list.find(f => f.name === localName)
|
|
2499
3240
|
if (func) { renameFunc(func, mangled); func._modulePrefix = prefix }
|
|
2500
3241
|
if (ctx.scope.globals.has(localName)) {
|
|
@@ -2543,16 +3284,12 @@ function prepareModule(specifier, source) {
|
|
|
2543
3284
|
// Already renamed as a named export
|
|
2544
3285
|
moduleExports.set('default', moduleExports.get(alias))
|
|
2545
3286
|
} else {
|
|
2546
|
-
// Not a named export — rename the function/global
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
ctx.scope.globals.set(mangled, ctx.scope.globals.get(alias))
|
|
2553
|
-
ctx.scope.globals.delete(alias)
|
|
2554
|
-
if (ctx.scope.userGlobals.has(alias)) { ctx.scope.userGlobals.delete(alias); ctx.scope.userGlobals.add(mangled) }
|
|
2555
|
-
}
|
|
3287
|
+
// Not a named export — rename the function/global. `export default helper`
|
|
3288
|
+
// is itself an aliased export (exportName 'default' vs localName `alias`),
|
|
3289
|
+
// the same shape `exportLocal` already handles (incl. registering `alias`
|
|
3290
|
+
// as its own walk-lookup key) — delegate instead of re-deriving the same
|
|
3291
|
+
// logic with a narrower (and previously buggy — see exportLocal) copy.
|
|
3292
|
+
exportLocal('default', alias)
|
|
2556
3293
|
}
|
|
2557
3294
|
}
|
|
2558
3295
|
|
|
@@ -2564,6 +3301,11 @@ function prepareModule(specifier, source) {
|
|
|
2564
3301
|
const func = ctx.func.list[i]
|
|
2565
3302
|
if (func.raw || func.name.startsWith(prefix + '$')) continue
|
|
2566
3303
|
if (func._modulePrefix && func._modulePrefix !== prefix) continue
|
|
3304
|
+
// Cross-module func-prop lifts carry the OWNING module's prefix in their
|
|
3305
|
+
// name already (`__A$lex$next` written from module B) — mangling again
|
|
3306
|
+
// would double-prefix and break the owner's direct-call resolution. Their
|
|
3307
|
+
// bodies still take THIS module's reference walk below.
|
|
3308
|
+
if (func._ownerPrefix && func._ownerPrefix !== prefix) continue
|
|
2567
3309
|
const mangled = `${prefix}$${func.name}`
|
|
2568
3310
|
moduleExports.set(func.name, mangled)
|
|
2569
3311
|
renameFunc(func, mangled)
|