jz 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +315 -318
  2. package/bench/README.md +369 -0
  3. package/bench/bench.svg +102 -0
  4. package/cli.js +104 -30
  5. package/dist/interop.js +1 -0
  6. package/dist/jz.js +6024 -0
  7. package/index.d.ts +126 -0
  8. package/index.js +362 -84
  9. package/interop.js +159 -186
  10. package/jz.svg +5 -0
  11. package/jzify/arguments.js +97 -0
  12. package/jzify/bundler.js +382 -0
  13. package/jzify/classes.js +328 -0
  14. package/jzify/hoist-vars.js +177 -0
  15. package/jzify/index.js +51 -0
  16. package/jzify/names.js +37 -0
  17. package/jzify/switch.js +106 -0
  18. package/jzify/transform.js +349 -0
  19. package/layout.js +184 -0
  20. package/module/array.js +377 -176
  21. package/module/collection.js +639 -145
  22. package/module/console.js +55 -43
  23. package/module/core.js +264 -153
  24. package/module/date.js +15 -3
  25. package/module/function.js +73 -6
  26. package/module/index.js +2 -1
  27. package/module/json.js +226 -61
  28. package/module/math.js +551 -187
  29. package/module/number.js +327 -60
  30. package/module/object.js +474 -184
  31. package/module/regex.js +255 -25
  32. package/module/schema.js +24 -6
  33. package/module/simd.js +117 -0
  34. package/module/string.js +621 -226
  35. package/module/symbol.js +1 -1
  36. package/module/timer.js +9 -14
  37. package/module/typedarray.js +459 -73
  38. package/package.json +58 -14
  39. package/src/abi/index.js +39 -23
  40. package/src/abi/string.js +38 -41
  41. package/src/ast.js +460 -0
  42. package/src/autoload.js +29 -24
  43. package/src/bridge.js +111 -0
  44. package/src/compile/analyze-scans.js +824 -0
  45. package/src/compile/analyze.js +1600 -0
  46. package/src/compile/emit-assign.js +411 -0
  47. package/src/compile/emit.js +3512 -0
  48. package/src/compile/flow-types.js +103 -0
  49. package/src/{compile.js → compile/index.js} +778 -128
  50. package/src/{infer.js → compile/infer.js} +62 -98
  51. package/src/compile/loop-divmod.js +155 -0
  52. package/src/{narrow.js → compile/narrow.js} +409 -106
  53. package/src/compile/peel-stencil.js +261 -0
  54. package/src/compile/plan/advise.js +370 -0
  55. package/src/compile/plan/common.js +150 -0
  56. package/src/compile/plan/index.js +122 -0
  57. package/src/compile/plan/inline.js +682 -0
  58. package/src/compile/plan/literals.js +1199 -0
  59. package/src/compile/plan/loops.js +472 -0
  60. package/src/compile/plan/scope.js +649 -0
  61. package/src/compile/program-facts.js +423 -0
  62. package/src/ctx.js +220 -64
  63. package/src/ir.js +589 -172
  64. package/src/kind-traits.js +132 -0
  65. package/src/kind.js +524 -0
  66. package/src/op-policy.js +57 -0
  67. package/src/{optimize.js → optimize/index.js} +1432 -473
  68. package/src/optimize/vectorize.js +4660 -0
  69. package/src/param-reps.js +65 -0
  70. package/src/parse.js +44 -0
  71. package/src/{prepare.js → prepare/index.js} +649 -205
  72. package/src/prepare/lift-iife.js +149 -0
  73. package/src/reps.js +116 -0
  74. package/src/resolve.js +12 -3
  75. package/src/static.js +208 -0
  76. package/src/type.js +651 -0
  77. package/src/{assemble.js → wat/assemble.js} +275 -55
  78. package/src/wat/optimize.js +3938 -0
  79. package/transform.js +21 -0
  80. package/wasi.js +47 -5
  81. package/src/analyze.js +0 -3818
  82. package/src/emit.js +0 -3040
  83. package/src/jzify.js +0 -1580
  84. package/src/plan.js +0 -2132
  85. package/src/vectorize.js +0 -1088
  86. /package/src/{codegen.js → wat/codegen.js} +0 -0
@@ -19,33 +19,70 @@
19
19
  * Each handler may touch multiple concerns, but helpers keep each concern self-contained.
20
20
  * Unhandled ops fall through to recursive prep() of their children.
21
21
  *
22
+ * # Forward seeding (the two compile/ imports — deliberate, not a layering leak)
23
+ * Prepare is the only pass that sees module-scope declarations in source order,
24
+ * so it seeds two compile-stage fact stores AS it walks (re-deriving them later
25
+ * would need a second whole-AST pass over information prepare already holds):
26
+ * - `recordGlobalRep` (compile/infer.js) — module-global value reps
27
+ * - `observeNodeFacts` (compile/program-facts.js) — per-node program facts
28
+ * The contract is write-only: prepare never READS compile-stage state, so the
29
+ * stage remains re-runnable and compile owns every read path.
30
+ *
22
31
  * @module prepare
23
32
  */
24
33
 
25
- import { parse } from 'subscript/feature/jessie'
26
- import { ctx, err, derive } from './ctx.js'
27
- import { T, STMT_OPS, VAL, extractParams, collectParamNames, classifyParam, observeNodeFacts, staticObjectProps, staticPropertyKey } from './analyze.js'
28
- import { recordGlobalRep } from './infer.js'
29
- import { isFuncRef } from './ir.js'
34
+ import { handlerArgs, refsName, ASSIGN_OPS, JZ_NULL, JZ_UNDEF, TYPEOF } from '../ast.js'
35
+ import { ctx, err, derive, emitArity, declGlobal } from '../ctx.js'
36
+ import { T } from '../ast.js'
37
+ import { extractParams, collectParamNames, classifyParam } from '../ast.js'
38
+ import { observeNodeFacts } from '../compile/program-facts.js'
39
+ import { staticObjectProps, staticPropertyKey } from '../static.js'
40
+ import { VAL } from '../reps.js'
41
+ import { STMT_OPS } from '../ast.js'
42
+ import { REJECT_IDENTS, REJECT_OPS, rejectHandlers } from '../op-policy.js'
43
+ import { recordGlobalRep } from '../compile/infer.js'
44
+ import { isFuncRef } from '../ir.js'
30
45
  import {
31
- CTORS, TIMER_NAMES,
32
- hasModule, includeModule,
46
+ CTORS, COLLECTION_CTORS, TIMER_NAMES,
47
+ hasModule, includeModule, includeMods,
33
48
  includeForArrayAccess, includeForArrayLiteral, includeForArrayPattern, includeForCallableValue,
34
49
  includeForGenericMethod, includeForKnownKeyIteration, includeForNamedCall, includeForNumericCoercion,
35
50
  includeForObjectLiteral, includeForObjectPattern, includeForOp, includeForProperty, includeForRuntimeCtor,
36
51
  includeForRuntimeKeyIteration, includeForStringOnly, includeForStringValue, includeForTimerRuntime,
37
- } from './autoload.js'
38
-
39
- let depth = 0 // arrow nesting depth (0=top-level, >0=inside function)
40
- let scopes = [] // block scope stack: [{names: Set, renames: Map}]
41
- let staticConstScopes = [] // lexical const facts: [{strings: Map, arrays: Map}]
42
- let assignedStaticGlobals = null
52
+ } from '../autoload.js'
53
+
54
+ // SIMD intrinsic namespaces pure namespaces backed by the `simd` module.
55
+ const SIMD_NS = new Set(['f32x4', 'i32x4', 'f64x2', 'v128'])
56
+
57
+ // Module-level prepare state. Six independent stacks/scalars that together form
58
+ // the prepare-pass working set. Lifecycle: reinitialized via `resetPrepState()`
59
+ // at the top of `prepare()` (line ~368) — any throw inside prepare is cleared
60
+ // on the next entry, so leak across compilations is impossible. Kept at module
61
+ // scope (rather than ctx.prepare.*) because 78 read sites would mean a single
62
+ // indirection on every scope query; the consolidated reset documents the set.
63
+ let depth // arrow nesting depth (0=top-level, >0=inside function)
64
+ let scopes // block scope stack: [{names: Set, renames: Map}]
65
+ let staticConstScopes // lexical const facts: [{strings: Map, arrays: Map}]
66
+ let assignedStaticGlobals
43
67
  // Per-arrow set of names already declared anywhere in the function body. Used
44
68
  // to force a rename when the same identifier is declared in two sibling blocks
45
69
  // (else-if arms, separate { ... } chunks): without renaming, both decls lower
46
70
  // to the same WASM local, but downstream optimizations (directClosures) gate
47
71
  // on per-decl `isReassigned`, not per-WASM-local — they'd read a stale binding.
48
- let funcLocalNames = []
72
+ let funcLocalNames
73
+ // Per-arrow set of local names bound to a function literal (`let g = () => …`).
74
+ // Lets the `.`-handler tell a function receiver — where `.caller`/`.callee` are
75
+ // prohibited introspection — from a data object that merely has such a field.
76
+ let funcValueNames
77
+
78
+ const resetPrepState = () => {
79
+ depth = 0
80
+ scopes = []
81
+ staticConstScopes = []
82
+ assignedStaticGlobals = new Set()
83
+ funcLocalNames = [new Set()]
84
+ funcValueNames = [new Set()]
85
+ }
49
86
 
50
87
  // ES spec: identifier with \uHHHH or \u{...} escape is equivalent to the decoded
51
88
  // form. subscript preserves raw spelling in the AST; normalize once before prep.
@@ -54,6 +91,55 @@ const decodeIdent = s => s.includes('\\u')
54
91
  ? s.replace(IDESC, (_, b, p) => String.fromCodePoint(parseInt(b || p, 16)))
55
92
  : s
56
93
 
