jz 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -9
- package/bench/README.md +121 -50
- package/bench/bench.svg +23 -23
- package/cli.js +3 -1
- package/dist/interop.js +1 -1
- package/dist/jz.js +7541 -6271
- 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 +299 -44
- 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 +892 -32
- 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 +301 -84
- package/module/typedarray.js +264 -72
- package/module/web.js +36 -0
- package/package.json +5 -5
- package/src/abi/string.js +71 -5
- 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 +979 -54
- package/src/compile/index.js +271 -29
- package/src/compile/infer.js +34 -3
- package/src/compile/inplace-store.js +329 -0
- package/src/compile/narrow.js +543 -15
- 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 +1060 -750
- package/src/optimize/recurse.js +2 -2
- package/src/optimize/vectorize.js +972 -67
- package/src/prepare/index.js +792 -63
- 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
|
|
@@ -287,6 +356,67 @@ function collectTopLevelStaticAssignments(node, facts) {
|
|
|
287
356
|
if (str != null || arr) facts.set(node[1], { str, arr })
|
|
288
357
|
}
|
|
289
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
|
+
|
|
290
420
|
function seedStaticGlobalAssignments(node) {
|
|
291
421
|
// jzify hoists function declarations ahead of `var` initializer assignments.
|
|
292
422
|
// Seed one-write static globals before preparing those function bodies so
|
|
@@ -388,15 +518,6 @@ function staticStringExpr(node) {
|
|
|
388
518
|
}
|
|
389
519
|
return out
|
|
390
520
|
}
|
|
391
|
-
if (op === '``' && Array.isArray(args[0]) && args[0][0] === '.' && args[0][1] === 'String' && args[0][2] === 'raw') {
|
|
392
|
-
let out = ''
|
|
393
|
-
for (const part of args.slice(1)) {
|
|
394
|
-
const s = staticStringExpr(part)
|
|
395
|
-
if (s == null) return null
|
|
396
|
-
out += s
|
|
397
|
-
}
|
|
398
|
-
return out
|
|
399
|
-
}
|
|
400
521
|
if (op === '()' && Array.isArray(args[0]) && args[0][0] === '.' && args[0][2] === 'join' && typeof args[0][1] === 'string') {
|
|
401
522
|
const arr = lookupStaticStringArray(args[0][1])
|
|
402
523
|
if (!arr) return null
|
|
@@ -427,7 +548,8 @@ function recordModuleInitFacts(root) {
|
|
|
427
548
|
dynVars: new Set(), dynWriteVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
|
|
428
549
|
hasFuncValue: false, timerNames: new Set(),
|
|
429
550
|
maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false,
|
|
430
|
-
writtenProps: new Set(),
|
|
551
|
+
writtenProps: new Set(), literalWriteKeys: new Map(),
|
|
552
|
+
arrResized: new Set(), nameEscapes: new Set(),
|
|
431
553
|
}
|
|
432
554
|
const visitFuncValue = (node) => {
|
|
433
555
|
if (facts.hasFuncValue || !Array.isArray(node)) return
|
|
@@ -508,6 +630,8 @@ export default function prepare(node) {
|
|
|
508
630
|
normalizeIdents(node)
|
|
509
631
|
fuseSparseMapReads(node) // AST-level fusion; needs pre-resolution shape — defined at end of file
|
|
510
632
|
seedStaticGlobalAssignments(node)
|
|
633
|
+
node = hoistIndexedConstLiterals(node)
|
|
634
|
+
reassignedTopLevel = scanReassignedTopLevel(node)
|
|
511
635
|
const ast = prep(node)
|
|
512
636
|
// Top-level functions referenced as first-class values (e.g. `let o = { fn: g }`,
|
|
513
637
|
// `arr.push(g)`, `return g`) need trampoline emission, which depends on the fn
|
|
@@ -625,7 +749,7 @@ function isDeclared(name) {
|
|
|
625
749
|
|
|
626
750
|
function pushScope(scope = new Map()) {
|
|
627
751
|
scopes.push(scope)
|
|
628
|
-
staticConstScopes.push({ strings: new Map(), arrays: new Map() })
|
|
752
|
+
staticConstScopes.push({ strings: new Map(), arrays: new Map(), consts: new Set() })
|
|
629
753
|
}
|
|
630
754
|
|
|
631
755
|
function popScope() {
|
|
@@ -683,8 +807,17 @@ const hasFunc = name => ctx.func.names.has(name)
|
|
|
683
807
|
// fast-paths bail and fall through to `resolveCallee`, which already routes a
|
|
684
808
|
// declared name to its local value. Mirrors the guard in
|
|
685
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
|
+
}
|
|
686
819
|
const shadowsBuiltin = name => typeof name === 'string' &&
|
|
687
|
-
((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))
|
|
688
821
|
// A local bound to a function literal in any active arrow scope (the nested-
|
|
689
822
|
// closure counterpart to `hasFunc`, which only knows depth-0 lifted functions).
|
|
690
823
|
const isFuncValueLocal = name => typeof name === 'string' && funcValueNames.some(s => s.has(name))
|
|
@@ -762,6 +895,25 @@ function resolveTypeof(node) {
|
|
|
762
895
|
return node
|
|
763
896
|
}
|
|
764
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
|
+
|
|
765
917
|
// Prepare a strict `===`/`!==`. resolveTypeof may fold `typeof x === 'type'` to a
|
|
766
918
|
// literal or rewrite it to a numeric-code compare; either way we prep the result's
|
|
767
919
|
// operands directly. The strict op stays intact (no collapse to loose `==`) so
|
|
@@ -818,6 +970,9 @@ function prep(node) {
|
|
|
818
970
|
if (node in CONSTANTS) return [, CONSTANTS[node]]
|
|
819
971
|
if (node in F64_CONSTANTS) return [, F64_CONSTANTS[node]]
|
|
820
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`)
|
|
821
976
|
// Boolean/Number as value → identity arrow (for .filter(Boolean), .map(Number) etc.)
|
|
822
977
|
if (node === 'Boolean' || node === 'Number') { includeForCallableValue(); return ['=>', 'x', 'x'] }
|
|
823
978
|
// Block locals shadow module imports/globals, even when the local keeps the same name.
|
|
@@ -841,11 +996,26 @@ function prep(node) {
|
|
|
841
996
|
}
|
|
842
997
|
|
|
843
998
|
const [op, ...args] = node
|
|
844
|
-
if (op === 'void' && ctx.transform.strict) err('strict mode: `void` is prohibited
|
|
845
|
-
// jz's `==`/`!=`
|
|
846
|
-
//
|
|
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.
|
|
847
1003
|
if ((op === '==' || op === '!=') && ctx.transform.strict)
|
|
848
|
-
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
|
+
}
|
|
849
1019
|
if (op == null) {
|
|
850
1020
|
if (typeof args[0] === 'string') {
|
|
851
1021
|
includeForStringValue()
|
|
@@ -867,6 +1037,8 @@ function prep(node) {
|
|
|
867
1037
|
// need a list of these names to know what to emit, so the table lives here either way.
|
|
868
1038
|
export const GLOBALS = Object.assign(Object.create(null), {
|
|
869
1039
|
Math: 'math',
|
|
1040
|
+
fs: 'fs',
|
|
1041
|
+
fetch: 'web',
|
|
870
1042
|
Number: 'Number',
|
|
871
1043
|
Array: 'Array',
|
|
872
1044
|
Object: 'Object',
|
|
@@ -897,7 +1069,10 @@ export const GLOBALS = Object.assign(Object.create(null), {
|
|
|
897
1069
|
TextDecoder: 'TextDecoder',
|
|
898
1070
|
})
|
|
899
1071
|
|
|
900
|
-
|
|
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]
|
|
901
1076
|
const isDestructPattern = (node) => Array.isArray(node) && (node[0] === '[]' || node[0] === '{}')
|
|
902
1077
|
|
|
903
1078
|
// Element count of a prepared inline array literal `['[', e0, e1, …]` with no
|
|
@@ -956,9 +1131,37 @@ function bindingNames(pattern, out = new Set()) {
|
|
|
956
1131
|
return out
|
|
957
1132
|
}
|
|
958
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
|
+
|
|
959
1156
|
function pushPatternAssign(target, valueExpr, out, decls = null) {
|
|
960
1157
|
if (Array.isArray(target) && target[0] === '=') {
|
|
961
|
-
|
|
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)
|
|
962
1165
|
return
|
|
963
1166
|
}
|
|
964
1167
|
|
|
@@ -1026,8 +1229,9 @@ function expandDestruct(pattern, source, out, decls = null, srcLen = null) {
|
|
|
1026
1229
|
}
|
|
1027
1230
|
|
|
1028
1231
|
if (Array.isArray(item) && item[0] === '=') {
|
|
1232
|
+
// Route through pushPatternAssign's `=` case: undefined-only default.
|
|
1029
1233
|
if (typeof item[1] === 'string')
|
|
1030
|
-
pushPatternAssign(item
|
|
1234
|
+
pushPatternAssign(item, ['.', source, item[1]], out, decls)
|
|
1031
1235
|
continue
|
|
1032
1236
|
}
|
|
1033
1237
|
|
|
@@ -1035,7 +1239,17 @@ function expandDestruct(pattern, source, out, decls = null, srcLen = null) {
|
|
|
1035
1239
|
const key = item[1]
|
|
1036
1240
|
const computedKey = Array.isArray(key) && key[0] === '[]' && key.length === 2 ? key[1] : null
|
|
1037
1241
|
if (computedKey) includeForArrayAccess()
|
|
1038
|
-
|
|
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)
|
|
1039
1253
|
continue
|
|
1040
1254
|
}
|
|
1041
1255
|
}
|
|
@@ -1060,6 +1274,196 @@ function expandDestruct(pattern, source, out, decls = null, srcLen = null) {
|
|
|
1060
1274
|
}
|
|
1061
1275
|
}
|
|
1062
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
|
+
|
|
1063
1467
|
/** Prepare let/const declaration. */
|
|
1064
1468
|
function prepDecl(op, ...inits) {
|
|
1065
1469
|
const rest = []
|
|
@@ -1113,7 +1517,67 @@ function prepDecl(op, ...inits) {
|
|
|
1113
1517
|
const staticArr = op === 'const' ? staticStringArrayValues(init) : null
|
|
1114
1518
|
const normed = prep(init)
|
|
1115
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
|
+
|
|
1116
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
|
+
}
|
|
1117
1581
|
// Register each binding both as a module global (depth 0) and in the
|
|
1118
1582
|
// current arrow's local scope (depth ≠ 0). Without the local registration
|
|
1119
1583
|
// the name is invisible to `isUnresolvableBareIdent`, so a later
|
|
@@ -1167,6 +1631,11 @@ function prepDecl(op, ...inits) {
|
|
|
1167
1631
|
if (typeof declName === 'string' && Array.isArray(normed) && normed[0] === '=>')
|
|
1168
1632
|
funcValueNames[funcValueNames.length - 1]?.add(declName)
|
|
1169
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)
|
|
1170
1639
|
// Track const for reassignment checks — only module-scope consts (depth 0)
|
|
1171
1640
|
if (typeof declName === 'string' && depth === 0) {
|
|
1172
1641
|
if (ctx.module.currentPrefix) {
|
|
@@ -1262,6 +1731,87 @@ function dispatchConstructorCall(callee, args) {
|
|
|
1262
1731
|
return undefined
|
|
1263
1732
|
}
|
|
1264
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
|
+
|
|
1265
1815
|
// Compile-time namespace introspection on a `obj.prop(...)` callee:
|
|
1266
1816
|
// `Array.isArray(NS)` on a bare builtin global folds to `false` (a namespace
|
|
1267
1817
|
// value is never an array); `NS.hasOwnProperty("member")` on a builtin
|
|
@@ -1273,8 +1823,11 @@ function foldNamespaceIntrospection(callee, args) {
|
|
|
1273
1823
|
if (obj === 'Array' && prop === 'isArray') {
|
|
1274
1824
|
const cargs = handlerArgs(args)
|
|
1275
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()).
|
|
1276
1829
|
if (typeof a0 === 'string' && GLOBALS[a0] && !(scopes.length && isDeclared(a0)) && !hasFunc(a0))
|
|
1277
|
-
return [,
|
|
1830
|
+
return [, false]
|
|
1278
1831
|
}
|
|
1279
1832
|
if (prop === 'hasOwnProperty' && typeof obj === 'string' && !(scopes.length && isDeclared(obj))) {
|
|
1280
1833
|
const mod = ctx.scope.chain[obj]
|
|
@@ -1298,6 +1851,15 @@ function foldNamespaceIntrospection(callee, args) {
|
|
|
1298
1851
|
// (function table / closure) machinery.
|
|
1299
1852
|
const INTRINSIC_CALLEES = new Set(['__iter_arr', '__keys_ro'])
|
|
1300
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
|
+
|
|
1301
1863
|
function resolveCallee(callee, args) {
|
|
1302
1864
|
if (typeof callee === 'string') {
|
|
1303
1865
|
const local = scopes.length && isDeclared(callee)
|
|
@@ -1305,6 +1867,14 @@ function resolveCallee(callee, args) {
|
|
|
1305
1867
|
if (local) return resolveScope(callee)
|
|
1306
1868
|
if (resolved?.includes('.')) return resolved
|
|
1307
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
|
+
}
|
|
1308
1878
|
if (resolved && !resolved.includes('.')) {
|
|
1309
1879
|
if (hasModule(resolved) && !ctx.module.imports.some(i => i[3]?.[1] === `$${resolved}`)) includeModule(resolved)
|
|
1310
1880
|
return callee
|
|
@@ -1335,8 +1905,8 @@ function resolveCallee(callee, args) {
|
|
|
1335
1905
|
}
|
|
1336
1906
|
if (key && includeForNamedCall(key)) return key
|
|
1337
1907
|
if (includeForGenericMethod(prop)) return prep(callee)
|
|
1338
|
-
const mod =
|
|
1339
|
-
if (
|
|
1908
|
+
const mod = namespaceModOf(obj)
|
|
1909
|
+
if (mod)
|
|
1340
1910
|
return (includeModule(mod), mod + '.' + prop)
|
|
1341
1911
|
return prep(callee)
|
|
1342
1912
|
}
|
|
@@ -1388,6 +1958,20 @@ const handlers = {
|
|
|
1388
1958
|
// Destructuring assignment: [a, ...r] = expr or ({x: a} = expr)
|
|
1389
1959
|
// Distinguishing from index assignment: destructuring patterns have exactly one payload node.
|
|
1390
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
|
+
|
|
1391
1975
|
const scalar = scalarArrayDestruct(lhs, rhs)
|
|
1392
1976
|
if (scalar) return scalar
|
|
1393
1977
|
|
|
@@ -1424,7 +2008,24 @@ const handlers = {
|
|
|
1424
2008
|
// Build the target `.` node directly from the resolved base — re-`prep`ing
|
|
1425
2009
|
// the lhs would resolve a multiProp `fn.prop` to an rvalue (closure
|
|
1426
2010
|
// materialization block), which is not a valid assignment target.
|
|
1427
|
-
|
|
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
|
+
}
|
|
1428
2029
|
}
|
|
1429
2030
|
}
|
|
1430
2031
|
const staticStr = staticStringExpr(rhs)
|
|
@@ -1466,10 +2067,24 @@ const handlers = {
|
|
|
1466
2067
|
const catchClause = clauses.find(c => Array.isArray(c) && c[0] === 'catch')
|
|
1467
2068
|
const finallyClause = clauses.find(c => Array.isArray(c) && c[0] === 'finally')
|
|
1468
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
|
+
}
|
|
1469
2084
|
// prep(handler) ONCE — it has side effects (uniq++, scope pushes, includes), so
|
|
1470
2085
|
// the no-finally catch branch must reuse `caught`, not re-prep (FE-3 fix).
|
|
1471
2086
|
const caught = catchClause
|
|
1472
|
-
? ['catch', tryBody,
|
|
2087
|
+
? ['catch', tryBody, cParam, prep(cHandler)]
|
|
1473
2088
|
: tryBody
|
|
1474
2089
|
return finallyClause ? ['finally', caught, prep(finallyClause[1])] : caught
|
|
1475
2090
|
},
|
|
@@ -1489,6 +2104,13 @@ const handlers = {
|
|
|
1489
2104
|
|
|
1490
2105
|
// Tagged template: tag`a${x}b` → tag(['a','b'], x)
|
|
1491
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')
|
|
1492
2114
|
const raw = staticStringExpr(['``', tag, ...parts])
|
|
1493
2115
|
if (raw != null) return staticString(raw)
|
|
1494
2116
|
const strs = [], exprs = []
|
|
@@ -1648,8 +2270,18 @@ const handlers = {
|
|
|
1648
2270
|
'==='(a, b) { return prepStrictEq('===', a, b) },
|
|
1649
2271
|
'!=='(a, b) { return prepStrictEq('!==', a, b) },
|
|
1650
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
|
+
|
|
1651
2280
|
// Statements
|
|
1652
|
-
';': (...stmts) =>
|
|
2281
|
+
';': (...stmts) => {
|
|
2282
|
+
preRegisterBuiltinAliases(stmts)
|
|
2283
|
+
return [';', ...stmts.map(prep).filter(x => x != null).map(dropDeadPostfix)]
|
|
2284
|
+
},
|
|
1653
2285
|
'let': (...inits) => prepDecl('let', ...inits),
|
|
1654
2286
|
'const': (...inits) => prepDecl('const', ...inits),
|
|
1655
2287
|
|
|
@@ -1680,8 +2312,13 @@ const handlers = {
|
|
|
1680
2312
|
'export': decl => {
|
|
1681
2313
|
if (Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const'))
|
|
1682
2314
|
for (const i of decl.slice(1))
|
|
1683
|
-
if (Array.isArray(i) && i[0] === '='
|
|
1684
|
-
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
|
+
}
|
|
1685
2322
|
// export name → bare-identifier re-export (shorthand for `export { name }`).
|
|
1686
2323
|
// Register the binding and emit nothing; without this the name falls through
|
|
1687
2324
|
// to `prep(decl)` below and compiles as a dead `global.get; drop` statement
|
|
@@ -1819,19 +2456,12 @@ const handlers = {
|
|
|
1819
2456
|
return result
|
|
1820
2457
|
},
|
|
1821
2458
|
|
|
1822
|
-
// Switch
|
|
1823
|
-
//
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
return prep(body)
|
|
1829
|
-
}
|
|
1830
|
-
return ['switch', prep(discriminant), ...cases.map(c => {
|
|
1831
|
-
if (c[0] === 'case') return ['case', prep(c[1]), prepCase(c[2])]
|
|
1832
|
-
if (c[0] === 'default') return ['default', prep(c[1])]
|
|
1833
|
-
return prep(c)
|
|
1834
|
-
})]
|
|
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)')
|
|
1835
2465
|
},
|
|
1836
2466
|
|
|
1837
2467
|
// Optional chaining / typeof — need ptr module. Optional member access pulls
|
|
@@ -1938,6 +2568,8 @@ const handlers = {
|
|
|
1938
2568
|
const folded = foldImportMetaResolve(callee, args)
|
|
1939
2569
|
?? dispatchConstructorCall(callee, args)
|
|
1940
2570
|
?? foldNamespaceIntrospection(callee, args)
|
|
2571
|
+
?? foldFnCallApplyBind(callee, args)
|
|
2572
|
+
?? foldJsonReviver(callee, args)
|
|
1941
2573
|
if (folded !== undefined) return folded
|
|
1942
2574
|
|
|
1943
2575
|
callee = resolveCallee(callee, args)
|
|
@@ -1956,6 +2588,20 @@ const handlers = {
|
|
|
1956
2588
|
// `(x.pop)` and drop the call. Keeping the slot makes `prep` idempotent for
|
|
1957
2589
|
// calls and matches `setCallArgs`'s canonical shape; `commaList(node[2])`
|
|
1958
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
|
+
|
|
1959
2605
|
const result = preppedArgs.length ? ['()', callee, ...preppedArgs] : ['()', callee, null]
|
|
1960
2606
|
|
|
1961
2607
|
if (callee === 'Object.assign' && ctx.schema.register) inferAssignSchema(result)
|
|
@@ -2087,13 +2733,46 @@ const handlers = {
|
|
|
2087
2733
|
|
|
2088
2734
|
// For loop
|
|
2089
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
|
+
}
|
|
2090
2767
|
pushScope()
|
|
2091
2768
|
// A comma/sequence Expression in a for-IN head RHS — `for (x in a, b)` — is valid (the RHS is
|
|
2092
2769
|
// an Expression): evaluate left-to-right for side effects, value as the last element. (for-OF's
|
|
2093
|
-
// RHS is an AssignmentExpression — no comma — so it is left alone.)
|
|
2094
|
-
// node in the source slot. Don't wrap it in `()`:
|
|
2095
|
-
//
|
|
2096
|
-
//
|
|
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.
|
|
2097
2776
|
let forInSeqPre = null
|
|
2098
2777
|
if (Array.isArray(head) && head[0] === 'in' && Array.isArray(head[2]) && head[2][0] === ',') {
|
|
2099
2778
|
const parts = head[2].slice(1)
|
|
@@ -2146,7 +2825,10 @@ const handlers = {
|
|
|
2146
2825
|
// Divergence from JS: mutating arr during iteration won't extend/shorten the loop.
|
|
2147
2826
|
// jz philosophy: explicit > implicit; mutation during iteration is a code smell.
|
|
2148
2827
|
const [, decl, src] = head
|
|
2149
|
-
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
|
|
2150
2832
|
const idx = `${T}i${ctx.func.uniq++}`
|
|
2151
2833
|
const lenVar = `${T}len${ctx.func.uniq++}`
|
|
2152
2834
|
const arrVar = `${T}arr${ctx.func.uniq++}`
|
|
@@ -2160,7 +2842,14 @@ const handlers = {
|
|
|
2160
2842
|
const decls = ['let', ['=', arrVar, ['()', '__iter_arr', src]], ['=', idx, [, 0]], ['=', lenVar, lenE]]
|
|
2161
2843
|
const cond = ['<', idx, lenVar]
|
|
2162
2844
|
const step = ['++', idx]
|
|
2163
|
-
|
|
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]
|
|
2164
2853
|
r = prep(['for', [';', decls, cond, step], inner])
|
|
2165
2854
|
} else if (Array.isArray(head) && head[0] === 'in') {
|
|
2166
2855
|
// `for…in` relies on runtime key enumeration — outside the pure canonical subset. strict
|
|
@@ -2206,8 +2895,22 @@ const handlers = {
|
|
|
2206
2895
|
['=', ks, keysExpr],
|
|
2207
2896
|
['=', ix, [, 0]],
|
|
2208
2897
|
['=', lenV, ['|', ['.', ks, 'length'], [, 0]]]]
|
|
2209
|
-
|
|
2210
|
-
|
|
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)
|
|
2211
2914
|
: ['let', ['=', target, ['[]', ks, ix]]] // let k = key (fresh per-iteration binding)
|
|
2212
2915
|
const forNode = ['for', [';', decls, ['<', ix, lenV], ['++', ix]],
|
|
2213
2916
|
[';', bindEach, body]]
|
|
@@ -2238,9 +2941,11 @@ const handlers = {
|
|
|
2238
2941
|
// A user binding named like a builtin namespace (`let Math = {…}`) shadows it
|
|
2239
2942
|
// — read the property off the local value, not the builtin namespace table.
|
|
2240
2943
|
if (shadowsBuiltin(obj)) { includeForProperty(prop); return ['.', prep(obj), prop] }
|
|
2241
|
-
|
|
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)
|
|
2242
2947
|
// Only treat as module namespace if it's a known built-in module (not a mangled import name)
|
|
2243
|
-
if (
|
|
2948
|
+
if (mod) {
|
|
2244
2949
|
includeModule(mod)
|
|
2245
2950
|
const key = mod + '.' + prop
|
|
2246
2951
|
if (emitArity(ctx.core.emit[key]) > 0) includeForCallableValue()
|
|
@@ -2346,6 +3051,10 @@ function defFunc(name, node) {
|
|
|
2346
3051
|
if (!Array.isArray(node) || node[0] !== '=>') return false
|
|
2347
3052
|
// Only extract top-level functions, not nested (closures stay as values)
|
|
2348
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
|
|
2349
3058
|
let [, rawParams, body] = node
|
|
2350
3059
|
const raw = extractParams(rawParams)
|
|
2351
3060
|
|
|
@@ -2358,7 +3067,12 @@ function defFunc(name, node) {
|
|
|
2358
3067
|
else if (c.kind === 'plain') params.push({ name: c.name, type: 'f64' })
|
|
2359
3068
|
else if (c.kind === 'default') {
|
|
2360
3069
|
params.push({ name: c.name, type: 'f64' })
|
|
2361
|
-
|
|
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
|
|
2362
3076
|
defaults[c.name] = defVal
|
|
2363
3077
|
if (Array.isArray(defVal) && defVal[0] === '{}' && defVal.length > 1 && ctx.schema.register) {
|
|
2364
3078
|
const props = defVal.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
|
|
@@ -2367,7 +3081,7 @@ function defFunc(name, node) {
|
|
|
2367
3081
|
} else {
|
|
2368
3082
|
const tmp = `${T}p${ctx.func.uniq++}`
|
|
2369
3083
|
params.push({ name: tmp, type: 'f64' })
|
|
2370
|
-
if (c.kind === 'destruct-default') defaults[tmp] =
|
|
3084
|
+
if (c.kind === 'destruct-default') defaults[tmp] = c.defValue // prepped (see 'default' above)
|
|
2371
3085
|
bodyPrefix.push(['let', ['=', c.pattern, tmp]])
|
|
2372
3086
|
}
|
|
2373
3087
|
}
|
|
@@ -2499,8 +3213,12 @@ function prepareModule(specifier, source) {
|
|
|
2499
3213
|
ast = ctx.transform.parse(source)
|
|
2500
3214
|
}
|
|
2501
3215
|
if (ctx.transform.jzify) ast = ctx.transform.jzify(ast)
|
|
3216
|
+
ast = hoistIndexedConstLiterals(ast)
|
|
2502
3217
|
const savedDepth = depth; depth = 0
|
|
3218
|
+
const savedReassigned = reassignedTopLevel
|
|
3219
|
+
reassignedTopLevel = scanReassignedTopLevel(ast)
|
|
2503
3220
|
const moduleInit = prep(ast)
|
|
3221
|
+
reassignedTopLevel = savedReassigned
|
|
2504
3222
|
depth = savedDepth
|
|
2505
3223
|
|
|
2506
3224
|
// Collect exports: rename exported funcs with prefix
|
|
@@ -2508,6 +3226,16 @@ function prepareModule(specifier, source) {
|
|
|
2508
3226
|
const exportLocal = (exportName, localName) => {
|
|
2509
3227
|
const mangled = `${prefix}$${localName}`
|
|
2510
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)
|
|
2511
3239
|
const func = ctx.func.list.find(f => f.name === localName)
|
|
2512
3240
|
if (func) { renameFunc(func, mangled); func._modulePrefix = prefix }
|
|
2513
3241
|
if (ctx.scope.globals.has(localName)) {
|
|
@@ -2556,16 +3284,12 @@ function prepareModule(specifier, source) {
|
|
|
2556
3284
|
// Already renamed as a named export
|
|
2557
3285
|
moduleExports.set('default', moduleExports.get(alias))
|
|
2558
3286
|
} else {
|
|
2559
|
-
// Not a named export — rename the function/global
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
ctx.scope.globals.set(mangled, ctx.scope.globals.get(alias))
|
|
2566
|
-
ctx.scope.globals.delete(alias)
|
|
2567
|
-
if (ctx.scope.userGlobals.has(alias)) { ctx.scope.userGlobals.delete(alias); ctx.scope.userGlobals.add(mangled) }
|
|
2568
|
-
}
|
|
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)
|
|
2569
3293
|
}
|
|
2570
3294
|
}
|
|
2571
3295
|
|
|
@@ -2577,6 +3301,11 @@ function prepareModule(specifier, source) {
|
|
|
2577
3301
|
const func = ctx.func.list[i]
|
|
2578
3302
|
if (func.raw || func.name.startsWith(prefix + '$')) continue
|
|
2579
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
|
|
2580
3309
|
const mangled = `${prefix}$${func.name}`
|
|
2581
3310
|
moduleExports.set(func.name, mangled)
|
|
2582
3311
|
renameFunc(func, mangled)
|