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.
Files changed (73) hide show
  1. package/README.md +56 -9
  2. package/bench/README.md +121 -50
  3. package/bench/bench.svg +27 -27
  4. package/cli.js +3 -1
  5. package/dist/interop.js +1 -1
  6. package/dist/jz.js +7541 -6178
  7. package/index.js +165 -74
  8. package/interop.js +189 -17
  9. package/jzify/arguments.js +32 -1
  10. package/jzify/async.js +409 -0
  11. package/jzify/classes.js +136 -18
  12. package/jzify/generators.js +639 -0
  13. package/jzify/hoist-vars.js +73 -1
  14. package/jzify/index.js +123 -4
  15. package/jzify/names.js +1 -0
  16. package/jzify/transform.js +239 -1
  17. package/layout.js +48 -3
  18. package/module/array.js +318 -43
  19. package/module/atomics.js +144 -0
  20. package/module/collection.js +1429 -155
  21. package/module/core.js +431 -40
  22. package/module/date.js +142 -120
  23. package/module/fs.js +144 -0
  24. package/module/function.js +6 -3
  25. package/module/index.js +4 -1
  26. package/module/json.js +270 -49
  27. package/module/math.js +1032 -145
  28. package/module/number.js +532 -163
  29. package/module/object.js +353 -95
  30. package/module/regex.js +157 -7
  31. package/module/schema.js +100 -2
  32. package/module/string.js +428 -93
  33. package/module/typedarray.js +264 -72
  34. package/module/web.js +36 -0
  35. package/package.json +5 -5
  36. package/src/abi/string.js +82 -11
  37. package/src/ast.js +11 -5
  38. package/src/autoload.js +28 -1
  39. package/src/bridge.js +7 -2
  40. package/src/compile/analyze-scans.js +22 -7
  41. package/src/compile/analyze.js +136 -30
  42. package/src/compile/dyn-closure-tables.js +277 -0
  43. package/src/compile/emit-assign.js +281 -15
  44. package/src/compile/emit.js +1055 -70
  45. package/src/compile/index.js +277 -29
  46. package/src/compile/infer.js +42 -4
  47. package/src/compile/inplace-store.js +329 -0
  48. package/src/compile/loop-recurrence.js +167 -0
  49. package/src/compile/narrow.js +555 -18
  50. package/src/compile/peel-stencil.js +5 -0
  51. package/src/compile/plan/index.js +45 -3
  52. package/src/compile/plan/inline.js +8 -1
  53. package/src/compile/plan/literals.js +39 -0
  54. package/src/compile/plan/scope.js +430 -23
  55. package/src/compile/program-facts.js +732 -38
  56. package/src/ctx.js +64 -9
  57. package/src/helper-counters.js +7 -1
  58. package/src/ir.js +113 -5
  59. package/src/kind-traits.js +66 -3
  60. package/src/kind.js +84 -12
  61. package/src/op-policy.js +7 -4
  62. package/src/optimize/index.js +1179 -704
  63. package/src/optimize/recurse.js +2 -2
  64. package/src/optimize/vectorize.js +982 -67
  65. package/src/prepare/index.js +809 -67
  66. package/src/prepare/math-kernel.js +331 -0
  67. package/src/prepare/pre-eval.js +714 -0
  68. package/src/snapshot.js +194 -0
  69. package/src/type.js +1170 -56
  70. package/src/wat/assemble.js +403 -65
  71. package/src/wat/codegen.js +74 -14
  72. package/transform.js +113 -4
  73. package/wasi.js +3 -0
@@ -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. It diverges from JS by evaluating to 0.')
832
- // jz's `==`/`!=` already never coerce (identical to `===`/`!==`), so default mode accepts them.
833
- // strict enforces the canonical subset, where `===`/`!==` are the one spelling reject the loose form.
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}=\`. (jz's \`${op}\` doesn't coerce, but the canonical subset is \`===\`/\`!==\` only.)`)
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
- const patternItems = (node) => node?.[0] === ',' ? node.slice(1) : [node]
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
- pushPatternAssign(target[1], ['??', valueExpr, prep(target[2])], out, decls)
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[1], ['??', ['.', source, item[1]], prep(item[2])], out, decls)
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
- pushPatternAssign(item[2], computedKey ? ['[]', source, computedKey] : ['.', source, key], out, decls)
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 [, 0]
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 = ctx.scope.chain[obj]
1326
- if (typeof obj === 'string' && mod && !mod.includes('.') && hasModule(mod))
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
- if (defFunc(name, prep(rhs))) return ['=', ['.', fnBase, lhs[2]], name]
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, catchClause[1], prep(catchClause[2])]
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) => [';', ...stmts.map(prep).filter(x => x != null)],
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] === '=' && typeof i[1] === 'string')
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: prep discriminant and case values/bodies
1810
- // Parser appends fall-through flag (number) to case bodies strip it
1811
- 'switch'(discriminant, ...cases) {
1812
- const prepCase = body => {
1813
- if (Array.isArray(body) && body[0] === ';')
1814
- return prep([';', ...body.slice(1).filter(s => typeof s !== 'number')])
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.) jzify lands it as a bare `,`
2081
- // node in the source slot. Don't wrap it in `()`: Object.keys((a, obj)) hides `obj` behind the
2082
- // sequence and loses its static schema (a non-escaping literal scalarizes → 0 keys). Instead take
2083
- // the LAST element as the (direct) iteration source and run the earlier elements once first.
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 varName = Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const') ? decl[1] : decl
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
- const inner = [';', ['let', ['=', varName, ['[]', arrVar, idx]]], body]
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
- const bindEach = isMemberTarget
2197
- ? ['=', target, ['[]', ks, ix]] // x.y = key / obj[k] = key
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
- const mod = ctx.scope.chain[obj]
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 (typeof obj === 'string' && mod && !mod.includes('.') && hasModule(mod)) {
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
- const defVal = prep(c.defValue)
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] = prep(c.defValue)
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
- const mangled = `${prefix}$${alias}`
2548
- moduleExports.set('default', mangled)
2549
- const func = ctx.func.list.find(f => f.name === alias)
2550
- if (func) renameFunc(func, mangled)
2551
- if (ctx.scope.globals.has(alias)) {
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)