94
+ // A for-loop bound `arr.length` may be snapshotted into a pre-loop local only when
95
+ // nothing in the loop can change it. Two ways it can change: a write to the receiver
96
+ // (`arr = …`, `arr.length = …`, `arr[k] = …`) or a call — push/pop/splice mutate
97
+ // directly, and any call can reach `arr` through an alias the compiler can't track
98
+ // locally (compilePendingClosures grows ctx.closure.bodies this way). Both predicates
99
+ // recurse the whole node; nested arrow *definitions* are harmless until invoked, and
100
+ // an invocation is itself a call node, so `callFree` already covers escaped mutators.
101
+ const callFree = node => {
102
+ if (!Array.isArray(node)) return true
103
+ if (node[0] === '()' || node[0] === 'new') return false
104
+ for (let i = 1; i < node.length; i++) if (!callFree(node[i])) return false
105
+ return true
106
+ }
107
+ // Calls that provably can't resize ANY receiver: read-only builtin methods
108
+ // (no mutators, no callback-takers — a callback could close over the receiver
109
+ // and push) and pure namespaces. Everything else (user fns, push/splice,
110
+ // map/forEach) may reach the bound receiver through an alias — disqualifies
111
+ // the length snapshot. A user object shadowing one of these names with a
112
+ // mutating closure is a documented divergence (same class as for-of's).
113
+ const _BOUND_PURE_NS = new Set(['Math', 'math', 'Number', 'String', 'JSON', 'console', 'Date', 'performance'])
114
+ const _BOUND_RO_METHODS = new Set([
115
+ 'charCodeAt', 'charAt', 'codePointAt', 'at', 'indexOf', 'lastIndexOf', 'includes',
116
+ 'startsWith', 'endsWith', 'slice', 'substring', 'trim', 'toUpperCase', 'toLowerCase',
117
+ 'join', 'concat', 'toString', 'get', 'has', 'now',
118
+ ])
119
+ const boundSafeCalls = node => {
120
+ if (!Array.isArray(node)) return true
121
+ if (node[0] === 'new') return false
122
+ if (node[0] === '()' || node[0] === '?.()') {
123
+ const callee = node[1]
124
+ const safe = Array.isArray(callee) && (callee[0] === '.' || callee[0] === '?.') &&
125
+ (_BOUND_RO_METHODS.has(callee[2]) ||
126
+ (typeof callee[1] === 'string' && _BOUND_PURE_NS.has(callee[1])))
127
+ if (!safe) return false
128
+ }
129
+ for (let i = 1; i < node.length; i++) if (!boundSafeCalls(node[i])) return false
130
+ return true
131
+ }
132
+ const writesReceiver = (node, recv) => {
133
+ if (!Array.isArray(node)) return false
134
+ const op = node[0]
135
+ if ((ASSIGN_OPS.has(op) || op === '++' || op === '--') &&
136
+ (node[1] === recv ||
137
+ (Array.isArray(node[1]) && (node[1][0] === '[]' || node[1][0] === '.') && node[1][1] === recv)))
138
+ return true
139
+ for (let i = 1; i < node.length; i++) if (writesReceiver(node[i], recv)) return true
140
+ return false
141
+ }
142
+
57
143
  const normalizeIdents = node => {
58
144
  if (!Array.isArray(node)) return
59
145
  // Literal-value wrapper [null, X] / [undefined, X]: X is a value, not an identifier
@@ -67,14 +153,26 @@ const normalizeIdents = node => {
67
153
 
68
154
  const hostReturnValType = spec => {
69
155
  if (!spec || typeof spec === 'function') return null
156
+ // Return type is the canonical string name ('number'/'string'/'bigint'/'f64').
157
+ // (Earlier this also accepted the constructor identity `ret === String` etc.,
158
+ // but that references host-only globals with no first-class value in jz — it
159
+ // broke self-hosting and was never used. String names are the portable form.)
70
160
  const ret = spec.returns ?? spec.return ?? spec.result
71
- if (ret === 'number' || ret === 'f64' || ret === Number) return VAL.NUMBER
72
- if (ret === 'string' || ret === String) return VAL.STRING
73
- if (ret === 'bigint' || ret === BigInt) return VAL.BIGINT
161
+ if (ret === 'number' || ret === 'f64') return VAL.NUMBER
162
+ if (ret === 'string') return VAL.STRING
163
+ if (ret === 'bigint') return VAL.BIGINT
74
164
  return null
75
165
  }
76
166
 
77
167
  const addHostImport = (mod, name, alias, spec) => {
168
+ // A numeric host constant (e.g. `Math.PI` via `{ imports: { math: Math } }`) has no callable
169
+ // ABI — record it so references fold to an f64 literal (see prep's identifier resolution) instead
170
+ // of emitting a 0-arg func import that can't be read as a value ("'PI' is not in scope").
171
+ if (typeof spec === 'number') {
172
+ if (!ctx.scope.hostConsts) ctx.scope.hostConsts = {}
173
+ ctx.scope.hostConsts[alias] = spec
174
+ return
175
+ }
78
176
  const nParams = typeof spec === 'function' ? spec.length : (spec?.params || 0)
79
177
  // User-supplied imports carry NaN-boxed values via i64 (not f64) so V8 cannot
80
178
  // canonicalize the NaN payload across the wasm↔JS function boundary —
@@ -99,7 +197,6 @@ const stripBoolNot = c => {
99
197
  return c
100
198
  }
101
199
  const stringValue = node => Array.isArray(node) && node[0] == null && typeof node[1] === 'string' ? node[1] : null
102
- const flatArgs = args => args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',' ? args[0].slice(1) : args
103
200
  const MUTATING_ARRAY_METHODS = new Set(['copyWithin', 'fill', 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'])
104
201
 
105
202
  function staticStringArrayValues(expr) {
@@ -224,6 +321,30 @@ function lookupStaticStringArray(name) {
224
321
  return ctx.scope.shapeStrArrays?.get(resolved) ?? null
225
322
  }
226
323
 
324
+ /** Evaluate a constant numeric expression (number literals + basic arithmetic) for
325
+ * compile-time string/template folding. Returns null when it isn't a pure-number
326
+ * constant — string `+` and dynamic parts fall through to the caller's runtime path. */
327
+ function constNum(node) {
328
+ if (Array.isArray(node) && node[0] == null && typeof node[1] === 'number') return node[1]
329
+ if (!Array.isArray(node)) return null
330
+ const [op, a, b] = node
331
+ if ((op === 'u-' || op === '-' || op === '+') && b === undefined) {
332
+ const x = constNum(a)
333
+ return x == null ? null : op === 'u-' || op === '-' ? -x : +x
334
+ }
335
+ const x = constNum(a), y = constNum(b)
336
+ if (x == null || y == null) return null
337
+ switch (op) {
338
+ case '+': return x + y
339
+ case '-': return x - y
340
+ case '*': return x * y
341
+ case '/': return y === 0 ? null : x / y
342
+ case '%': return y === 0 ? null : x % y
343
+ case '**': return x ** y
344
+ }
345
+ return null
346
+ }
347
+
227
348
  function staticStringExpr(node) {
228
349
  const lit = stringValue(node)
229
350
  if (lit != null) return lit
@@ -234,12 +355,21 @@ function staticStringExpr(node) {
234
355
  if (op === '+') {
235
356
  const a = staticStringExpr(args[0])
236
357
  const b = staticStringExpr(args[1])
237
- return a != null && b != null ? a + b : null
358
+ // Accumulate from a fresh empty string (`'' + a + b`) rather than concatenating two
359
+ // source-derived substrings directly. Under self-host the latter can yield a string
360
+ // backed by transient parse-time storage that's invalid by the time emit['//'] reads
361
+ // it for regex compilation (OOB); forcing a fresh allocation, as the template-literal
362
+ // path already does, keeps it stable. Identical value in both legs.
363
+ return a != null && b != null ? '' + a + b : null
238
364
  }
239
365
  if (op === '`') {
240
366
  let out = ''
241
367
  for (const part of args) {
242
- const s = staticStringExpr(part)
368
+ let s = staticStringExpr(part)
369
+ // A numeric interpolation (`${123}`, `${1+2}`) is a constant in string context —
370
+ // ToString it so a fully-static template folds to one literal instead of a runtime
371
+ // concat. (Only the template case stringifies numbers; `+` stays polymorphic.)
372
+ if (s == null) { const n = constNum(part); if (n != null) s = String(n) }
243
373
  if (s == null) return null
244
374
  out += s
245
375
  }
@@ -270,15 +400,21 @@ function importMetaUrl() {
270
400
 
271
401
  function resolveImportMeta(spec) {
272
402
  const base = importMetaUrl()
273
- try { return new URL(spec, base).href }
403
+ // URL resolution is a host capability (WHATWG URL parsing), injected via
404
+ // ctx.transform.resolveUrl rather than referencing the `URL` global — the same
405
+ // inversion as ctx.transform.parse. Keeps the self-host kernel (which bundles
406
+ // its module graph and never resolves import.meta at runtime) free of `URL`.
407
+ if (!ctx.transform.resolveUrl) err('import.meta resolution requires ctx.transform.resolveUrl (injected by the jz pipeline)')
408
+ try { return ctx.transform.resolveUrl(spec, base) }
274
409
  catch { err(`Cannot resolve import.meta specifier '${spec}' from '${base}'`) }
275
410
  }
276
411
 
277
412
  function recordModuleInitFacts(root) {
278
413
  const facts = ctx.module.initFacts ||= {
279
- dynVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
414
+ dynVars: new Set(), dynWriteVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
280
415
  hasFuncValue: false, timerNames: new Set(),
281
416
  maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false,
417
+ writtenProps: new Set(),
282
418
  }
283
419
  const visitFuncValue = (node) => {
284
420
  if (facts.hasFuncValue || !Array.isArray(node)) return
@@ -323,13 +459,39 @@ function recordModuleInitFacts(root) {
323
459
  * @param {ASTNode} node - Raw AST from parser
324
460
  * @returns {ASTNode} Normalized AST
325
461
  */
462
+ // ES2020 §13.13: the nullish-coalescing `??` cannot be combined with `||` or `&&`
463
+ // without parentheses — V8 raises a SyntaxError. subscript/jessie doesn't enforce
464
+ // it, so jz would otherwise silently accept (and pick its own parse for) the mix.
465
+ // Run on the RAW input AST: a parenthesized operand parses as `['()', …]`, so a
466
+ // bare `??`/`||`/`&&` child is exactly the illegal unparenthesized form — and at
467
+ // this stage no compiler-synthesized `??` (e.g. destructuring defaults) exists yet,
468
+ // so `let [a = b || c] = arr` can't false-positive.
469
+ function validateCoalesceMixing(n) {
470
+ if (!Array.isArray(n)) return
471
+ const op = n[0]
472
+ if (op === '||' || op === '&&') {
473
+ for (let i = 1; i < n.length; i++) if (Array.isArray(n[i]) && n[i][0] === '??')
474
+ err(`'??' cannot be mixed with '${op}' without parentheses (ES2020) — wrap one side, e.g. (a ?? b) ${op} c`)
475
+ } else if (op === '??') {
476
+ for (let i = 1; i < n.length; i++) if (Array.isArray(n[i]) && (n[i][0] === '||' || n[i][0] === '&&'))
477
+ err(`'??' cannot be mixed with '||' / '&&' without parentheses (ES2020) — wrap one side, e.g. a ?? (b || c)`)
478
+ }
479
+ for (let i = 1; i < n.length; i++) validateCoalesceMixing(n[i])
480
+ }
481
+
326
482
  export default function prepare(node) {
327
- depth = 0
328
- scopes = []
329
- staticConstScopes = []
330
- assignedStaticGlobals = new Set()
331
- funcLocalNames = [new Set()]
483
+ resetPrepState()
484
+ // Inject the module-include primitive so stdlib modules can pull dependency
485
+ // modules (e.g. object → collection) without importing autoload.js — that
486
+ // import would cycle (autoload imports every module via module/index.js).
487
+ ctx.module.include = includeModule
332
488
  includeModule('core')
489
+ // Empty or whitespace-only source parses to a bare '' — an empty program, not an
490
+ // identifier reference. Normalize to an empty statement so it compiles to a bare
491
+ // `(module)` instead of a `(local.get $)` against a zero-length name. (A non-empty
492
+ // bare identifier like `foo` parses to `'foo'` and stays a real reference.)
493
+ if (node === '') node = [';']
494
+ validateCoalesceMixing(node) // ES2020: reject unparenthesized `??` mixed with `||`/`&&`
333
495
  normalizeIdents(node)
334
496
  fuseSparseMapReads(node) // AST-level fusion; needs pre-resolution shape — defined at end of file
335
497
  seedStaticGlobalAssignments(node)
@@ -426,9 +588,9 @@ export default function prepare(node) {
426
588
  return ast
427
589
  }
428
590
 
429
- // Named constants → numeric literals
430
- export const JZ_NULL = Symbol('null')
431
- const CONSTANTS = { 'true': true, 'false': false, 'null': JZ_NULL, 'undefined': JZ_NULL }
591
+ // Named constants → numeric literals. The JZ_NULL/JZ_UNDEF atom sentinels live
592
+ // in ast.js shared with emit without crossing the prepare↔compile boundary.
593
+ const CONSTANTS = { 'true': true, 'false': false, 'null': JZ_NULL, 'undefined': JZ_UNDEF }
432
594
  // NaN/Infinity stay as special f64 values in emit()
433
595
  const F64_CONSTANTS = { 'NaN': NaN, 'Infinity': Infinity }
434
596
 
@@ -472,7 +634,43 @@ function deleteStaticGlobal(name) {
472
634
  ctx.scope.shapeStrArrays?.delete(name)
473
635
  }
474
636
 
637
+ // Schema id when prhs is a bare object literal with static keys, else null.
638
+ function objLiteralSid(prhs) {
639
+ if (!Array.isArray(prhs) || prhs[0] !== '{}') return null
640
+ const props = staticObjectProps(prhs.slice(1))
641
+ return props ? ctx.schema.register(props.names) : null
642
+ }
643
+
644
+ // Shape-consensus accounting for every `name = …` assignment. `sid` is the
645
+ // RHS literal's schema id (null for any non-literal source). A name's schema
646
+ // binds only while ALL its assignments agree on that one literal shape: the
647
+ // first literal binds; any disagreeing assignment — non-literal RHS or a
648
+ // different-shape literal — unbinds and poisons. Poisoned names never rebind,
649
+ // so compile-time fixed-slot reads can't be aimed at one shape while the
650
+ // variable holds another (the misread class: `.x` returning a foreign
651
+ // object's slot-0 value). Compile consumes the END state — order-insensitive.
652
+ function bindAssignSchema(name, sid) {
653
+ const had = ctx.schema.vars.get(name)
654
+ if (had != null) {
655
+ if (had !== sid) { ctx.schema.vars.delete(name); ctx.schema.poisoned?.add(name) }
656
+ } else if (sid != null) {
657
+ if (!ctx.schema.poisoned?.has(name)) ctx.schema.vars.set(name, sid)
658
+ } else ctx.schema.poisoned?.add(name)
659
+ }
660
+
475
661
  const hasFunc = name => ctx.func.names.has(name)
662
+ // A builtin name (`Map`, `Array`, `Math`, …) is shadowed when the user bound it
663
+ // as a local (let/const/param, via `isDeclared`), a top-level function (via
664
+ // `hasFunc`), or a top-level let/const global (via `userGlobals`). A shadowed
665
+ // name must resolve to the user binding, so the constructor / named-call
666
+ // fast-paths bail and fall through to `resolveCallee`, which already routes a
667
+ // declared name to its local value. Mirrors the guard in
668
+ // `foldNamespaceIntrospection`.
669
+ const shadowsBuiltin = name => typeof name === 'string' &&
670
+ ((scopes.length && isDeclared(name)) || hasFunc(name) || ctx.scope.userGlobals?.has?.(name))
671
+ // A local bound to a function literal in any active arrow scope (the nested-
672
+ // closure counterpart to `hasFunc`, which only knows depth-0 lifted functions).
673
+ const isFuncValueLocal = name => typeof name === 'string' && funcValueNames.some(s => s.has(name))
476
674
 
477
675
  const renameFunc = (func, nextName) => {
478
676
  ctx.func.names.delete(func.name)
@@ -480,8 +678,8 @@ const renameFunc = (func, nextName) => {
480
678
  ctx.func.names.add(nextName)
481
679
  }
482
680
 
483
- /** Map JS typeof strings to jz type checks. Codes < 0 trigger specialized emitTypeofCmp paths. */
484
- const TYPEOF_MAP = { 'number': -1, 'string': -2, 'undefined': -3, 'boolean': -4, 'object': -5, 'function': -6, 'bigint': -7 }
681
+ // `typeof`-string code table lives in ast.js (TYPEOF) shared with
682
+ // emitTypeofCmp and flow-types so the codes have one home.
485
683
  // Spec §13.5.3: `typeof undeclared_x` returns 'undefined' without throwing.
486
684
  // True iff `name` is a bare identifier with no resolution path. Mirrors the
487
685
  // resolution chain inside `prep()` so we don't speculate emit-time failures.
@@ -489,7 +687,7 @@ function isUnresolvableBareIdent(name) {
489
687
  if (typeof name !== 'string') return false
490
688
  if (name in CONSTANTS || name in F64_CONSTANTS) return false
491
689
  if (name === 'Boolean' || name === 'Number') return false
492
- if (PROHIBITED[name]) return false
690
+ if (REJECT_IDENTS[name]) return false
493
691
  if (scopes.length && isDeclared(name)) return false
494
692
  if (ctx.scope.chain[name]) return false
495
693
  if (GLOBALS[name]) return false
@@ -508,9 +706,9 @@ function staticTypeofString(x) {
508
706
  // Spec §13.5.3: unresolvable bare ref → 'undefined'.
509
707
  if (isUnresolvableBareIdent(x)) return 'undefined'
510
708
  // Bare callable global: parseInt, parseFloat, isNaN, isFinite, Error, BigInt, etc.
511
- if (typeof x === 'string' && !ctx.func?.locals?.has(x) && GLOBALS[x] && ctx.core.emit?.[x]?.length > 0) return 'function'
709
+ if (typeof x === 'string' && !ctx.func?.locals?.has(x) && GLOBALS[x] && emitArity(ctx.core.emit?.[x]) > 0) return 'function'
512
710
  const px = prep(x)
513
- if (typeof px === 'string' && px.includes('.') && ctx.core.emit?.[px]?.length > 0) return 'function'
711
+ if (typeof px === 'string' && px.includes('.') && emitArity(ctx.core.emit?.[px]) > 0) return 'function'
514
712
  return null
515
713
  }
516
714
  // Builtin-namespace constructors expose `prototype`/`length`/`name` as own
@@ -534,14 +732,14 @@ function resolveTypeof(node) {
534
732
  if (Array.isArray(a) && a[0] === 'typeof' && Array.isArray(b) && b[0] == null && typeof b[1] === 'string') {
535
733
  const known = staticTypeofString(a[1])
536
734
  if (known != null) return [, eqLike ? known === b[1] : known !== b[1]]
537
- const code = TYPEOF_MAP[b[1]]
735
+ const code = TYPEOF[b[1]]
538
736
  if (code != null) return [op, ['typeof', a[1]], [, code]]
539
737
  }
540
738
  // 'string' == typeof x
541
739
  if (Array.isArray(b) && b[0] === 'typeof' && Array.isArray(a) && a[0] == null && typeof a[1] === 'string') {
542
740
  const known = staticTypeofString(b[1])
543
741
  if (known != null) return [, eqLike ? known === a[1] : known !== a[1]]
544
- const code = TYPEOF_MAP[a[1]]
742
+ const code = TYPEOF[a[1]]
545
743
  if (code != null) return [op, ['typeof', b[1]], [, code]]
546
744
  }
547
745
  return node
@@ -602,11 +800,19 @@ function prep(node) {
602
800
  if (typeof node === 'string') {
603
801
  if (node in CONSTANTS) return [, CONSTANTS[node]]
604
802
  if (node in F64_CONSTANTS) return [, F64_CONSTANTS[node]]
605
- if (PROHIBITED[node]) err(PROHIBITED[node])
803
+ if (REJECT_IDENTS[node]) err(REJECT_IDENTS[node])
606
804
  // Boolean/Number as value → identity arrow (for .filter(Boolean), .map(Number) etc.)
607
805
  if (node === 'Boolean' || node === 'Number') { includeForCallableValue(); return ['=>', 'x', 'x'] }
608
806
  // Block locals shadow module imports/globals, even when the local keeps the same name.
609
807
  if (scopes.length && isDeclared(node)) return resolveScope(node)
808
+ // A user top-level binding (`let Math = …`) shadows a same-named builtin
809
+ // namespace seeded into the scope chain (`Math → math`). Resolve to the
810
+ // user global, not the builtin. (Mangled globals drop their original name
811
+ // from userGlobals, so this fires only for un-renamed user bindings.)
812
+ if (ctx.scope.userGlobals?.has?.(node)) return node
813
+ // Host numeric constant (`Math.PI` etc.) → fold to its f64 literal. Placed after the
814
+ // local/user-global checks above so a same-named binding still shadows it.
815
+ if (ctx.scope.hostConsts && node in ctx.scope.hostConsts) return [, ctx.scope.hostConsts[node]]
610
816
  const resolved = ctx.scope.chain[node]
611
817
  if (resolved?.includes('.')) return resolved
612
818
  // Cross-module import: mangled name (e.g. __util_js$clone)
@@ -619,6 +825,10 @@ function prep(node) {
619
825
 
620
826
  const [op, ...args] = node
621
827
  if (op === 'void' && ctx.transform.strict) err('strict mode: `void` is prohibited. It diverges from JS by evaluating to 0.')
828
+ // jz's `==`/`!=` already never coerce (identical to `===`/`!==`), so default mode accepts them.
829
+ // strict enforces the canonical subset, where `===`/`!==` are the one spelling — reject the loose form.
830
+ if ((op === '==' || op === '!=') && ctx.transform.strict)
831
+ err(`strict mode: \`${op}\` is prohibited — use \`${op}=\`. (jz's \`${op}\` doesn't coerce, but the canonical subset is \`===\`/\`!==\` only.)`)
622
832
  if (op == null) {
623
833
  if (typeof args[0] === 'string') {
624
834
  includeForStringValue()
@@ -630,19 +840,9 @@ function prep(node) {
630
840
  return handler ? handler(...args) : [op, ...args.map(prep)]
631
841
  }
632
842
 
633
- // Strict-jz prohibitions. `class` and `arguments` are *also* listed here but
634
- // only reach this point when jzify is off — jzify lowers `class` to a factory
635
- // arrow and rewrites `arguments` to a rest param before prepare runs. The
636
- // remainder (`with`, `this`, `super`, `yield`, `eval`) have no safe lowering
637
- // and stay errors in both modes.
638
- const PROHIBITED = { 'with': '`with` not supported', 'class': '`class` not supported', 'yield': '`yield` not supported',
639
- 'this': '`this` not supported: use explicit parameter',
640
- 'super': '`super` not supported: no class inheritance',
641
- 'arguments': '`arguments` not supported: use rest params',
642
- 'eval': '`eval` not supported'
643
- }
843
+ // Identifier prohibitions: op-policy.js REJECT_IDENTS (prep string nodes).
644
844
 
645
- // Predefined globals seeded into scope.chain at ctx.reset(). Value is the scope alias
845
+ // Predefined globals seeded into scope.chain at ctx.reset().
646
846
  // used in ctx.core.emit[]. Dotted lookups (Math.sin) go through the '.' handler which
647
847
  // resolves via scope.chain → module 'math' → registers 'math.sin' emitter.
648
848
  // Not actually "implicit imports" — these are ambient globals that exist in every jz/JS
@@ -683,6 +883,13 @@ export const GLOBALS = {
683
883
  const patternItems = (node) => node?.[0] === ',' ? node.slice(1) : [node]
684
884
  const isDestructPattern = (node) => Array.isArray(node) && (node[0] === '[]' || node[0] === '{}')
685
885
 
886
+ // Element count of a prepared inline array literal `['[', e0, e1, …]` with no
887
+ // spread (spread → dynamic length). Returns null when not such a literal, so
888
+ // destructuring a non-literal source keeps its runtime element reads.
889
+ const inlineArrayLen = (e) =>
890
+ Array.isArray(e) && e[0] === '[' && !e.slice(1).some(x => Array.isArray(x) && x[0] === '...')
891
+ ? e.length - 1 : null
892
+
686
893
  const simpleArrayPatternItems = (pattern) => {
687
894
  if (!Array.isArray(pattern) || pattern[0] !== '[]' || pattern.length !== 2) return null
688
895
  const items = patternItems(pattern[1])
@@ -714,7 +921,7 @@ function scalarArrayDestruct(pattern, rhs) {
714
921
  function declareGlobal(name, user = true) {
715
922
  if (depth !== 0 || typeof name !== 'string') return name
716
923
  if (ctx.scope.globals.has(name)) err(`'${name}' conflicts with a compiler internal — choose a different name`)
717
- ctx.scope.globals.set(name, `(global $${name} (mut f64) (f64.const 0))`)
924
+ declGlobal(name, 'f64')
718
925
  if (user) ctx.scope.userGlobals.add(name)
719
926
  return name
720
927
  }
@@ -749,7 +956,7 @@ function pushPatternAssign(target, valueExpr, out, decls = null) {
749
956
  out.push(['=', target, valueExpr])
750
957
  }
751
958
 
752
- function expandDestruct(pattern, source, out, decls = null) {
959
+ function expandDestruct(pattern, source, out, decls = null, srcLen = null) {
753
960
  if (!isDestructPattern(pattern)) return
754
961
 
755
962
  if (pattern[0] === '[]') {
@@ -764,6 +971,15 @@ function expandDestruct(pattern, source, out, decls = null) {
764
971
  continue
765
972
  }
766
973
 
974
+ // Source is a known-length inline literal and this index is past its end →
975
+ // the element is statically `undefined` (so any `= default` applies). Folding
976
+ // it here skips a provably out-of-range read — which both avoids the runtime
977
+ // access and dodges an optimizer miscompile of the destructuring-temp shape.
978
+ if (srcLen != null && j >= srcLen) {
979
+ pushPatternAssign(item, [, JZ_UNDEF], out, decls)
980
+ continue
981
+ }
982
+
767
983
  pushPatternAssign(item, ['[]', source, [, j]], out, decls)
768
984
  }
769
985
  return
@@ -844,7 +1060,7 @@ function prepDecl(op, ...inits) {
844
1060
  ctx.scope.chain[i] = declName
845
1061
  }
846
1062
  if (ctx.scope.globals.has(declName)) err(`'${declName}' conflicts with a compiler internal — choose a different name`)
847
- ctx.scope.globals.set(declName, `(global $${declName} (mut f64) (f64.const 0))`)
1063
+ declGlobal(declName, 'f64')
848
1064
  ctx.scope.userGlobals.add(declName)
849
1065
  } else if (typeof declName === 'string') {
850
1066
  // Bare hoisted decl inside a function (var X jzified to `let X` at top
@@ -859,6 +1075,23 @@ function prepDecl(op, ...inits) {
859
1075
  continue
860
1076
  }
861
1077
  const [, name, init] = i
1078
+ // `const alias = fn` whose RHS is a bare identifier naming a known function
1079
+ // is a compile-time function alias — the ES `export { fn as alias }` written
1080
+ // in declaration form (a recurring kernel idiom: paramList = extractParams,
1081
+ // toBoolFromEmitted = truthyIR …). Resolve `alias` straight to the function
1082
+ // so calls compile to a direct call and the export table re-exports the same
1083
+ // mangled func. Otherwise it would box a closure into a module global that a
1084
+ // cross-module callee resolves to the bare, unmangled name → "not in scope".
1085
+ // Module scope + `const` only: depth>0 aliases already work as closure values,
1086
+ // and a reassignable `let` is a genuine value binding, not an alias.
1087
+ if (op === 'const' && depth === 0 && typeof name === 'string' && typeof init === 'string') {
1088
+ const fn = hasFunc(init) ? init : (hasFunc(ctx.scope.chain[init]) ? ctx.scope.chain[init] : null)
1089
+ if (fn) {
1090
+ ctx.scope.chain[name] = fn
1091
+ if (name in ctx.func.exports) ctx.func.exports[name] = fn
1092
+ continue
1093
+ }
1094
+ }
862
1095
  const staticStr = op === 'const' ? staticStringExpr(init) : null
863
1096
  const staticArr = op === 'const' ? staticStringArrayValues(init) : null
864
1097
  const normed = prep(init)
@@ -894,7 +1127,7 @@ function prepDecl(op, ...inits) {
894
1127
  const p = normed.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
895
1128
  if (p.length) ctx.schema.vars.set(tmp, ctx.schema.register(p))
896
1129
  }
897
- expandDestruct(name, tmp, rest)
1130
+ expandDestruct(name, tmp, rest, null, inlineArrayLen(normed))
898
1131
  continue
899
1132
  }
900
1133
 
@@ -912,6 +1145,10 @@ function prepDecl(op, ...inits) {
912
1145
  scopes[scopes.length - 1].set(name, name)
913
1146
  }
914
1147
  if (typeof declName === 'string' && fnNames) fnNames.add(declName)
1148
+ // A nested arrow stays a closure value (defFunc only lifts depth-0). Record
1149
+ // the binding so `.caller`/`.callee` on it reads as prohibited introspection.
1150
+ if (typeof declName === 'string' && Array.isArray(normed) && normed[0] === '=>')
1151
+ funcValueNames[funcValueNames.length - 1]?.add(declName)
915
1152
  if (op === 'const') bindStaticConst(declName, staticStr, staticArr)
916
1153
  // Track const for reassignment checks — only module-scope consts (depth 0)
917
1154
  if (typeof declName === 'string' && depth === 0) {
@@ -942,20 +1179,29 @@ function prepDecl(op, ...inits) {
942
1179
  // Track object schemas (after prefix so schema is keyed to final name)
943
1180
  if (typeof declName === 'string' && Array.isArray(normed) && normed[0] === '{}' && normed.length > 1) {
944
1181
  const props = []
1182
+ const addProp = n => { if (!props.includes(n)) props.push(n) }
1183
+ let allKnown = true
945
1184
  for (const p of normed.slice(1)) {
946
- if (Array.isArray(p) && p[0] === ':') props.push(p[1])
1185
+ // Dedupe every key (explicit AND spread-sourced) so a `k: v` that overrides
1186
+ // a spread-provided key doesn't push a duplicate — that would shift the
1187
+ // indices of later keys past emitObjectSpread's deduped slot assignment
1188
+ // (its `addName` dedupes both), making `decl.laterKey` read the wrong slot.
1189
+ if (Array.isArray(p) && p[0] === ':') addProp(p[1])
947
1190
  else if (Array.isArray(p) && p[0] === '...') {
948
- // Merge spread source schema into this object's schema
949
1191
  const srcSchema = typeof p[1] === 'string' && ctx.schema.resolve(p[1])
950
- if (srcSchema) for (const n of srcSchema) { if (!props.includes(n)) props.push(n) }
1192
+ if (srcSchema) for (const n of srcSchema) addProp(n)
1193
+ else allKnown = false
951
1194
  }
952
1195
  }
953
- if (props.length && ctx.schema.register) ctx.schema.vars.set(declName, ctx.schema.register(props))
1196
+ // An unknown spread source makes the value a runtime HASH (see
1197
+ // emitObjectSpread). Binding a static schema would compile `decl.prop`
1198
+ // to a fixed slot load that misreads the hash, so leave reads dynamic.
1199
+ if (allKnown && props.length && ctx.schema.register) ctx.schema.vars.set(declName, ctx.schema.register(props))
954
1200
  }
955
1201
  // Module-scope variable → WASM global (mark as user-declared)
956
1202
  if (depth === 0 && typeof declName === 'string') {
957
1203
  if (ctx.scope.globals.has(declName)) err(`'${declName}' conflicts with a compiler internal — choose a different name`)
958
- ctx.scope.globals.set(declName, `(global $${declName} (mut f64) (f64.const 0))`)
1204
+ declGlobal(declName, 'f64')
959
1205
  ctx.scope.userGlobals.add(declName)
960
1206
  }
961
1207
  rest.push(['=', declName, normed])
@@ -972,7 +1218,7 @@ function prepDecl(op, ...inits) {
972
1218
  // `import.meta.resolve("spec")` → the resolved URL as a static string.
973
1219
  function foldImportMetaResolve(callee, args) {
974
1220
  if (!isImportMetaProp(callee, 'resolve')) return undefined
975
- const callArgs = flatArgs(args).filter(a => a != null)
1221
+ const callArgs = handlerArgs(args)
976
1222
  if (callArgs.length !== 1) err('`import.meta.resolve` requires one string literal argument')
977
1223
  const spec = stringValue(callArgs[0])
978
1224
  if (spec == null) err('`import.meta.resolve` supports only string literal arguments')
@@ -986,8 +1232,11 @@ function foldImportMetaResolve(callee, args) {
986
1232
  // Returns the replacement IR, or `undefined` for an ordinary call.
987
1233
  function dispatchConstructorCall(callee, args) {
988
1234
  if (typeof callee !== 'string') return undefined
1235
+ // A user binding named like a constructor (`let Map = …`, `let Array = …`)
1236
+ // shadows the builtin — don't lower `Map(x)` to `new.Map`.
1237
+ if (shadowsBuiltin(callee)) return undefined
989
1238
  if (callee === 'Array') {
990
- const callArgs = flatArgs(args).filter(a => a != null)
1239
+ const callArgs = handlerArgs(args)
991
1240
  if (callArgs.length === 1) return handlers['new'](['()', callee, callArgs[0]])
992
1241
  }
993
1242
  if (CTORS.includes(callee)) return handlers['new'](['()', callee, ...args])
@@ -1005,7 +1254,7 @@ function foldNamespaceIntrospection(callee, args) {
1005
1254
  if (!Array.isArray(callee) || callee[0] !== '.') return undefined
1006
1255
  const [, obj, prop] = callee
1007
1256
  if (obj === 'Array' && prop === 'isArray') {
1008
- const cargs = flatArgs(args).filter(a => a != null)
1257
+ const cargs = handlerArgs(args)
1009
1258
  const a0 = cargs.length === 1 ? cargs[0] : null
1010
1259
  if (typeof a0 === 'string' && GLOBALS[a0] && !(scopes.length && isDeclared(a0)) && !hasFunc(a0))
1011
1260
  return [, 0]
@@ -1013,7 +1262,7 @@ function foldNamespaceIntrospection(callee, args) {
1013
1262
  if (prop === 'hasOwnProperty' && typeof obj === 'string' && !(scopes.length && isDeclared(obj))) {
1014
1263
  const mod = ctx.scope.chain[obj]
1015
1264
  if (mod && !mod.includes('.') && hasModule(mod)) {
1016
- const cargs = flatArgs(args).filter(a => a != null)
1265
+ const cargs = handlerArgs(args)
1017
1266
  const member = cargs.length === 1 ? stringValue(cargs[0]) : null
1018
1267
  // Include the module so its emit keys (the namespace's member set) are
1019
1268
  // registered; unreferenced emitters/data dead-strip in compile.
@@ -1027,6 +1276,11 @@ function foldNamespaceIntrospection(callee, args) {
1027
1276
  // way: a bare identifier through the scope chain, an `obj.prop` member call
1028
1277
  // through host imports / named-call / generic-method / namespace tables, and
1029
1278
  // any other expression through `prep` (a callable runtime value).
1279
+ // Compiler-internal synthetic callees: emit-handled intrinsics, never user
1280
+ // function values — so a bare reference must not pull in the callable-value
1281
+ // (function table / closure) machinery.
1282
+ const INTRINSIC_CALLEES = new Set(['__iter_arr', '__keys_ro'])
1283
+
1030
1284
  function resolveCallee(callee, args) {
1031
1285
  if (typeof callee === 'string') {
1032
1286
  const local = scopes.length && isDeclared(callee)
@@ -1038,12 +1292,23 @@ function resolveCallee(callee, args) {
1038
1292
  if (hasModule(resolved) && !ctx.module.imports.some(i => i[3]?.[1] === `$${resolved}`)) includeModule(resolved)
1039
1293
  return callee
1040
1294
  }
1041
- if (depth > 0 && !resolved && !ctx.func.exports[callee] && !ctx.module.imports.some(i => i[3]?.[1] === `$${callee}`))
1295
+ if (depth > 0 && !resolved && !INTRINSIC_CALLEES.has(callee) && !ctx.func.exports[callee] && !ctx.module.imports.some(i => i[3]?.[1] === `$${callee}`))
1042
1296
  includeForCallableValue()
1043
1297
  return callee
1044
1298
  }
1045
1299
  if (Array.isArray(callee) && callee[0] === '.') {
1046
1300
  const [, obj, prop] = callee
1301
+ // A user binding named like a builtin namespace (`let Math = {…}`) shadows
1302
+ // it — resolve `Math.max(…)` as a method call on the local value, not the
1303
+ // builtin named-call. (Property reads route through the `.` handler's own
1304
+ // shadow check.)
1305
+ if (shadowsBuiltin(obj)) return prep(callee)
1306
+ // SIMD intrinsic namespaces resolve members directly to their emit key, ahead of
1307
+ // generic-method dispatch — they're pure namespaces (never runtime values), and
1308
+ // names like `f32x4.add` must not be mistaken for the generic `.add` (Set/Map).
1309
+ if (typeof obj === 'string' && typeof prop === 'string' && SIMD_NS.has(obj) && !(scopes.length && isDeclared(obj)) && !ctx.scope.userGlobals?.has?.(obj)) {
1310
+ includeModule(obj); return `${obj}.${prop}`
1311
+ }
1047
1312
  const key = typeof obj === 'string' && typeof prop === 'string' ? `${obj}.${prop}` : null
1048
1313
  if (key && ctx.module.hostImports?.[obj]?.[prop]) {
1049
1314
  const spec = ctx.module.hostImports[obj][prop]
@@ -1078,18 +1343,13 @@ function renestSoleCommaArg(args) {
1078
1343
  }
1079
1344
 
1080
1345
  const handlers = {
1346
+ ...rejectHandlers(err),
1081
1347
  // Spread operator: [...expr] in arrays, f(...args) in calls, {...obj} in objects
1082
1348
  '...'(expr) {
1083
1349
  includeForArrayLiteral()
1084
1350
  return ['...', prep(expr)]
1085
1351
  },
1086
1352
 
1087
- // Prohibited ops — duplicated from jzify deliberately: .jz source bypasses jzify,
1088
- // so prepare is the actual defense. Messages here fire for both .js and .jz.
1089
- 'async': () => err('async/await not supported: WASM is synchronous'),
1090
- 'await': () => err('async/await not supported: WASM is synchronous'),
1091
- 'class': () => err('class not supported: use object literals'),
1092
- 'yield': () => err('generators not supported: use loops'),
1093
1353
  'debugger': () => null,
1094
1354
  // Static-key delete (.x, ["x"], [literal]) would change the fixed schema → reject.
1095
1355
  // Computed-key delete (obj[expr]) — including jessie's `delete ctx[k]` — lowers
@@ -1104,12 +1364,7 @@ const handlers = {
1104
1364
  err('delete not supported: object shape is fixed')
1105
1365
  },
1106
1366
  'in'(key, obj) { return ['in', prep(key), prep(obj)] },
1107
- 'instanceof': () => err('instanceof not supported: use typeof'),
1108
- 'with': () => err('`with` not supported: deprecated'),
1109
- ':': () => err('labeled statements not supported'),
1110
1367
  'label'(name, body) { return ['label', name, prep(body)] },
1111
- 'var': () => err('`var` not supported: use let/const'),
1112
- 'function': () => err('`function` not supported: use arrow functions'),
1113
1368
 
1114
1369
  // Destructuring assignment: [a, ...b] = expr or {x, y} = expr
1115
1370
  '='(lhs, rhs) {
@@ -1147,7 +1402,7 @@ const handlers = {
1147
1402
  // emit a dynamic property read + indirect call instead of a direct call.
1148
1403
  if (ctx.func.names.has(name)) {
1149
1404
  ctx.func.multiProp.add(`${fnBase}.${lhs[2]}`)
1150
- do name = `${fnBase}$${lhs[2]}$${ctx.func.uniq++}`; while (ctx.func.names.has(name))
1405
+ do { name = `${fnBase}$${lhs[2]}$${ctx.func.uniq++}` } while (ctx.func.names.has(name))
1151
1406
  }
1152
1407
  // Build the target `.` node directly from the resolved base — re-`prep`ing
1153
1408
  // the lhs would resolve a multiProp `fn.prop` to an rvalue (closure
@@ -1165,22 +1420,25 @@ const handlers = {
1165
1420
  recordGlobalRep(plhs, prhs)
1166
1421
  if (Array.isArray(prhs) && prhs[0] === '{}') {
1167
1422
  const props = staticObjectProps(prhs.slice(1))
1168
- if (props) ctx.schema.vars.set(plhs, ctx.schema.register(props.names))
1169
- }
1170
- }
1423
+ if (props) bindAssignSchema(plhs, ctx.schema.register(props.names))
1424
+ } else bindAssignSchema(plhs, null)
1425
+ } else bindAssignSchema(plhs, objLiteralSid(prhs))
1171
1426
  // Static string/array facts hold only while every assignment is constant.
1172
1427
  if (!assignedStaticGlobals.has(plhs) && (staticStr != null || staticArr)) bindStaticGlobal(plhs, staticStr, staticArr)
1173
1428
  else deleteStaticGlobal(plhs)
1174
1429
  assignedStaticGlobals.add(plhs)
1175
1430
  }
1176
- // Local object-literal assignment to a not-yet-shaped variable — e.g. a `var`
1177
- // that jzify hoisted into `let x; x = {…}`. Recording the schema here lets the
1178
- // binding behave like `let x = {…}`: fixed-slot field access and for-in unroll.
1179
- // First assignment fixes the shape (mirrors the global rule above).
1180
- else if (typeof plhs === 'string' && Array.isArray(prhs) && prhs[0] === '{}'
1181
- && !ctx.schema.vars.has(plhs)) {
1182
- const props = staticObjectProps(prhs.slice(1))
1183
- if (props) ctx.schema.vars.set(plhs, ctx.schema.register(props.names))
1431
+ // Object-literal assignment to a variable — e.g. a `var` that jzify hoisted
1432
+ // into `let x; x = {…}`. Recording the schema lets the binding behave like
1433
+ // `let x = {…}`: fixed-slot field access and for-in unroll. SOUNDNESS: the
1434
+ // shape holds only while EVERY assignment to the name agrees one literal
1435
+ // shape, no other sources. Any disagreeing assignment (non-literal RHS such
1436
+ // as a table/Map lookup, or a different-shape literal) unbinds and poisons
1437
+ // the name; fixed-slot reads against one literal's layout would misread the
1438
+ // other sources' objects (e.g. `.x` returning another shape's slot-0 value).
1439
+ // Compile reads the END state, so the conflict check is order-insensitive.
1440
+ else if (typeof plhs === 'string') {
1441
+ bindAssignSchema(plhs, objLiteralSid(prhs))
1184
1442
  }
1185
1443
  return ['=', plhs, prhs]
1186
1444
  },
@@ -1191,23 +1449,21 @@ const handlers = {
1191
1449
  const catchClause = clauses.find(c => Array.isArray(c) && c[0] === 'catch')
1192
1450
  const finallyClause = clauses.find(c => Array.isArray(c) && c[0] === 'finally')
1193
1451
  const tryBody = prep(body)
1452
+ // prep(handler) ONCE — it has side effects (uniq++, scope pushes, includes), so
1453
+ // the no-finally catch branch must reuse `caught`, not re-prep (FE-3 fix).
1194
1454
  const caught = catchClause
1195
- ? (() => {
1196
- const [, errName, handler] = catchClause
1197
- return ['catch', tryBody, errName, prep(handler)]
1198
- })()
1455
+ ? ['catch', tryBody, catchClause[1], prep(catchClause[2])]
1199
1456
  : tryBody
1200
- if (finallyClause) return ['finally', caught, prep(finallyClause[1])]
1201
- if (catchClause) {
1202
- const [, errName, handler] = catchClause
1203
- return ['catch', tryBody, errName, prep(handler)]
1204
- }
1205
- return tryBody
1457
+ return finallyClause ? ['finally', caught, prep(finallyClause[1])] : caught
1206
1458
  },
1207
1459
  'throw'(expr) { return ['throw', prep(expr)] },
1208
1460
 
1209
1461
  // Template literal: [``, part, ...] → fused single-allocation string concat.
1210
1462
  '`'(...parts) {
1463
+ // Fully-static template (`a${123}b`, `hello ${1+2} world`) folds to a single string
1464
+ // literal — a static data segment / SSO box, no runtime concat and no heap machinery.
1465
+ const folded = staticStringExpr(['`', ...parts])
1466
+ if (folded != null) return staticString(folded)
1211
1467
  includeForStringValue()
1212
1468
  const nodes = parts.map(p =>
1213
1469
  Array.isArray(p) && p[0] == null && typeof p[1] === 'string' ? ['str', p[1]] : prep(p))
@@ -1238,6 +1494,24 @@ const handlers = {
1238
1494
  return handlers['from'](fromNode[1], fromNode[2])
1239
1495
  },
1240
1496
 
1497
+ // Mixed default+named import `import d, { n } from 'm'` — jessie emits it as a
1498
+ // statement-level comma `[',', ['import', d], ['from', spec, src]]` (the default
1499
+ // fragment lost its source). Reunite: bind the default, then the named specifiers,
1500
+ // both against the shared source. (prepareModule caches by specifier, so preparing
1501
+ // the source twice is a no-op — same as two separate `import` statements.)
1502
+ // Any other comma is a sequence expression: fall through to generic prep.
1503
+ ','(...items) {
1504
+ if (items.length === 2
1505
+ && Array.isArray(items[0]) && items[0][0] === 'import' && typeof items[0][1] === 'string'
1506
+ && Array.isArray(items[1]) && items[1][0] === 'from') {
1507
+ const source = items[1][2]
1508
+ handlers['from'](items[0][1], source)
1509
+ handlers['from'](items[1][1], source)
1510
+ return null
1511
+ }
1512
+ return [',', ...items.map(prep)]
1513
+ },
1514
+
1241
1515
  'from'(specifiers, source) {
1242
1516
  const mod = source?.[1]
1243
1517
  if (!mod || typeof mod !== 'string') return err('Invalid import source')
@@ -1296,8 +1570,8 @@ const handlers = {
1296
1570
  }
1297
1571
 
1298
1572
  // Tier 2: Source module (bundling)
1299
- if (ctx.module.importSources?.[mod]) {
1300
- const resolved = prepareModule(mod, ctx.module.importSources[mod])
1573
+ if (isBundledModule(mod)) {
1574
+ const resolved = prepareModule(mod, ctx.module.importSources?.[mod])
1301
1575
  // Default import: import name from 'mod' → bind to default export
1302
1576
  if (typeof specifiers === 'string') {
1303
1577
  const mangled = resolved.exports.get('default')
@@ -1374,19 +1648,39 @@ const handlers = {
1374
1648
  pushScope(); const b = prep(body); popScope()
1375
1649
  return ['while', c, b]
1376
1650
  },
1651
+ // do { body } while (cond) → flag-guarded while: `flag=true; while (flag||cond) { flag=false; body }`.
1652
+ // jzify lowers this in default mode (jzify/transform.js), but strict mode skips jzify — without
1653
+ // this prepare-stage twin, strict `do-while` reaches emit as a raw 'do' and dies ("Unknown op: do"),
1654
+ // contradicting the README's strict-subset list. Re-prep the synthetic tree so scope/normalize apply.
1655
+ 'do': (body, cond) => {
1656
+ const flag = `${T}do${ctx.func.uniq++}`
1657
+ return prep([';',
1658
+ ['let', ['=', flag, [null, true]]],
1659
+ ['while', ['||', flag, cond],
1660
+ ['{}', [';', ['=', flag, [null, false]], body]]]])
1661
+ },
1377
1662
 
1378
1663
  'export': decl => {
1379
1664
  if (Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const'))
1380
1665
  for (const i of decl.slice(1))
1381
1666
  if (Array.isArray(i) && i[0] === '=' && typeof i[1] === 'string')
1382
1667
  ctx.func.exports[i[1]] = true
1668
+ // export name → bare-identifier re-export (shorthand for `export { name }`).
1669
+ // Register the binding and emit nothing; without this the name falls through
1670
+ // to `prep(decl)` below and compiles as a dead `global.get; drop` statement
1671
+ // while the export itself is silently lost.
1672
+ if (typeof decl === 'string') {
1673
+ const resolved = ctx.scope.chain[decl]
1674
+ ctx.func.exports[decl] = (resolved && resolved !== decl) ? resolved : decl
1675
+ return null
1676
+ }
1383
1677
  // export { name, name as alias } from './mod' or export * from './mod'
1384
1678
  if (Array.isArray(decl) && decl[0] === 'from') {
1385
1679
  const mod = decl[2]?.[1]
1386
1680
  if (!mod || typeof mod !== 'string') return null
1387
1681
  // Source module re-export
1388
- if (ctx.module.importSources?.[mod]) {
1389
- const resolved = prepareModule(mod, ctx.module.importSources[mod])
1682
+ if (isBundledModule(mod)) {
1683
+ const resolved = prepareModule(mod, ctx.module.importSources?.[mod])
1390
1684
  if (decl[1] === '*') {
1391
1685
  // export * from './mod' → register all exports
1392
1686
  for (const [name, mangled] of resolved.exports) {
@@ -1439,7 +1733,7 @@ const handlers = {
1439
1733
  if (defFunc('default', prep(val))) return null
1440
1734
  }
1441
1735
  // export default expr → create global 'default'
1442
- ctx.scope.globals.set('default', `(global $default (mut f64) (f64.const 0))`)
1736
+ declGlobal('default', 'f64')
1443
1737
  ctx.scope.userGlobals.add('default')
1444
1738
  return ['=', 'default', prep(val)]
1445
1739
  }
@@ -1456,12 +1750,18 @@ const handlers = {
1456
1750
  depth++
1457
1751
  pushScope(fnScope)
1458
1752
  funcLocalNames.push(new Set(collectParamNames(raw)))
1753
+ funcValueNames.push(new Set())
1459
1754
 
1460
1755
  const nextParams = []
1461
1756
  const bodyPrefix = []
1462
1757
  for (const r of raw) {
1463
1758
  const c = classifyParam(r)
1464
1759
  if (c.kind === 'rest') {
1760
+ // A rest param is an array: the binding holds one, and every call site
1761
+ // builds the rest array via `['[', …]`. Pull in the array emitter even
1762
+ // when the body never names an array literal (e.g. `(...xs) => 0`),
1763
+ // otherwise the call-site rest construction hits "Unknown op: [".
1764
+ includeForArrayLiteral()
1465
1765
  nextParams.push(r)
1466
1766
  if (typeof c.name === 'string') fnScope.set(c.name, c.name)
1467
1767
  } else if (c.kind === 'plain') {
@@ -1497,6 +1797,7 @@ const handlers = {
1497
1797
  const result = ['=>', Array.isArray(params) && params[0] === '()' ? ['()', inner] : inner, preparedBody]
1498
1798
  popScope()
1499
1799
  funcLocalNames.pop()
1800
+ funcValueNames.pop()
1500
1801
  depth--
1501
1802
  return result
1502
1803
  },
@@ -1516,9 +1817,13 @@ const handlers = {
1516
1817
  })]
1517
1818
  },
1518
1819
 
1519
- // Optional chaining / typeof — need ptr module
1520
- '?.'(obj, prop) { return ['?.', prep(obj), prop] },
1521
- '?.[]'(obj, idx) { return ['?.[]', prep(obj), prep(idx)] },
1820
+ // Optional chaining / typeof — need ptr module. Optional member access pulls
1821
+ // the same modules as plain `.`/`[]` (a method like `includes` needs string +
1822
+ // array for emit's runtime dispatch); the only difference is the nullish guard,
1823
+ // which is emit's concern. Without this, `obj?.m(…)` reaches emit missing the
1824
+ // `.m` emitter and falls to the dynamic path that needs an unincluded module.
1825
+ '?.'(obj, prop) { includeForProperty(prop); return ['?.', prep(obj), prop] },
1826
+ '?.[]'(obj, idx) { includeForArrayAccess(); return ['?.[]', prep(obj), prep(idx)] },
1522
1827
  '?.()'(callee, callArgs) {
1523
1828
  // Parser wraps multi-args in a comma list, like '()'. Unwrap so emit gets flat positional args.
1524
1829
  const items = callArgs == null ? []
@@ -1557,7 +1862,16 @@ const handlers = {
1557
1862
  return ['+', pa, pb]
1558
1863
  },
1559
1864
  '-'(a, b) {
1560
- if (b === undefined) { const na = prep(a); return isLit(na) && typeof na[1] === 'number' ? [, -na[1]] : ['u-', na] }
1865
+ // Fold `-<numeric literal>` to a literal, but NOT a bigint: jz's own `typeof` reports
1866
+ // a bigint value as 'number' too (its carrier is an f64), so under self-host this test
1867
+ // alone wrongly folds `-5n`, and negating the bigint here yields garbage (-2^63+5).
1868
+ // `typeof !== 'bigint'` excludes it in both engines (real JS: 'bigint'; jz: matches
1869
+ // 'bigint'). Bigint negation then flows to emit's i64.sub(0,·) path correctly.
1870
+ // `-0` is NOT folded: the self-host kernel evaluates the constant `-na[1]` with
1871
+ // i32 negation (i32 has no signed zero), collapsing -0→+0 — observable via sort's
1872
+ // -0<+0 tiebreak, Object.is, and 1/x. Leaving it as a runtime `u-` emits f64.neg,
1873
+ // which preserves the sign in both engines; V8 re-folds it, so no native cost.
1874
+ if (b === undefined) { const na = prep(a); return isLit(na) && typeof na[1] === 'number' && typeof na[1] !== 'bigint' && na[1] !== 0 ? [, -na[1]] : ['u-', na] }
1561
1875
  return ['-', prep(a), prep(b)]
1562
1876
  },
1563
1877
 
@@ -1565,17 +1879,19 @@ const handlers = {
1565
1879
  '?'(cond, then, els) { return ['?:', prep(stripBoolNot(cond)), prep(then), prep(els)] },
1566
1880
 
1567
1881
  // ++/-- prefix vs postfix: parser sends trailing null for postfix
1568
- // Postfix i++ = (++i) - 1: increment happens, arithmetic recovers old value
1569
- // Property increment: obj.prop++ obj.prop = obj.prop + 1
1882
+ // Postfix i++ = (++i) - 1: increment happens, arithmetic recovers old value.
1883
+ // Property obj.prop++ has no dedicated ++ node (the ++ emitter is name-based),
1884
+ // so it lowers to `obj.prop = obj.prop + 1` (returns the NEW value) — and the
1885
+ // same -1/+1 recovery wraps it for postfix to yield the OLD value.
1570
1886
  '++'(a, _post) {
1571
1887
  const n = prep(a)
1572
- if (Array.isArray(n) && (n[0] === '.' || n[0] === '[]')) return ['=', n, ['+', n, [, 1]]]
1573
- return _post !== undefined ? ['-', ['++', n], [, 1]] : ['++', n]
1888
+ const inc = Array.isArray(n) && (n[0] === '.' || n[0] === '[]') ? ['=', n, ['+', n, [, 1]]] : ['++', n]
1889
+ return _post !== undefined ? ['-', inc, [, 1]] : inc
1574
1890
  },
1575
1891
  '--'(a, _post) {
1576
1892
  const n = prep(a)
1577
- if (Array.isArray(n) && (n[0] === '.' || n[0] === '[]')) return ['=', n, ['-', n, [, 1]]]
1578
- return _post !== undefined ? ['+', ['--', n], [, 1]] : ['--', n]
1893
+ const dec = Array.isArray(n) && (n[0] === '.' || n[0] === '[]') ? ['=', n, ['-', n, [, 1]]] : ['--', n]
1894
+ return _post !== undefined ? ['+', dec, [, 1]] : dec
1579
1895
  },
1580
1896
 
1581
1897
  // Regex literal: ['//','pattern','flags?'] → include regex module, pass through
@@ -1583,13 +1899,22 @@ const handlers = {
1583
1899
  return ['//', pattern, flags]
1584
1900
  },
1585
1901
 
1586
- '**'(a, b) { return ['**', prep(a), prep(b)] },
1902
+ '**'(a, b) {
1903
+ // ES2016 §13.6: an unparenthesized unary expression cannot be the base of `**`
1904
+ // — `-x**2`, `~x**2`, `!x**2`, `+x**2`, `typeof x**2`, `void x**2`, `delete o[k]**2`
1905
+ // are all SyntaxErrors (the precedence is ambiguous). The parser leaves a grouping
1906
+ // as `['()', …]`, so a parenthesized base `(-x)**2` (and `-(x**2)`, where the unary
1907
+ // sits outside the `**`) arrives with a non-unary root op and is allowed.
1908
+ if (Array.isArray(a) && a.length === 2 && (a[0] === '-' || a[0] === '+' || a[0] === '!' || a[0] === '~' || a[0] === 'typeof' || a[0] === 'void' || a[0] === 'delete'))
1909
+ err(`Unary '${a[0]}' before '**' is a SyntaxError (ES2016 §13.6) — parenthesize: (${a[0]} x) ** 2 or ${a[0]} (x ** 2)`)
1910
+ return ['**', prep(a), prep(b)]
1911
+ },
1587
1912
 
1588
1913
  // Function call or grouping parens
1589
1914
  '()'(callee, ...args) {
1590
1915
  // Grouping: (expr) → ['()', expr] with no args. Call: f() → ['()', 'f', null] with null arg.
1591
1916
  if (args.length === 0) return prep(callee)
1592
- if (typeof callee === 'string' && PROHIBITED[callee]) err(PROHIBITED[callee])
1917
+ if (typeof callee === 'string' && REJECT_IDENTS[callee]) err(REJECT_IDENTS[callee])
1593
1918
 
1594
1919
  // Compile-time folds: the callee names something resolvable now. Each fold
1595
1920
  // is gated by callee shape, so at most one of the three fires.
@@ -1607,7 +1932,14 @@ const handlers = {
1607
1932
  includeForCallableValue(); break
1608
1933
  }
1609
1934
  }
1610
- const result = ['()', callee, ...preppedArgs]
1935
+ // A zero-arg call keeps its explicit `null` args slot: `['()', callee, null]`,
1936
+ // not the slot-less `['()', callee]`. The latter is indistinguishable from a
1937
+ // grouping `(expr)`, so a second `prep` pass (the destructuring-assignment
1938
+ // lowering re-`prep`s its result) would re-read `x.pop()` as the grouping
1939
+ // `(x.pop)` and drop the call. Keeping the slot makes `prep` idempotent for
1940
+ // calls and matches `setCallArgs`'s canonical shape; `commaList(node[2])`
1941
+ // reads it back as zero args everywhere downstream.
1942
+ const result = preppedArgs.length ? ['()', callee, ...preppedArgs] : ['()', callee, null]
1611
1943
 
1612
1944
  if (callee === 'Object.assign' && ctx.schema.register) inferAssignSchema(result)
1613
1945
 
@@ -1649,9 +1981,11 @@ const handlers = {
1649
1981
  },
1650
1982
 
1651
1983
  // Object literal - flatten comma, expand shorthand
1652
- '{}'(inner) {
1653
- // Detect block body vs object literal
1654
- if (Array.isArray(inner) && STMT_OPS.has(inner[0])) {
1984
+ '{}'(...args) {
1985
+ const inner = args[0]
1986
+ // Block body: a single statement-op child (object props always start with
1987
+ // ':' or '...', never a statement op, so this never misfires on a literal).
1988
+ if (args.length === 1 && Array.isArray(inner) && STMT_OPS.has(inner[0])) {
1655
1989
  // Block body: push block scope for let/const shadowing
1656
1990
  pushScope()
1657
1991
  const result = ['{}', prep(inner)]
@@ -1660,10 +1994,16 @@ const handlers = {
1660
1994
  }
1661
1995
 
1662
1996
  includeForObjectLiteral()
1663
- if (inner == null) return ['{}']
1664
- const items = Array.isArray(inner) && inner[0] === ','
1665
- ? inner.slice(1)
1666
- : [inner]
1997
+ if (args.length === 0 || inner == null) return ['{}']
1998
+ // The parser emits one comma-grouped child `['{}', [',', p1, p2]]`, but prep's
1999
+ // own output is spread `['{}', p1, p2]` (see `result` below). Accept both so
2000
+ // prep stays idempotent: the destructuring-assignment lowering ('=' handler)
2001
+ // re-preps a wrapper that already holds a normalized literal, and reading only
2002
+ // the first child here would drop every property but the first — mis-sizing the
2003
+ // schema to cap-1 and losing the rest.
2004
+ const items = args.length === 1
2005
+ ? (Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner])
2006
+ : args
1667
2007
 
1668
2008
  // Computed keys: `{[k]: v}` where `k` isn't compile-time foldable. jz's
1669
2009
  // object layout is slot-based (fixed schema at the literal site), so a
@@ -1699,6 +2039,12 @@ const handlers = {
1699
2039
  if (key == null) err('computed property name not supported for fixed-shape object: use a compile-time string/number key')
1700
2040
  return [':', key, prep(p[2])]
1701
2041
  }
2042
+ // Accessors (`{ get x() {…} }` / `{ set x(v) {…} }`) parse to ['get'|'set', …].
2043
+ // jz objects are fixed-shape slot records with no accessor protocol, so they'd
2044
+ // otherwise fall through and compile to dead code (0 schema slots → `o.x` reads
2045
+ // undefined). Reject loudly — silent miscompile breaks "valid jz = valid JS".
2046
+ if (Array.isArray(p) && (p[0] === 'get' || p[0] === 'set'))
2047
+ err('object getter/setter not supported — jz objects have no accessors; use a method or a plain property + function')
1702
2048
  return prep(p)
1703
2049
  }
1704
2050
  let prepped = items.map(prop)
@@ -1725,27 +2071,56 @@ const handlers = {
1725
2071
  // For loop
1726
2072
  'for'(head, body) {
1727
2073
  pushScope()
2074
+ // A comma/sequence Expression in a for-IN head RHS — `for (x in a, b)` — is valid (the RHS is
2075
+ // an Expression): evaluate left-to-right for side effects, value as the last element. (for-OF's
2076
+ // RHS is an AssignmentExpression — no comma — so it is left alone.) jzify lands it as a bare `,`
2077
+ // node in the source slot. Don't wrap it in `()`: Object.keys((a, obj)) hides `obj` behind the
2078
+ // sequence and loses its static schema (a non-escaping literal scalarizes → 0 keys). Instead take
2079
+ // the LAST element as the (direct) iteration source and run the earlier elements once first.
2080
+ let forInSeqPre = null
2081
+ if (Array.isArray(head) && head[0] === 'in' && Array.isArray(head[2]) && head[2][0] === ',') {
2082
+ const parts = head[2].slice(1)
2083
+ head = [head[0], head[1], parts[parts.length - 1]]
2084
+ if (parts.length > 2) forInSeqPre = [',', ...parts.slice(0, -1)]
2085
+ else if (parts.length === 2) forInSeqPre = parts[0]
2086
+ }
1728
2087
  let r
1729
2088
  if (Array.isArray(head) && head[0] === ';') {
1730
2089
  let [, init, cond, step] = head
1731
2090
  cond = stripBoolNot(cond)
1732
- // Hoist .length / .size / .byteLength from for-condition:
1733
- // `i < arr.length` → `let __len = arr.length | 0; ... i < __len`
1734
- // The `| 0` forces i32 even for unknown-typed receivers (where __length
1735
- // returns f64). NaN→0 via i32.trunc_sat matches JS semantics: a NaN bound
1736
- // makes `i < NaN` false on both representations, so the loop is skipped
1737
- // either way. Keeping the hoisted bound i32 lets the counter `i` stay i32
1738
- // through the comparison and `i++`, eliminating the per-iteration
1739
- // f64.convert_i32_s + f64.lt + f64.add + i32.trunc_sat_f64_s sequence.
2091
+ // Keep a `.length` / `.size` / `.byteLength` for-bound i32 without snapshotting it:
2092
+ // `i < arr.length` → `i < (arr.length | 0)` (re-read every iteration)
2093
+ // The `| 0` forces i32 even for unknown-typed receivers (where __length returns
2094
+ // f64), so the counter `i` stays i32 through the comparison and `i++` — no
2095
+ // per-iteration f64.convert_i32_s + f64.lt + f64.add + i32.trunc_sat round-trip.
2096
+ // It must stay INLINE, not hoisted into a pre-loop local: JS re-reads the bound
2097
+ // each step, and a loop body can grow/shrink the array mid-iteration — including
2098
+ // through an alias the compiler can't see locally (e.g. `arr` shares identity with
2099
+ // a field a called helper pushes to, as compilePendingClosures does over
2100
+ // ctx.closure.bodies). A snapshot diverges from JS and silently truncates such loops.
1740
2101
  if (cond && Array.isArray(cond) && (cond[0] === '<' || cond[0] === '<=' || cond[0] === '>' || cond[0] === '>=')) {
1741
2102
  const lenExpr = cond[0] === '<' || cond[0] === '<=' ? cond[2] : cond[1]
1742
2103
  if (Array.isArray(lenExpr) && lenExpr[0] === '.' &&
1743
2104
  (lenExpr[2] === 'length' || lenExpr[2] === 'size' || lenExpr[2] === 'byteLength')) {
1744
- const lenVar = `${T}len${ctx.func.uniq++}`
1745
- const lenDecl = ['let', ['=', lenVar, ['|', lenExpr, [, 0]]]]
1746
- init = init ? [';', init, lenDecl] : lenDecl
1747
- if (cond[0] === '<' || cond[0] === '<=') cond = [cond[0], cond[1], lenVar]
1748
- else cond = [cond[0], lenVar, cond[2]]
2105
+ const recv = lenExpr[1]
2106
+ const bound = ['|', lenExpr, [, 0]]
2107
+ const lengthStable = typeof recv === 'string' &&
2108
+ boundSafeCalls(body) && boundSafeCalls(step) && !writesReceiver(body, recv) && !writesReceiver(step, recv)
2109
+ if (lengthStable) {
2110
+ // Body can't change the bound → snapshot it once into an i32 local. Keeps
2111
+ // the counter `i` i32 through compare + `i++` (no per-iteration f64 round
2112
+ // trip) and gives the vectorizer the hoisted trip count it matches on.
2113
+ const lenVar = `${T}len${ctx.func.uniq++}`
2114
+ const lenDecl = ['let', ['=', lenVar, bound]]
2115
+ init = init ? [';', init, lenDecl] : lenDecl
2116
+ if (cond[0] === '<' || cond[0] === '<=') cond = [cond[0], cond[1], lenVar]
2117
+ else cond = [cond[0], lenVar, cond[2]]
2118
+ } else {
2119
+ // Body may grow/shrink the array (push/pop, or alias mutation through a
2120
+ // call) → re-read every iteration, as JS does. Still `| 0` for an i32 bound.
2121
+ if (cond[0] === '<' || cond[0] === '<=') cond = [cond[0], cond[1], bound]
2122
+ else cond = [cond[0], bound, cond[2]]
2123
+ }
1749
2124
  }
1750
2125
  }
1751
2126
  r = ['for', init ? prep(init) : null, cond ? prep(cond) : null, step ? prep(step) : null, prep(body)]
@@ -1757,61 +2132,71 @@ const handlers = {
1757
2132
  const varName = Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const') ? decl[1] : decl
1758
2133
  const idx = `${T}i${ctx.func.uniq++}`
1759
2134
  const lenVar = `${T}len${ctx.func.uniq++}`
1760
- const trivial = typeof src === 'string'
1761
- const arrVar = trivial ? src : `${T}arr${ctx.func.uniq++}`
2135
+ const arrVar = `${T}arr${ctx.func.uniq++}`
2136
+ // Normalize the source to an index-iterable once: a Set→keys / Map→[k,v]
2137
+ // array, while an Array/String/TypedArray passes through untouched (no
2138
+ // copy). Without this, `coll[i]` on a Set/Map reads raw open-addressing
2139
+ // slot words instead of live entries.
1762
2140
  // Wrap .length in `| 0` so the hoisted bound is i32 even for unknown
1763
2141
  // receivers (same rationale as the for-cond hoist above).
1764
2142
  const lenE = ['|', ['.', arrVar, 'length'], [, 0]]
1765
- const decls = trivial
1766
- ? ['let', ['=', idx, [, 0]], ['=', lenVar, lenE]]
1767
- : ['let', ['=', arrVar, src], ['=', idx, [, 0]], ['=', lenVar, lenE]]
2143
+ const decls = ['let', ['=', arrVar, ['()', '__iter_arr', src]], ['=', idx, [, 0]], ['=', lenVar, lenE]]
1768
2144
  const cond = ['<', idx, lenVar]
1769
2145
  const step = ['++', idx]
1770
2146
  const inner = [';', ['let', ['=', varName, ['[]', arrVar, idx]]], body]
1771
2147
  r = prep(['for', [';', decls, cond, step], inner])
1772
2148
  } else if (Array.isArray(head) && head[0] === 'in') {
1773
- // for (let k in obj) unroll at compile time when schema known, else HASH runtime iteration
2149
+ // `forin` relies on runtime key enumeration outside the pure canonical subset. strict
2150
+ // rejects it (consistent with `obj[k]` / unknown-receiver methods); use `Object.keys(obj)`.
2151
+ if (ctx.transform.strict) err('strict mode: `for…in` is not in the canonical subset — it relies on runtime key enumeration. Iterate `Object.keys(obj)` explicitly instead.')
2152
+ // for (let k in src) → enumerate src's own keys via Object.keys (schema ∪ any keys added
2153
+ // later for objects; "0".."n-1" for arrays/strings; [] for Set/Map) and iterate the resulting
2154
+ // array by index. One uniform path keeps for-in consistent with Object.keys (so dynamically
2155
+ // added keys appear in both), and break/continue work as in any for-loop. Object.keys'
2156
+ // enumeration stdlib is pulled only when for-in is actually used.
1774
2157
  const [, decl, src] = head
1775
- const varName = Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const') ? decl[1] : decl
1776
- const srcName = typeof src === 'string' ? (ctx.scope.chain[src] || src) : null
1777
- const sid = typeof srcName === 'string' ? ctx.schema.vars.get(srcName) : null
1778
- if (sid != null) {
1779
- // Known schema compile-time unrolling with string keys
1780
- const keys = ctx.schema.list[sid]
1781
- if (!keys || !keys.length) { popScope(); return null }
1782
- includeForKnownKeyIteration()
1783
- if (!hasLoopJump(body)) {
1784
- // No break/continue flat unroll, no loop frame needed.
1785
- const stmts = []
1786
- for (let i = 0; i < keys.length; i++) {
1787
- stmts.push(i === 0
1788
- ? ['let', ['=', varName, [, keys[i]]]]
1789
- : ['=', varName, [, keys[i]]])
1790
- stmts.push(cloneNode(body))
1791
- }
1792
- r = prep([';', ...stmts])
1793
- } else {
1794
- // break/continue present an unrolled loop still needs its frames.
1795
- // Wrap each iteration in a labeled block (continue target) and the
1796
- // whole run in an outer labeled block (break target): `break` exits
1797
- // the construct, `continue` falls through to the next iteration.
1798
- const brkL = `${T}fibrk${ctx.func.uniq++}`
1799
- const decl = prep(['let', ['=', varName, [, keys[0]]]])
1800
- const parts = [decl]
1801
- for (let i = 0; i < keys.length; i++) {
1802
- const contL = `${T}ficont${ctx.func.uniq++}`
1803
- const iter = prep(i === 0
1804
- ? cloneNode(body)
1805
- : [';', ['=', varName, [, keys[i]]], cloneNode(body)])
1806
- parts.push(['label', contL, retargetLoopJumps(iter, brkL, contL)])
1807
- }
1808
- r = ['label', brkL, [';', ...parts]]
1809
- }
1810
- } else {
1811
- // Dynamic object HASH runtime iteration
1812
- includeForRuntimeKeyIteration()
1813
- r = ['for-in', varName, prep(src), prep(body)]
1814
- }
2158
+ const isDecl = Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const')
2159
+ // `for ((x) in …)` the LHS may be a cover-parenthesized identifier; unwrap to the target.
2160
+ let lhs = decl; while (Array.isArray(lhs) && lhs[0] === '()') lhs = lhs[1]
2161
+ const target = isDecl ? decl[1] : lhs
2162
+ // A member/computed LHS (`for (x.y in …)`, `for (obj[k] in …)`) assigns each key into the
2163
+ // existing place; let/const and a bare name take a fresh per-iteration `let` binding.
2164
+ const isMemberTarget = Array.isArray(target) && (target[0] === '.' || target[0] === '[]')
2165
+ // for-in over null/undefined is a no-op — ES ForIn/OfHeadEvaluation returns a break
2166
+ // completion before enumerating — but Object.keys(null|undefined) throws. So a nullish
2167
+ // source must enumerate the empty set. A static null (`[null,null]`) / undefined (`[]`)
2168
+ // skips Object.keys entirely; a bare identifier is guarded by a runtime `== null` test
2169
+ // (evaluated twice side-effect-free keeping Object.keys' *direct* receiver so its
2170
+ // static key schema still resolves); object/array literals and other expressions, which
2171
+ // are never nullish or carry no static schema to lose, stay direct.
2172
+ // A nullish literal node is `[<nullish-op>, value]` with both slots nullish: `null` is
2173
+ // `[null, null]`, `undefined` is `[null]` (empty value slot). A numeric/string literal
2174
+ // `[null, v]` has a non-nullish value slot, so `src[1] == null` discriminates them.
2175
+ const nullish = Array.isArray(src) && src[0] == null && src[1] == null
2176
+ // `__keys_ro` is for-in's read-only key list: identical to Object.keys, but
2177
+ // when the receiver has a complete static schema the keys are a compile-time
2178
+ // constant, so it pools ONE static-data array instead of allocating a fresh
2179
+ // one each evaluation the per-iteration heap-growth cliff (jz#deopt-forin).
2180
+ // Sound only because for-in reads ks[i]/ks.length and never mutates (unlike
2181
+ // user Object.keys, which permits in-place `.sort()`/`.reverse()`).
2182
+ includeMods('core', 'object', 'string')
2183
+ const keysExpr = nullish ? ['[]', null]
2184
+ : typeof src === 'string'
2185
+ ? ['?', ['==', src, [null, null]], ['[]', null], ['()', '__keys_ro', src]]
2186
+ : ['()', '__keys_ro', src]
2187
+ const ks = `${T}fik${ctx.func.uniq++}`, ix = `${T}fii${ctx.func.uniq++}`, lenV = `${T}fil${ctx.func.uniq++}`
2188
+ const decls = ['let',
2189
+ ['=', ks, keysExpr],
2190
+ ['=', ix, [, 0]],
2191
+ ['=', lenV, ['|', ['.', ks, 'length'], [, 0]]]]
2192
+ const bindEach = isMemberTarget
2193
+ ? ['=', target, ['[]', ks, ix]] // x.y = key / obj[k] = key
2194
+ : ['let', ['=', target, ['[]', ks, ix]]] // let k = key (fresh per-iteration binding)
2195
+ const forNode = ['for', [';', decls, ['<', ix, lenV], ['++', ix]],
2196
+ [';', bindEach, body]]
2197
+ // Run the dropped sequence prefix (earlier comma elements) once for side effects, before
2198
+ // the loop. Built raw and prepped as a unit so prep inserts the value-drop on the prefix.
2199
+ r = prep(forInSeqPre ? [';', forInSeqPre, forNode] : forNode)
1815
2200
  } else {
1816
2201
  // Some parser/jzify shapes for `for (;;)` and `for (; cond; )` arrive
1817
2202
  // as a null or bare-condition head instead of the canonical
@@ -1826,14 +2211,22 @@ const handlers = {
1826
2211
  // Property access - resolve namespaces or object/array properties
1827
2212
  '.'(obj, prop) {
1828
2213
  prop = typeof prop === 'string' ? prop : staticPropertyKey(prop)
1829
- if (prop === 'caller' || prop === 'callee') err('`.caller` and `.callee` are prohibited: deprecated stack introspection')
2214
+ // `.caller`/`.callee` on a function value (or `arguments`) are deprecated
2215
+ // stack introspection — prohibited as bad practice. On a plain data object
2216
+ // they are ordinary field names (e.g. an ESTree call node's `.callee`), so
2217
+ // the ban keys off a known-function receiver, not the bare property name.
2218
+ if ((obj === 'arguments' || hasFunc(obj) || isFuncValueLocal(obj)) && (prop === 'caller' || prop === 'callee'))
2219
+ err('`.caller`/`.callee` are prohibited: deprecated function stack introspection')
1830
2220
  if (prop === 'url' && isImportMeta(obj)) return staticString(importMetaUrl())
2221
+ // A user binding named like a builtin namespace (`let Math = {…}`) shadows it
2222
+ // — read the property off the local value, not the builtin namespace table.
2223
+ if (shadowsBuiltin(obj)) { includeForProperty(prop); return ['.', prep(obj), prop] }
1831
2224
  const mod = ctx.scope.chain[obj]
1832
2225
  // Only treat as module namespace if it's a known built-in module (not a mangled import name)
1833
2226
  if (typeof obj === 'string' && mod && !mod.includes('.') && hasModule(mod)) {
1834
2227
  includeModule(mod)
1835
2228
  const key = mod + '.' + prop
1836
- if (ctx.core.emit[key]?.length > 0) includeForCallableValue()
2229
+ if (emitArity(ctx.core.emit[key]) > 0) includeForCallableValue()
1837
2230
  return key
1838
2231
  }
1839
2232
  // Source module namespace: import * as X → X.prop resolved to mangled name
@@ -1849,7 +2242,22 @@ const handlers = {
1849
2242
  'new'(ctor, ...args) {
1850
2243
  let name = ctor, ctorArgs = args
1851
2244
  if (Array.isArray(ctor) && ctor[0] === '()') { name = ctor[1]; ctorArgs = ctor.slice(2) }
1852
- if (name === 'Date' && ctorArgs.length === 1 && ctorArgs[0] == null) ctorArgs = []
2245
+ // No GC weakness is unobservable, so we fold WeakSet/WeakMap to Set/Map right here:
2246
+ // construction and every .add/.has/.get/.set/.delete reuse the concrete emit path.
2247
+ // The fold lives in prepare, not jzify — so `strict` (which only skips jzify) wouldn't
2248
+ // drop it on its own; we reject explicitly. It's a deviation anyway (accepts primitive
2249
+ // keys, exposes .size/iteration), not a true subset member — there, use Set/Map directly.
2250
+ if (name === 'WeakSet' || name === 'WeakMap') {
2251
+ const concrete = name === 'WeakSet' ? 'Set' : 'Map'
2252
+ if (ctx.transform.strict) err(`strict mode: ${name} is not in the canonical subset — use ${concrete} (jz has no GC, so weak references are unobservable).`)
2253
+ name = concrete
2254
+ }
2255
+ // A lone `null` ctorArg is the parser's no-args sentinel (`new Map()`), and
2256
+ // `new Map(null)`/`new Map(undefined)` are spec-equivalent to it (null/undefined
2257
+ // → empty collection). Drop it so the emit hits the empty-collection fast path
2258
+ // rather than lowering `prep(null)` → `[, 0]` and routing through `__map_from`.
2259
+ // Typed arrays keep the sentinel: there `[, 0]` is a legitimate zero length.
2260
+ if (ctorArgs.length === 1 && ctorArgs[0] == null && (name === 'Date' || COLLECTION_CTORS.includes(name))) ctorArgs = []
1853
2261
  // Flatten comma-grouped args: [',', a, b, c] → [a, b, c]
1854
2262
  if (ctorArgs.length === 1 && Array.isArray(ctorArgs[0]) && ctorArgs[0][0] === ',')
1855
2263
  ctorArgs = ctorArgs[0].slice(1)
@@ -2009,6 +2417,20 @@ function collectReturns(node, out) {
2009
2417
 
2010
2418
  const isLit = n => Array.isArray(n) && n[0] == null
2011
2419
 
2420
+ /** Self-host: pre-parsed module AST for a specifier, or undefined. Linear scan over
2421
+ * [specifier, ast] pairs — array indexing + string `===` are the ABI-safe primitives
2422
+ * the kernel can read off a host-marshalled argument (dynamic-key object reads aren't). */
2423
+ function moduleAstFor(specifier) {
2424
+ const asts = ctx.module.importAsts
2425
+ if (!asts) return undefined
2426
+ for (let i = 0; i < asts.length; i++) if (asts[i][0] === specifier) return asts[i][1]
2427
+ return undefined
2428
+ }
2429
+
2430
+ /** True when `mod` is bundled in-process — as source (host parses it) or as a
2431
+ * pre-parsed AST (self-host kernel). Either path routes through prepareModule. */
2432
+ const isBundledModule = mod => !!ctx.module.importSources?.[mod] || moduleAstFor(mod) !== undefined
2433
+
2012
2434
  /** Compile-time bundling: parse + prepare an imported module, collect exports. */
2013
2435
  function prepareModule(specifier, source) {
2014
2436
  includeModule('core')
@@ -2020,8 +2442,24 @@ function prepareModule(specifier, source) {
2020
2442
 
2021
2443
  ctx.module.moduleStack.push(specifier)
2022
2444
 
2023
- // Name mangling prefix: ./math.jz _math_jz
2024
- const prefix = specifier.replace(/[^a-zA-Z0-9]/g, '_')
2445
+ // Name mangling prefix. Long specifiers (the bundler keys modules by
2446
+ // ABSOLUTE path — 40-60 byte '_Users_…' / '_home_runner_…' prefixes on every
2447
+ // symbol) compact to 'm<N>_<basename>': symbol strings shrink ~4×, which is
2448
+ // a direct hot-path win in the SELF-HOST — watr resolves every `call $name`
2449
+ // and `local.get $name` through name-keyed maps, paying hash+compare per
2450
+ // byte, and shared 35-byte path prefixes defeated the hash-probe early-outs.
2451
+ // Deterministic per compile (registration order); short relative specifiers
2452
+ // keep the readable form.
2453
+ const sanitized = specifier.replace(/[^a-zA-Z0-9]/g, '_')
2454
+ let prefix
2455
+ if (sanitized.length <= 24) prefix = sanitized
2456
+ else {
2457
+ if (!ctx.module.prefixIds) ctx.module.prefixIds = new Map()
2458
+ let id = ctx.module.prefixIds.get(specifier)
2459
+ if (id == null) { id = ctx.module.prefixIds.size; ctx.module.prefixIds.set(specifier, id) }
2460
+ const base = sanitized.replace(/_(js|mjs|jz)$/, '').match(/[a-zA-Z0-9]+$/)?.[0] ?? ''
2461
+ prefix = `m${id}_${base.slice(-16)}`
2462
+ }
2025
2463
 
2026
2464
  // Save caller state
2027
2465
  const savedScope = ctx.scope.chain, savedExports = ctx.func.exports
@@ -2031,8 +2469,18 @@ function prepareModule(specifier, source) {
2031
2469
  ctx.func.exports = {}
2032
2470
  ctx.module.currentPrefix = prefix
2033
2471
 
2034
- // Parse + prepare imported source (may trigger recursive imports)
2035
- let ast = parse(source)
2472
+ try {
2473
+ // Parse + prepare imported source (may trigger recursive imports). The parser
2474
+ // is injected via ctx.transform.parse (the host pipeline sets it) rather than
2475
+ // imported, so prepare carries no hard dependency on a concrete parser — the
2476
+ // same inversion as ctx.transform.jzify. The self-host kernel can't parse, so it
2477
+ // pre-parses the whole graph on the host and passes the ASTs via importAsts;
2478
+ // we consult those first and only parse `source` when no AST was supplied.
2479
+ let ast = moduleAstFor(specifier)
2480
+ if (ast === undefined) {
2481
+ if (!ctx.transform.parse) err('compile-time module bundling requires ctx.transform.parse (injected by the jz pipeline)')
2482
+ ast = ctx.transform.parse(source)
2483
+ }
2036
2484
  if (ctx.transform.jzify) ast = ctx.transform.jzify(ast)
2037
2485
  const savedDepth = depth; depth = 0
2038
2486
  const moduleInit = prep(ast)
@@ -2046,9 +2494,9 @@ function prepareModule(specifier, source) {
2046
2494
  const func = ctx.func.list.find(f => f.name === localName)
2047
2495
  if (func) { renameFunc(func, mangled); func._modulePrefix = prefix }
2048
2496
  if (ctx.scope.globals.has(localName)) {
2049
- const wat = ctx.scope.globals.get(localName).replace(`$${localName}`, `$${mangled}`)
2497
+ // Records carry no name — a rename is a pure Map re-key.
2498
+ ctx.scope.globals.set(mangled, ctx.scope.globals.get(localName))
2050
2499
  ctx.scope.globals.delete(localName)
2051
- ctx.scope.globals.set(mangled, wat)
2052
2500
  if (ctx.scope.userGlobals.has(localName)) { ctx.scope.userGlobals.delete(localName); ctx.scope.userGlobals.add(mangled) }
2053
2501
  if (ctx.scope.globalTypes.has(localName)) { ctx.scope.globalTypes.set(mangled, ctx.scope.globalTypes.get(localName)); ctx.scope.globalTypes.delete(localName) }
2054
2502
  }
@@ -2097,9 +2545,8 @@ function prepareModule(specifier, source) {
2097
2545
  const func = ctx.func.list.find(f => f.name === alias)
2098
2546
  if (func) renameFunc(func, mangled)
2099
2547
  if (ctx.scope.globals.has(alias)) {
2100
- const wat = ctx.scope.globals.get(alias).replace(`$${alias}`, `$${mangled}`)
2548
+ ctx.scope.globals.set(mangled, ctx.scope.globals.get(alias))
2101
2549
  ctx.scope.globals.delete(alias)
2102
- ctx.scope.globals.set(mangled, wat)
2103
2550
  if (ctx.scope.userGlobals.has(alias)) { ctx.scope.userGlobals.delete(alias); ctx.scope.userGlobals.add(mangled) }
2104
2551
  }
2105
2552
  }
@@ -2165,15 +2612,18 @@ function prepareModule(specifier, source) {
2165
2612
  recordModuleInitFacts(moduleInit)
2166
2613
  }
2167
2614
 
2168
- // Restore caller state
2169
- ctx.scope.chain = savedScope
2170
- ctx.func.exports = savedExports
2171
- ctx.module.currentPrefix = savedModulePrefix
2172
- ctx.module.moduleStack.pop()
2173
-
2174
2615
  const result = { exports: moduleExports }
2175
2616
  ctx.module.resolvedModules.set(specifier, result)
2176
2617
  return result
2618
+ } finally {
2619
+ // ALWAYS restore caller state (FE-6): if `prep(ast)` or a recursive import threw
2620
+ // mid-prep, skipping this would leave ctx.scope/exports/prefix/moduleStack
2621
+ // corrupted for the rest of the pipeline.
2622
+ ctx.scope.chain = savedScope
2623
+ ctx.func.exports = savedExports
2624
+ ctx.module.currentPrefix = savedModulePrefix
2625
+ ctx.module.moduleStack.pop()
2626
+ }
2177
2627
  }
2178
2628
 
2179
2629
  // =============================================================================
@@ -2241,7 +2691,7 @@ function tryFusePair(decl, forNode, seq, declIdx) {
2241
2691
  if (!hasAnyIndexedRead(forBody, NAME) && !hasAnyIndexedRead(cond, NAME)) return null
2242
2692
  // `NAME` must not be read after the for-loop in the same block.
2243
2693
  for (let k = declIdx + 2; k < seq.length; k++) {
2244
- if (refsName(seq[k], NAME)) return null
2694
+ if (refsName(seq[k], NAME, { skipArrow: false })) return null
2245
2695
  }
2246
2696
  // RECV must not be reassigned inside the for-loop (would invalidate substitution).
2247
2697
  if (assignsName(forNode, RECV) || assignsName(forNode, NAME)) return null
@@ -2283,12 +2733,6 @@ function hasAnyIndexedRead(n, NAME) {
2283
2733
  for (let i = 1; i < n.length; i++) if (hasAnyIndexedRead(n[i], NAME)) return true
2284
2734
  return false
2285
2735
  }
2286
- function refsName(n, NAME) {
2287
- if (typeof n === 'string') return n === NAME
2288
- if (!Array.isArray(n)) return false
2289
- for (let i = 1; i < n.length; i++) if (refsName(n[i], NAME)) return true
2290
- return false
2291
- }
2292
2736
  function assignsName(n, NAME) {
2293
2737
  if (!Array.isArray(n)) return false
2294
2738
  const op = n[0]