jz 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/prepare.js CHANGED
@@ -24,7 +24,8 @@
24
24
 
25
25
  import { parse } from 'subscript/feature/jessie'
26
26
  import { ctx, err, derive } from './ctx.js'
27
- import { T, STMT_OPS, VAL, valTypeOf, typedElemCtor, extractParams, collectParamNames, classifyParam, observeNodeFacts, staticPropertyKey } from './analyze.js'
27
+ import { T, STMT_OPS, VAL, extractParams, collectParamNames, classifyParam, observeNodeFacts, staticObjectProps, staticPropertyKey } from './analyze.js'
28
+ import { recordGlobalRep } from './infer.js'
28
29
  import { isFuncRef } from './ir.js'
29
30
  import {
30
31
  CTORS, TIMER_NAMES,
@@ -37,6 +38,8 @@ import {
37
38
 
38
39
  let depth = 0 // arrow nesting depth (0=top-level, >0=inside function)
39
40
  let scopes = [] // block scope stack: [{names: Set, renames: Map}]
41
+ let staticConstScopes = [] // lexical const facts: [{strings: Map, arrays: Map}]
42
+ let assignedStaticGlobals = null
40
43
  // Per-arrow set of names already declared anywhere in the function body. Used
41
44
  // to force a rename when the same identifier is declared in two sibling blocks
42
45
  // (else-if arms, separate { ... } chunks): without renaming, both decls lower
@@ -92,6 +95,95 @@ const stringValue = node => Array.isArray(node) && node[0] == null && typeof nod
92
95
  const flatArgs = args => args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',' ? args[0].slice(1) : args
93
96
  const MUTATING_ARRAY_METHODS = new Set(['copyWithin', 'fill', 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'])
94
97
 
98
+ function staticStringArrayValues(expr) {
99
+ if (!Array.isArray(expr) || expr[0] !== '[]' || expr.length !== 2) return null
100
+ const raw = Array.isArray(expr[1]) && expr[1][0] === ',' ? expr[1].slice(1) : [expr[1]]
101
+ const out = []
102
+ for (const item of raw) {
103
+ const s = staticStringExpr(item)
104
+ if (s == null) return null
105
+ out.push(s)
106
+ }
107
+ return out
108
+ }
109
+
110
+ function immediateStaticStringExpr(node) {
111
+ const lit = stringValue(node)
112
+ if (lit != null) return lit
113
+ if (Array.isArray(node) && node[0] === 'str' && typeof node[1] === 'string') return node[1]
114
+ if (!Array.isArray(node)) return null
115
+ const [op, ...args] = node
116
+ if (op === '+') {
117
+ const a = immediateStaticStringExpr(args[0])
118
+ const b = immediateStaticStringExpr(args[1])
119
+ return a != null && b != null ? a + b : null
120
+ }
121
+ if (op === '`') {
122
+ let out = ''
123
+ for (const part of args) {
124
+ const s = immediateStaticStringExpr(part)
125
+ if (s == null) return null
126
+ out += s
127
+ }
128
+ return out
129
+ }
130
+ return null
131
+ }
132
+
133
+ function immediateStaticStringArrayValues(expr) {
134
+ if (!Array.isArray(expr) || expr[0] !== '[]' || expr.length !== 2) return null
135
+ const raw = Array.isArray(expr[1]) && expr[1][0] === ',' ? expr[1].slice(1) : [expr[1]]
136
+ const out = []
137
+ for (const item of raw) {
138
+ const s = immediateStaticStringExpr(item)
139
+ if (s == null) return null
140
+ out.push(s)
141
+ }
142
+ return out
143
+ }
144
+
145
+ function eachTopLevelStatement(node, fn) {
146
+ if (Array.isArray(node) && node[0] === ';') {
147
+ for (let i = 1; i < node.length; i++) fn(node[i])
148
+ } else {
149
+ fn(node)
150
+ }
151
+ }
152
+
153
+ function collectAssignmentWrites(node, writes) {
154
+ if (!Array.isArray(node)) return
155
+ const [op, lhs] = node
156
+ if (op === '=' && typeof lhs === 'string') writes.set(lhs, (writes.get(lhs) || 0) + 1)
157
+ if ((op === '++' || op === '--') && typeof lhs === 'string') writes.set(lhs, (writes.get(lhs) || 0) + 1)
158
+ for (let i = 1; i < node.length; i++) collectAssignmentWrites(node[i], writes)
159
+ }
160
+
161
+ function collectTopLevelStaticAssignments(node, facts) {
162
+ if (!Array.isArray(node)) return
163
+ if (node[0] === ',') {
164
+ for (let i = 1; i < node.length; i++) collectTopLevelStaticAssignments(node[i], facts)
165
+ return
166
+ }
167
+ if (node[0] !== '=' || typeof node[1] !== 'string') return
168
+ const str = immediateStaticStringExpr(node[2])
169
+ const arr = immediateStaticStringArrayValues(node[2])
170
+ if (str != null || arr) facts.set(node[1], { str, arr })
171
+ }
172
+
173
+ function seedStaticGlobalAssignments(node) {
174
+ // jzify hoists function declarations ahead of `var` initializer assignments.
175
+ // Seed one-write static globals before preparing those function bodies so
176
+ // compile-time-only consumers (for example `new RegExp(`${PART}`)`) can still
177
+ // resolve the same constants they would see after module initialization.
178
+ const writes = new Map()
179
+ const facts = new Map()
180
+ collectAssignmentWrites(node, writes)
181
+ eachTopLevelStatement(node, stmt => collectTopLevelStaticAssignments(stmt, facts))
182
+ for (const [name, fact] of facts) {
183
+ if (writes.get(name) === 1) bindStaticGlobal(name, fact.str, fact.arr)
184
+ }
185
+ }
186
+
95
187
  function stringArrayValues(expr) {
96
188
  if (!Array.isArray(expr) || expr[0] !== '[' || expr.length === 1) return null
97
189
  const out = []
@@ -107,6 +199,63 @@ function staticString(value) {
107
199
  return ['str', value]
108
200
  }
109
201
 
202
+ function lookupStaticString(name) {
203
+ const resolved = scopes.length && isDeclared(name) ? resolveScope(name) : (ctx.scope.chain[name] || name)
204
+ for (let i = staticConstScopes.length - 1; i >= 0; i--) {
205
+ const v = staticConstScopes[i].strings.get(resolved)
206
+ if (v != null) return v
207
+ }
208
+ return ctx.scope.shapeStrs?.get(resolved) ?? ctx.scope.constStrs?.get(resolved) ?? null
209
+ }
210
+
211
+ function lookupStaticStringArray(name) {
212
+ const resolved = scopes.length && isDeclared(name) ? resolveScope(name) : (ctx.scope.chain[name] || name)
213
+ for (let i = staticConstScopes.length - 1; i >= 0; i--) {
214
+ const v = staticConstScopes[i].arrays.get(resolved)
215
+ if (v) return v
216
+ }
217
+ return ctx.scope.shapeStrArrays?.get(resolved) ?? null
218
+ }
219
+
220
+ function staticStringExpr(node) {
221
+ const lit = stringValue(node)
222
+ if (lit != null) return lit
223
+ if (Array.isArray(node) && node[0] === 'str' && typeof node[1] === 'string') return node[1]
224
+ if (typeof node === 'string') return lookupStaticString(node)
225
+ if (!Array.isArray(node)) return null
226
+ const [op, ...args] = node
227
+ if (op === '+') {
228
+ const a = staticStringExpr(args[0])
229
+ const b = staticStringExpr(args[1])
230
+ return a != null && b != null ? a + b : null
231
+ }
232
+ if (op === '`') {
233
+ let out = ''
234
+ for (const part of args) {
235
+ const s = staticStringExpr(part)
236
+ if (s == null) return null
237
+ out += s
238
+ }
239
+ return out
240
+ }
241
+ if (op === '``' && Array.isArray(args[0]) && args[0][0] === '.' && args[0][1] === 'String' && args[0][2] === 'raw') {
242
+ let out = ''
243
+ for (const part of args.slice(1)) {
244
+ const s = staticStringExpr(part)
245
+ if (s == null) return null
246
+ out += s
247
+ }
248
+ return out
249
+ }
250
+ if (op === '()' && Array.isArray(args[0]) && args[0][0] === '.' && args[0][2] === 'join' && typeof args[0][1] === 'string') {
251
+ const arr = lookupStaticStringArray(args[0][1])
252
+ if (!arr) return null
253
+ const sep = args.length > 1 && args[1] != null ? staticStringExpr(args[1]) : ','
254
+ return sep != null ? arr.join(sep) : null
255
+ }
256
+ return null
257
+ }
258
+
110
259
  function importMetaUrl() {
111
260
  if (!ctx.transform.importMetaUrl) err('`import.meta.url` requires compile option `importMetaUrl`')
112
261
  return ctx.transform.importMetaUrl
@@ -118,17 +267,6 @@ function resolveImportMeta(spec) {
118
267
  catch { err(`Cannot resolve import.meta specifier '${spec}' from '${base}'`) }
119
268
  }
120
269
 
121
- function recordGlobalValueFact(name, expr) {
122
- if (typeof name !== 'string') return
123
- const vt = valTypeOf(expr)
124
- if (vt) {
125
- ;(ctx.scope.globalValTypes ||= new Map()).set(name, vt)
126
- if (vt === VAL.REGEX && ctx.runtime.regex) ctx.runtime.regex.vars.set(name, expr)
127
- }
128
- const ctor = typedElemCtor(expr)
129
- if (ctor) (ctx.scope.globalTypedElem ||= new Map()).set(name, ctor)
130
- }
131
-
132
270
  function recordModuleInitFacts(root) {
133
271
  const facts = ctx.module.initFacts ||= {
134
272
  dynVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
@@ -181,10 +319,13 @@ function recordModuleInitFacts(root) {
181
319
  export default function prepare(node) {
182
320
  depth = 0
183
321
  scopes = []
322
+ staticConstScopes = []
323
+ assignedStaticGlobals = new Set()
184
324
  funcLocalNames = [new Set()]
185
325
  includeModule('core')
186
326
  normalizeIdents(node)
187
- fuseSparseMapReads(node)
327
+ fuseSparseMapReads(node) // AST-level fusion; needs pre-resolution shape — defined at end of file
328
+ seedStaticGlobalAssignments(node)
188
329
  const ast = prep(node)
189
330
  // Top-level functions referenced as first-class values (e.g. `let o = { fn: g }`,
190
331
  // `arr.push(g)`, `return g`) need trampoline emission, which depends on the fn
@@ -249,7 +390,7 @@ export default function prepare(node) {
249
390
 
250
391
  // Invalidate shapeStrs for any module-level binding that's later assigned to.
251
392
  // shapeStrs is "effectively-const string literals at module scope" — used by
252
- // analyze.js's jsonConstString to enable shape inference on `let SRC = '{...}'`
393
+ // shape.js's jsonConstString to enable shape inference on `let SRC = '{...}'`
253
394
  // patterns (bench convention) without enabling the const-only static fold.
254
395
  // The scan must skip `=` nodes that are children of `let`/`const`/`export` —
255
396
  // those are decl-initializers, not reassignments.
@@ -280,7 +421,7 @@ export default function prepare(node) {
280
421
 
281
422
  // Named constants → numeric literals
282
423
  export const JZ_NULL = Symbol('null')
283
- const CONSTANTS = { 'true': 1, 'false': 0, 'null': JZ_NULL, 'undefined': JZ_NULL }
424
+ const CONSTANTS = { 'true': true, 'false': false, 'null': JZ_NULL, 'undefined': JZ_NULL }
284
425
  // NaN/Infinity stay as special f64 values in emit()
285
426
  const F64_CONSTANTS = { 'NaN': NaN, 'Infinity': Infinity }
286
427
 
@@ -296,6 +437,34 @@ function isDeclared(name) {
296
437
  return scopes.some(s => s.has(name))
297
438
  }
298
439
 
440
+ function pushScope(scope = new Map()) {
441
+ scopes.push(scope)
442
+ staticConstScopes.push({ strings: new Map(), arrays: new Map() })
443
+ }
444
+
445
+ function popScope() {
446
+ scopes.pop()
447
+ staticConstScopes.pop()
448
+ }
449
+
450
+ function bindStaticConst(name, str, arr) {
451
+ const frame = staticConstScopes.at(-1)
452
+ if (!frame || typeof name !== 'string') return
453
+ if (str != null) frame.strings.set(name, str)
454
+ if (arr) frame.arrays.set(name, arr)
455
+ }
456
+
457
+ function bindStaticGlobal(name, str, arr) {
458
+ if (typeof name !== 'string') return
459
+ if (str != null) (ctx.scope.shapeStrs ||= new Map()).set(name, str)
460
+ if (arr) (ctx.scope.shapeStrArrays ||= new Map()).set(name, arr)
461
+ }
462
+
463
+ function deleteStaticGlobal(name) {
464
+ ctx.scope.shapeStrs?.delete(name)
465
+ ctx.scope.shapeStrArrays?.delete(name)
466
+ }
467
+
299
468
  const hasFunc = name => ctx.func.names.has(name)
300
469
 
301
470
  const renameFunc = (func, nextName) => {
@@ -306,33 +475,81 @@ const renameFunc = (func, nextName) => {
306
475
 
307
476
  /** Map JS typeof strings to jz type checks. Codes < 0 trigger specialized emitTypeofCmp paths. */
308
477
  const TYPEOF_MAP = { 'number': -1, 'string': -2, 'undefined': -3, 'boolean': -4, 'object': -5, 'function': -6, 'bigint': -7 }
478
+ // Spec §13.5.3: `typeof undeclared_x` returns 'undefined' without throwing.
479
+ // True iff `name` is a bare identifier with no resolution path. Mirrors the
480
+ // resolution chain inside `prep()` so we don't speculate emit-time failures.
481
+ function isUnresolvableBareIdent(name) {
482
+ if (typeof name !== 'string') return false
483
+ if (name in CONSTANTS || name in F64_CONSTANTS) return false
484
+ if (name === 'Boolean' || name === 'Number') return false
485
+ if (PROHIBITED[name]) return false
486
+ if (scopes.length && isDeclared(name)) return false
487
+ if (ctx.scope.chain[name]) return false
488
+ if (GLOBALS[name]) return false
489
+ if (ctx.func.names.has(name)) return false
490
+ if (ctx.func?.locals?.has?.(name)) return false
491
+ // Top-level decls live in ctx.scope.globals / userGlobals (set by prepDecl at
492
+ // depth 0). Current arrow's local names are tracked in funcLocalNames.
493
+ if (ctx.scope.globals?.has?.(name)) return false
494
+ if (ctx.scope.userGlobals?.has?.(name)) return false
495
+ const fnNames = funcLocalNames[funcLocalNames.length - 1]
496
+ if (fnNames?.has(name)) return false
497
+ return true
498
+ }
309
499
  // Constant fold typeof for known builtin namespaces (e.g. Math.exp). prep(x) resolves Math.exp → 'math.exp'.
310
500
  function staticTypeofString(x) {
501
+ // Spec §13.5.3: unresolvable bare ref → 'undefined'.
502
+ if (isUnresolvableBareIdent(x)) return 'undefined'
311
503
  // Bare callable global: parseInt, parseFloat, isNaN, isFinite, Error, BigInt, etc.
312
504
  if (typeof x === 'string' && !ctx.func?.locals?.has(x) && GLOBALS[x] && ctx.core.emit?.[x]?.length > 0) return 'function'
313
505
  const px = prep(x)
314
506
  if (typeof px === 'string' && px.includes('.') && ctx.core.emit?.[px]?.length > 0) return 'function'
315
507
  return null
316
508
  }
509
+ // Builtin-namespace constructors expose `prototype`/`length`/`name` as own
510
+ // properties; plain namespaces (Math, JSON, Reflect, Atomics) do not.
511
+ const NS_CTORS = new Set(['Number', 'String', 'Boolean', 'BigInt', 'Object',
512
+ 'Array', 'Symbol', 'Error', 'Date', 'RegExp', 'Function', 'Map', 'Set',
513
+ 'Promise', 'ArrayBuffer', 'DataView', 'WeakMap', 'WeakSet'])
514
+ // `NS.hasOwnProperty("member")` is a compile-time question: jz models a
515
+ // builtin namespace as a set of emit keys, so a member is owned iff jz emits
516
+ // it — plus the universal constructor trio for constructor namespaces.
517
+ function namespaceHasOwn(mod, name, member) {
518
+ if (ctx.core.emit[`${mod}.${member}`] != null) return true
519
+ return NS_CTORS.has(name) && (member === 'prototype' || member === 'length' || member === 'name')
520
+ }
317
521
  function resolveTypeof(node) {
318
522
  const [op, a, b] = node
523
+ // `typeof` always yields a string, so `==`/`===` (and `!=`/`!==`) are
524
+ // equivalent here — both collapse to the same type check.
525
+ const eqLike = op === '==' || op === '==='
319
526
  // typeof x == 'string' → type check
320
527
  if (Array.isArray(a) && a[0] === 'typeof' && Array.isArray(b) && b[0] == null && typeof b[1] === 'string') {
321
528
  const known = staticTypeofString(a[1])
322
- if (known != null) return [, op === '==' ? known === b[1] : known !== b[1]]
529
+ if (known != null) return [, eqLike ? known === b[1] : known !== b[1]]
323
530
  const code = TYPEOF_MAP[b[1]]
324
531
  if (code != null) return [op, ['typeof', a[1]], [, code]]
325
532
  }
326
533
  // 'string' == typeof x
327
534
  if (Array.isArray(b) && b[0] === 'typeof' && Array.isArray(a) && a[0] == null && typeof a[1] === 'string') {
328
535
  const known = staticTypeofString(b[1])
329
- if (known != null) return [, op === '==' ? known === a[1] : known !== a[1]]
536
+ if (known != null) return [, eqLike ? known === a[1] : known !== a[1]]
330
537
  const code = TYPEOF_MAP[a[1]]
331
538
  if (code != null) return [op, ['typeof', b[1]], [, code]]
332
539
  }
333
540
  return node
334
541
  }
335
542
 
543
+ // Prepare a strict `===`/`!==`. resolveTypeof may fold `typeof x === 'type'` to a
544
+ // literal or rewrite it to a numeric-code compare; either way we prep the result's
545
+ // operands directly. The strict op stays intact (no collapse to loose `==`) so
546
+ // emit can apply the no-coercion type-mismatch fold.
547
+ function prepStrictEq(op, a, b) {
548
+ const r = resolveTypeof([op, a, b])
549
+ if (r[0] !== op) return prep(r) // folded to a literal — re-prep is safe
550
+ return [op, prep(r[1]), prep(r[2])] // keep strict op; prep operands only
551
+ }
552
+
336
553
  const cloneNode = (node) => {
337
554
  if (!Array.isArray(node)) return node
338
555
  const copy = node.map(cloneNode)
@@ -340,160 +557,40 @@ const cloneNode = (node) => {
340
557
  return copy
341
558
  }
342
559
 
343
- /** Sparse-read .map fusion: rewrite `const b = a.map(arrow); for(...; j<b.length; ...) USE(b[j])`
344
- * into a fused for-loop that inlines `arrow(a[j])` at the read site, eliminating the materialized
345
- * intermediate array. Pre-prep AST mutation; only fires on shapes where every use of `b` is a
346
- * numeric `b[idx]` read or a `b.length` read, the arrow is pure with a single named param, and
347
- * `b` is not referenced after the consumer for-loop. Preserves observable behavior because the
348
- * arrow's pure-expression body has no order-dependent effects. */
349
- function fuseSparseMapReads(root) {
350
- walkSparse(root)
351
- }
352
- function walkSparse(node) {
353
- if (!Array.isArray(node)) return
354
- for (let i = 1; i < node.length; i++) walkSparse(node[i])
355
- if (node[0] === ';') tryFuseInBlock(node)
356
- }
357
- function tryFuseInBlock(seq) {
358
- for (let i = 1; i < seq.length - 1; i++) {
359
- const fused = tryFusePair(seq[i], seq[i + 1], seq, i)
360
- if (fused) {
361
- seq.splice(i, 2, ...fused)
362
- i-- // re-examine same position (chained fusions)
363
- }
364
- }
365
- }
366
- function tryFusePair(decl, forNode, seq, declIdx) {
367
- if (!Array.isArray(decl) || (decl[0] !== 'const' && decl[0] !== 'let')) return null
368
- if (decl.length !== 2) return null // single binding only
369
- const bind = decl[1]
370
- if (!Array.isArray(bind) || bind[0] !== '=' || typeof bind[1] !== 'string') return null
371
- const NAME = bind[1], rhs = bind[2]
372
- if (!Array.isArray(rhs) || rhs[0] !== '()') return null
373
- const callee = rhs[1]
374
- if (!Array.isArray(callee) || callee[0] !== '.' || callee[2] !== 'map') return null
375
- const RECV = callee[1]
376
- if (typeof RECV !== 'string' || RECV === NAME) return null
377
- const arrow = rhs[2]
378
- if (!Array.isArray(arrow) || arrow[0] !== '=>') return null
379
- // Single-name param only: `x => …` or `(x) => …`
380
- const ap = arrow[1]
381
- const PARAM = typeof ap === 'string' ? ap :
382
- (Array.isArray(ap) && ap[0] === '()' && typeof ap[1] === 'string' ? ap[1] : null)
383
- if (!PARAM || PARAM === NAME || PARAM === RECV) return null
384
- // Body: single-expression arrow only (block bodies skipped — could extend later).
385
- const aBody = arrow[2]
386
- if (Array.isArray(aBody) && aBody[0] === '{}') return null
387
- if (!isPureSparseArrowBody(aBody, PARAM)) return null
388
- // For-loop: ['for', [';', initStmt, cond, inc], body]
389
- if (!Array.isArray(forNode) || forNode[0] !== 'for' || forNode.length !== 3) return null
390
- const head = forNode[1]
391
- if (!Array.isArray(head) || head[0] !== ';' || head.length !== 4) return null
392
- const cond = head[2], forBody = forNode[2]
393
- // Verify `NAME` is used only as `NAME[idx]` or `NAME.length` inside cond+forBody.
394
- if (!hasOnlySparseUses(cond, NAME)) return null
395
- if (!hasOnlySparseUses(forBody, NAME)) return null
396
- if (!hasAnyIndexedRead(forBody, NAME) && !hasAnyIndexedRead(cond, NAME)) return null
397
- // `NAME` must not be read after the for-loop in the same block.
398
- for (let k = declIdx + 2; k < seq.length; k++) {
399
- if (refsName(seq[k], NAME)) return null
400
- }
401
- // RECV must not be reassigned inside the for-loop (would invalidate substitution).
402
- if (assignsName(forNode, RECV) || assignsName(forNode, NAME)) return null
403
- // PARAM must not collide with any binding inside forBody (otherwise substitution shadows wrongly).
404
- if (bindsName(forNode, PARAM)) return null
405
- // Apply substitution: NAME.length → RECV.length; NAME[idx] → arrowBody[PARAM ← RECV[idx]].
406
- const newCond = substSparse(cond, NAME, RECV, PARAM, aBody)
407
- const newBody = substSparse(forBody, NAME, RECV, PARAM, aBody)
408
- const newHead = [';', head[1], newCond, head[3]]
409
- return [['for', newHead, newBody]]
410
- }
411
- function isPureSparseArrowBody(n, PARAM) {
412
- if (typeof n === 'string') return true
413
- if (!Array.isArray(n)) return true
414
- const op = n[0]
415
- // Calls / new / assignments / increments are unsafe for repeated-substitution semantics.
416
- if (op === '()' || op === '?.()' || op === 'new' || op === '++' || op === '--') return false
417
- if (op === '=>') return false // nested closure is opaque
418
- if (typeof op === 'string' && op !== '=>' && op !== '===' && op !== '!==' && op !== '==' && op !== '!=' && op !== '<=' && op !== '>=' && op.endsWith('=') && op !== '=') return false
419
- if (op === '=') return false
420
- for (let i = 1; i < n.length; i++) if (!isPureSparseArrowBody(n[i], PARAM)) return false
421
- return true
422
- }
423
- function hasOnlySparseUses(n, NAME) {
424
- if (typeof n === 'string') return n !== NAME
425
- if (!Array.isArray(n)) return true
426
- const op = n[0]
427
- if (op === '[]' && n.length === 3 && n[1] === NAME) return hasOnlySparseUses(n[2], NAME) // NAME[idx] — idx must not reference NAME
428
- if (op === '.' && n[1] === NAME) {
429
- if (n[2] === 'length') return true
430
- return false // any other property access on NAME is opaque
431
- }
432
- for (let i = 1; i < n.length; i++) if (!hasOnlySparseUses(n[i], NAME)) return false
433
- return true
434
- }
435
- function hasAnyIndexedRead(n, NAME) {
436
- if (!Array.isArray(n)) return false
437
- if (n[0] === '[]' && n.length === 3 && n[1] === NAME) return true
438
- for (let i = 1; i < n.length; i++) if (hasAnyIndexedRead(n[i], NAME)) return true
439
- return false
560
+ /** True if `node` contains a `break`/`continue` that belongs to it — i.e. not
561
+ * one nested inside its own function. (Nested loops are intentionally counted:
562
+ * an over-detection only opts into the safe frame-carrying lowering below.) */
563
+ const hasLoopJump = (node) => {
564
+ if (!Array.isArray(node)) return false
565
+ const op = node[0]
566
+ if (op === 'break' || op === 'continue') return true
567
+ if (op === '=>' || op === 'function') return false
568
+ return node.some(hasLoopJump)
440
569
  }
441
- function refsName(n, NAME) {
442
- if (typeof n === 'string') return n === NAME
443
- if (!Array.isArray(n)) return false
444
- for (let i = 1; i < n.length; i++) if (refsName(n[i], NAME)) return true
445
- return false
446
- }
447
- function assignsName(n, NAME) {
448
- if (!Array.isArray(n)) return false
449
- const op = n[0]
450
- if ((op === '=' || op === '++' || op === '--' ||
451
- (typeof op === 'string' && op.endsWith('=') && op !== '==' && op !== '===' && op !== '!=' && op !== '!==' && op !== '<=' && op !== '>='))
452
- && n[1] === NAME) return true
453
- for (let i = 1; i < n.length; i++) if (assignsName(n[i], NAME)) return true
454
- return false
455
- }
456
- function bindsName(n, NAME) {
457
- if (!Array.isArray(n)) return false
458
- const op = n[0]
459
- if ((op === 'let' || op === 'const')) {
460
- for (let i = 1; i < n.length; i++) {
461
- const bind = n[i]
462
- if (Array.isArray(bind) && bind[0] === '=' && bind[1] === NAME) return true
463
- }
464
- }
465
- if (op === '=>') {
466
- const p = n[1]
467
- if (p === NAME) return true
468
- if (Array.isArray(p)) {
469
- if (p[0] === '()' && p[1] === NAME) return true
470
- // skip deeper destructuring forms — conservative
471
- }
472
- }
473
- for (let i = 1; i < n.length; i++) if (bindsName(n[i], NAME)) return true
474
- return false
475
- }
476
- function substSparse(n, NAME, RECV, PARAM, arrowBody) {
477
- if (typeof n !== 'object' || n === null || !Array.isArray(n)) return n
478
- if (n[0] === '.' && n[1] === NAME && n[2] === 'length') return ['.', RECV, 'length']
479
- if (n[0] === '[]' && n.length === 3 && n[1] === NAME) {
480
- const idx = substSparse(n[2], NAME, RECV, PARAM, arrowBody)
481
- return cloneAndBind(arrowBody, PARAM, ['[]', RECV, idx])
482
- }
483
- return n.map((c, i) => i === 0 ? c : substSparse(c, NAME, RECV, PARAM, arrowBody))
484
- }
485
- function cloneAndBind(node, PARAM, replacement) {
486
- if (node === PARAM) return replacement
570
+
571
+ /** Retarget a for-in iteration's *own* unlabeled `break`/`continue` to explicit
572
+ * block labels `break` to the construct-wide label, `continue` to this
573
+ * iteration's label. Nested loops/functions own their jumps and are skipped;
574
+ * labeled jumps already name their target and are left untouched. */
575
+ const retargetLoopJumps = (node, brkLabel, contLabel) => {
487
576
  if (!Array.isArray(node)) return node
488
- return node.map((c, i) => i === 0 ? c : cloneAndBind(c, PARAM, replacement))
577
+ const op = node[0]
578
+ if (op === 'break' && node.length === 1) return ['break', brkLabel]
579
+ if (op === 'continue' && node.length === 1) return ['break', contLabel]
580
+ if (op === 'for' || op === 'for-in' || op === 'while' || op === 'do'
581
+ || op === '=>' || op === 'function') return node
582
+ return node.map(c => retargetLoopJumps(c, brkLabel, contLabel))
489
583
  }
490
584
 
491
585
  function prep(node) {
492
586
  if (Array.isArray(node)) includeForOp(node[0])
493
587
  if (Array.isArray(node) && node.loc != null) ctx.error.loc = node.loc
494
588
  if (node == null) return [, 0] // null/undefined → 0 literal
495
- if (node === true) return [, 1]
496
- if (node === false) return [, 0]
589
+ // Keep boolean identity (was folded to 1/0). The working representation is
590
+ // still i32/f64 0/1 emit lowers the raw boolean — but valTypeOf now reads
591
+ // VAL.BOOL off the literal, so typeof/String/JSON/host boundary stay faithful.
592
+ if (node === true) return [, true]
593
+ if (node === false) return [, false]
497
594
  if (!Array.isArray(node)) {
498
595
  if (typeof node === 'string') {
499
596
  if (node in CONSTANTS) return [, CONSTANTS[node]]
@@ -526,7 +623,11 @@ function prep(node) {
526
623
  return handler ? handler(...args) : [op, ...args.map(prep)]
527
624
  }
528
625
 
529
- // FIXME: can we jzify some of these?
626
+ // Strict-jz prohibitions. `class` and `arguments` are *also* listed here but
627
+ // only reach this point when jzify is off — jzify lowers `class` to a factory
628
+ // arrow and rewrites `arguments` to a rest param before prepare runs. The
629
+ // remainder (`with`, `this`, `super`, `yield`, `eval`) have no safe lowering
630
+ // and stay errors in both modes.
530
631
  const PROHIBITED = { 'with': '`with` not supported', 'class': '`class` not supported', 'yield': '`yield` not supported',
531
632
  'this': '`this` not supported: use explicit parameter',
532
633
  'super': '`super` not supported: no class inheritance',
@@ -552,7 +653,21 @@ export const GLOBALS = {
552
653
  isFinite: 'number',
553
654
  parseInt: 'number',
554
655
  parseFloat: 'number',
656
+ encodeURIComponent: 'encodeURIComponent',
657
+ decodeURIComponent: 'decodeURIComponent',
555
658
  Error: 'Error',
659
+ // Error subclasses: distinct names in JS, but jz doesn't carry typed error
660
+ // info — `throw` accepts any value and stringification goes through the
661
+ // host. Treat them all as Error-shaped passthrough constructors so user
662
+ // code that throws specific subclasses (`throw new SyntaxError(msg)`) compiles
663
+ // identically. If we ever model `instanceof SyntaxError`, this is where to
664
+ // distinguish them; until then the surfaced message is what matters.
665
+ TypeError: 'Error',
666
+ SyntaxError: 'Error',
667
+ RangeError: 'Error',
668
+ ReferenceError: 'Error',
669
+ URIError: 'Error',
670
+ EvalError: 'Error',
556
671
  BigInt: 'BigInt',
557
672
  TextEncoder: 'TextEncoder',
558
673
  TextDecoder: 'TextDecoder',
@@ -589,6 +704,27 @@ function scalarArrayDestruct(pattern, rhs) {
589
704
  return prep([';', ['let', ...decls], ...assigns])
590
705
  }
591
706
 
707
+ function declareGlobal(name, user = true) {
708
+ if (depth !== 0 || typeof name !== 'string') return name
709
+ if (ctx.scope.globals.has(name)) err(`'${name}' conflicts with a compiler internal — choose a different name`)
710
+ ctx.scope.globals.set(name, `(global $${name} (mut f64) (f64.const 0))`)
711
+ if (user) ctx.scope.userGlobals.add(name)
712
+ return name
713
+ }
714
+
715
+ function bindingNames(pattern, out = new Set()) {
716
+ if (typeof pattern === 'string') out.add(pattern)
717
+ else if (Array.isArray(pattern)) {
718
+ if (pattern[0] === '...' && typeof pattern[1] === 'string') out.add(pattern[1])
719
+ else if (pattern[0] === '=') bindingNames(pattern[1], out)
720
+ else if (pattern[0] === ':') bindingNames(pattern[2], out)
721
+ else if (pattern[0] === '[]' || pattern[0] === '{}' || pattern[0] === ',') {
722
+ for (const item of pattern.slice(1)) bindingNames(item, out)
723
+ }
724
+ }
725
+ return out
726
+ }
727
+
592
728
  function pushPatternAssign(target, valueExpr, out, decls = null) {
593
729
  if (Array.isArray(target) && target[0] === '=') {
594
730
  pushPatternAssign(target[1], ['??', valueExpr, prep(target[2])], out, decls)
@@ -703,19 +839,51 @@ function prepDecl(op, ...inits) {
703
839
  if (ctx.scope.globals.has(declName)) err(`'${declName}' conflicts with a compiler internal — choose a different name`)
704
840
  ctx.scope.globals.set(declName, `(global $${declName} (mut f64) (f64.const 0))`)
705
841
  ctx.scope.userGlobals.add(declName)
842
+ } else if (typeof declName === 'string') {
843
+ // Bare hoisted decl inside a function (var X jzified to `let X` at top
844
+ // of arrow + a later `X = …` assignment). Without registering here, the
845
+ // name is invisible to scope predicates like `isUnresolvableBareIdent`
846
+ // until the assignment runs — which is after any reference to it.
847
+ const fnNames = funcLocalNames[funcLocalNames.length - 1]
848
+ if (fnNames) fnNames.add(declName)
849
+ if (scopes.length > 0) scopes[scopes.length - 1].set(declName, declName)
706
850
  }
707
851
  rest.push(declName)
708
852
  continue
709
853
  }
710
- const [, name, init] = i, normed = prep(init)
854
+ const [, name, init] = i
855
+ const staticStr = op === 'const' ? staticStringExpr(init) : null
856
+ const staticArr = op === 'const' ? staticStringArrayValues(init) : null
857
+ const normed = prep(init)
711
858
 
712
859
  if (isDestructPattern(name)) {
860
+ // Register each binding both as a module global (depth 0) and in the
861
+ // current arrow's local scope (depth ≠ 0). Without the local registration
862
+ // the name is invisible to `isUnresolvableBareIdent`, so a later
863
+ // `typeof x` would mis-fold to 'undefined' (spec §13.5.3) before emit ever
864
+ // sees the binding — see the bare-hoisted-decl branch above for the same fix.
865
+ const fnNames = funcLocalNames[funcLocalNames.length - 1]
866
+ for (const n of bindingNames(name)) {
867
+ declareGlobal(n)
868
+ if (depth !== 0 && typeof n === 'string') {
869
+ if (fnNames) fnNames.add(n)
870
+ if (scopes.length > 0) scopes[scopes.length - 1].set(n, n)
871
+ }
872
+ }
873
+ // A bare-identifier source needs no temp: reads are idempotent and
874
+ // side-effect-free, so we destructure straight off it. This keeps each
875
+ // element's static type tag (e.g. `let [, x] = strs` resolves `x` to the
876
+ // same STRING that `strs[1]` would) — a copy temp drops the array's
877
+ // element-type shape and `typeof x` would degrade to 'undefined'.
878
+ if (typeof normed === 'string') {
879
+ expandDestruct(name, normed, rest)
880
+ continue
881
+ }
713
882
  const tmp = `${T}d${ctx.func.uniq++}`
883
+ declareGlobal(tmp, false)
714
884
  rest.push(['=', tmp, normed])
715
885
  // Propagate schema to temp so rest destructuring can resolve it
716
- if (typeof normed === 'string' && ctx.schema.vars.has(normed))
717
- ctx.schema.vars.set(tmp, ctx.schema.vars.get(normed))
718
- else if (Array.isArray(normed) && normed[0] === '{}') {
886
+ if (Array.isArray(normed) && normed[0] === '{}') {
719
887
  const p = normed.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
720
888
  if (p.length) ctx.schema.vars.set(tmp, ctx.schema.register(p))
721
889
  }
@@ -737,18 +905,19 @@ function prepDecl(op, ...inits) {
737
905
  scopes[scopes.length - 1].set(name, name)
738
906
  }
739
907
  if (typeof declName === 'string' && fnNames) fnNames.add(declName)
908
+ if (op === 'const') bindStaticConst(declName, staticStr, staticArr)
740
909
  // Track const for reassignment checks — only module-scope consts (depth 0)
741
910
  if (typeof declName === 'string' && depth === 0) {
742
911
  if (ctx.module.currentPrefix) {
743
912
  declName = `${ctx.module.currentPrefix}$${declName}`
744
913
  ctx.scope.chain[name] = declName
745
914
  }
915
+ if (op === 'const') bindStaticGlobal(declName, staticStr, staticArr)
746
916
  if (op === 'const') {
747
917
  if (!ctx.scope.consts) ctx.scope.consts = new Set()
748
918
  ctx.scope.consts.add(declName)
749
- if (Array.isArray(normed) && normed[0] === 'str' && typeof normed[1] === 'string')
750
- (ctx.scope.constStrs ||= new Map()).set(declName, normed[1])
751
- const strs = stringArrayValues(normed)
919
+ if (staticStr != null) (ctx.scope.constStrs ||= new Map()).set(declName, staticStr)
920
+ const strs = staticArr || stringArrayValues(normed)
752
921
  if (strs) (ctx.scope.shapeStrArrays ||= new Map()).set(declName, strs)
753
922
  } else if (op === 'let' && ctx.scope.consts?.has(declName)) {
754
923
  ctx.scope.consts.delete(declName)
@@ -761,7 +930,7 @@ function prepDecl(op, ...inits) {
761
930
  // entry whose name is later assigned to.
762
931
  if (Array.isArray(normed) && normed[0] === 'str' && typeof normed[1] === 'string')
763
932
  (ctx.scope.shapeStrs ||= new Map()).set(declName, normed[1])
764
- recordGlobalValueFact(declName, normed)
933
+ recordGlobalRep(declName, normed)
765
934
  }
766
935
  // Track object schemas (after prefix so schema is keyed to final name)
767
936
  if (typeof declName === 'string' && Array.isArray(normed) && normed[0] === '{}' && normed.length > 1) {
@@ -788,6 +957,119 @@ function prepDecl(op, ...inits) {
788
957
  return rest.length ? [op, ...rest] : null
789
958
  }
790
959
 
960
+ // --- `'()'` call-handler helpers --------------------------------------------
961
+ // The call handler is a thin dispatcher: it tries the compile-time folds
962
+ // below (each gated by callee shape, so at most one fires), then resolves the
963
+ // callee, then assembles the call. Each helper moves one concern out of line.
964
+
965
+ // `import.meta.resolve("spec")` → the resolved URL as a static string.
966
+ function foldImportMetaResolve(callee, args) {
967
+ if (!isImportMetaProp(callee, 'resolve')) return undefined
968
+ const callArgs = flatArgs(args).filter(a => a != null)
969
+ if (callArgs.length !== 1) err('`import.meta.resolve` requires one string literal argument')
970
+ const spec = stringValue(callArgs[0])
971
+ if (spec == null) err('`import.meta.resolve` supports only string literal arguments')
972
+ return staticString(resolveImportMeta(spec))
973
+ }
974
+
975
+ // String-callee constructor / named-builtin folds: `Array(n)` and the `CTORS`
976
+ // set redirect to the `new` handler; `BigInt64Array`/`BigUint64Array` build a
977
+ // direct module call. `includeForNamedCall` is probed for every string callee
978
+ // — that probe is also how a module-backed builtin gets its modules included.
979
+ // Returns the replacement IR, or `undefined` for an ordinary call.
980
+ function dispatchConstructorCall(callee, args) {
981
+ if (typeof callee !== 'string') return undefined
982
+ if (callee === 'Array') {
983
+ const callArgs = flatArgs(args).filter(a => a != null)
984
+ if (callArgs.length === 1) return handlers['new'](['()', callee, callArgs[0]])
985
+ }
986
+ if (CTORS.includes(callee)) return handlers['new'](['()', callee, ...args])
987
+ if (includeForNamedCall(callee) && (callee === 'BigInt64Array' || callee === 'BigUint64Array'))
988
+ return ['()', callee, ...args.filter(a => a != null).map(prep)]
989
+ return undefined
990
+ }
991
+
992
+ // Compile-time namespace introspection on a `obj.prop(...)` callee:
993
+ // `Array.isArray(NS)` on a bare builtin global folds to `false` (a namespace
994
+ // value is never an array); `NS.hasOwnProperty("member")` on a builtin
995
+ // namespace folds to a literal — no runtime namespace object. Returns the
996
+ // folded literal IR, or `undefined` when nothing folds.
997
+ function foldNamespaceIntrospection(callee, args) {
998
+ if (!Array.isArray(callee) || callee[0] !== '.') return undefined
999
+ const [, obj, prop] = callee
1000
+ if (obj === 'Array' && prop === 'isArray') {
1001
+ const cargs = flatArgs(args).filter(a => a != null)
1002
+ const a0 = cargs.length === 1 ? cargs[0] : null
1003
+ if (typeof a0 === 'string' && GLOBALS[a0] && !(scopes.length && isDeclared(a0)) && !hasFunc(a0))
1004
+ return [, 0]
1005
+ }
1006
+ if (prop === 'hasOwnProperty' && typeof obj === 'string' && !(scopes.length && isDeclared(obj))) {
1007
+ const mod = ctx.scope.chain[obj]
1008
+ if (mod && !mod.includes('.') && hasModule(mod)) {
1009
+ const cargs = flatArgs(args).filter(a => a != null)
1010
+ const member = cargs.length === 1 ? stringValue(cargs[0]) : null
1011
+ // Include the module so its emit keys (the namespace's member set) are
1012
+ // registered; unreferenced emitters/data dead-strip in compile.
1013
+ if (member != null) { includeModule(mod); return [, namespaceHasOwn(mod, obj, member) ? 1 : 0] }
1014
+ }
1015
+ }
1016
+ return undefined
1017
+ }
1018
+
1019
+ // Resolve a callee to its lowered form, triggering module autoloads along the
1020
+ // way: a bare identifier through the scope chain, an `obj.prop` member call
1021
+ // through host imports / named-call / generic-method / namespace tables, and
1022
+ // any other expression through `prep` (a callable runtime value).
1023
+ function resolveCallee(callee, args) {
1024
+ if (typeof callee === 'string') {
1025
+ const local = scopes.length && isDeclared(callee)
1026
+ const resolved = local ? null : ctx.scope.chain[callee]
1027
+ if (local) return resolveScope(callee)
1028
+ if (resolved?.includes('.')) return resolved
1029
+ if (resolved && hasFunc(resolved)) return resolved
1030
+ if (resolved && !resolved.includes('.')) {
1031
+ if (hasModule(resolved) && !ctx.module.imports.some(i => i[3]?.[1] === `$${resolved}`)) includeModule(resolved)
1032
+ return callee
1033
+ }
1034
+ if (depth > 0 && !resolved && !ctx.func.exports[callee] && !ctx.module.imports.some(i => i[3]?.[1] === `$${callee}`))
1035
+ includeForCallableValue()
1036
+ return callee
1037
+ }
1038
+ if (Array.isArray(callee) && callee[0] === '.') {
1039
+ const [, obj, prop] = callee
1040
+ const key = typeof obj === 'string' && typeof prop === 'string' ? `${obj}.${prop}` : null
1041
+ if (key && ctx.module.hostImports?.[obj]?.[prop]) {
1042
+ const spec = ctx.module.hostImports[obj][prop]
1043
+ const alias = `${obj}$${prop}`
1044
+ addHostImport(obj, prop, alias, spec)
1045
+ return alias
1046
+ }
1047
+ if (key && includeForNamedCall(key)) return key
1048
+ if (includeForGenericMethod(prop)) return prep(callee)
1049
+ const mod = ctx.scope.chain[obj]
1050
+ if (typeof obj === 'string' && mod && !mod.includes('.') && hasModule(mod))
1051
+ return (includeModule(mod), mod + '.' + prop)
1052
+ return prep(callee)
1053
+ }
1054
+ includeForCallableValue()
1055
+ return prep(callee)
1056
+ }
1057
+
1058
+ // A lone parenthesized comma-expression argument — `f((a, b, c))` — is ONE
1059
+ // argument whose value is the last comma operand. The parser keeps it wrapped
1060
+ // (`['()', [',', …]]`); prep would strip the grouping, leaving a bare comma
1061
+ // that emit can no longer tell apart from an arg list and splats into N args.
1062
+ // With ≥2 args an outer arg-list comma already nests it — only the sole-arg
1063
+ // case loses the distinction. Re-nest it under a 1-element arg-list comma.
1064
+ function renestSoleCommaArg(args) {
1065
+ if (args.length === 1 && Array.isArray(args[0]) && args[0][0] === '()' && args[0].length === 2) {
1066
+ const ungroup = n => Array.isArray(n) && n[0] === '()' && n.length === 2 ? ungroup(n[1]) : n
1067
+ const core = ungroup(args[0])
1068
+ if (Array.isArray(core) && core[0] === ',') return [[',', args[0]]]
1069
+ }
1070
+ return args
1071
+ }
1072
+
791
1073
  const handlers = {
792
1074
  // Spread operator: [...expr] in arrays, f(...args) in calls, {...obj} in objects
793
1075
  '...'(expr) {
@@ -802,11 +1084,23 @@ const handlers = {
802
1084
  'class': () => err('class not supported: use object literals'),
803
1085
  'yield': () => err('generators not supported: use loops'),
804
1086
  'debugger': () => null,
805
- 'delete': () => err('delete not supported: object shape is fixed'),
1087
+ // Static-key delete (.x, ["x"], [literal]) would change the fixed schema reject.
1088
+ // Computed-key delete (obj[expr]) — including jessie's `delete ctx[k]` — lowers
1089
+ // to runtime __dyn_del against the per-object shadow property store.
1090
+ 'delete'(target) {
1091
+ const t = prep(target)
1092
+ if (Array.isArray(t) && t[0] === '[]' && t.length === 3) {
1093
+ const key = t[2]
1094
+ const isLiteralKey = Array.isArray(key) && key[0] == null && key.length === 2
1095
+ if (!isLiteralKey) return ['delete', t[1], key]
1096
+ }
1097
+ err('delete not supported: object shape is fixed')
1098
+ },
806
1099
  'in'(key, obj) { return ['in', prep(key), prep(obj)] },
807
1100
  'instanceof': () => err('instanceof not supported: use typeof'),
808
1101
  'with': () => err('`with` not supported: deprecated'),
809
1102
  ':': () => err('labeled statements not supported'),
1103
+ 'label'(name, body) { return ['label', name, prep(body)] },
810
1104
  'var': () => err('`var` not supported: use let/const'),
811
1105
  'function': () => err('`function` not supported: use arrow functions'),
812
1106
 
@@ -828,23 +1122,60 @@ const handlers = {
828
1122
  expandDestruct(lhs, tmp, stmts, decls)
829
1123
  return prep([';', ['let', ...decls], ...stmts])
830
1124
  }
831
- // Parser ambiguity: }[pattern] = rhs mis-parsed as subscript when it's stmt; [pattern] = rhs
832
- // Detect: ['[]', stmtExpr, commaExpr] with spread in comma → split into stmt + destructuring
833
- if (Array.isArray(lhs) && lhs[0] === '[]' && lhs.length === 3) {
834
- const hasSpr = n => Array.isArray(n) && (n[0] === '...' || n.some(hasSpr))
835
- if (hasSpr(lhs[2])) {
836
- const preStmt = lhs[1]
837
- const pattern = ['[]', lhs[2]]
838
- return prep([';', preStmt, ['=', pattern, rhs]])
1125
+ // Function property assignment: fn.prop = arrow extract as top-level function fn$prop.
1126
+ // A property can be reassigned esbuild/jessie wrapper-composition does
1127
+ // `p.s = ...; var old = p.s; p.s = () => old()...`. Each assignment extracts
1128
+ // its own top-level function; the property holds whichever was assigned last,
1129
+ // and an earlier snapshot keeps pointing at the prior one. Collide → fresh name.
1130
+ // The base resolves through the scope chain first so an *imported* function
1131
+ // (mangled to `_mod$fn`) is recognised the same as a local one — the
1132
+ // subscript parser's plugin model mutates `parse.step` etc. across modules,
1133
+ // and a reassignment in module B must mark module A's call sites mutable.
1134
+ if (depth === 0 && Array.isArray(lhs) && lhs[0] === '.' && typeof lhs[1] === 'string'
1135
+ && Array.isArray(rhs) && rhs[0] === '=>') {
1136
+ const fnBase = ctx.scope.chain[lhs[1]] || lhs[1]
1137
+ if (hasFunc(fnBase)) {
1138
+ let name = `${fnBase}$${lhs[2]}`
1139
+ // Reassignment → the property is mutable; record it so `fn.prop()` calls
1140
+ // emit a dynamic property read + indirect call instead of a direct call.
1141
+ if (ctx.func.names.has(name)) {
1142
+ ctx.func.multiProp.add(`${fnBase}.${lhs[2]}`)
1143
+ do name = `${fnBase}$${lhs[2]}$${ctx.func.uniq++}`; while (ctx.func.names.has(name))
1144
+ }
1145
+ // Build the target `.` node directly from the resolved base — re-`prep`ing
1146
+ // the lhs would resolve a multiProp `fn.prop` to an rvalue (closure
1147
+ // materialization block), which is not a valid assignment target.
1148
+ if (defFunc(name, prep(rhs))) return ['=', ['.', fnBase, lhs[2]], name]
1149
+ }
1150
+ }
1151
+ const staticStr = staticStringExpr(rhs)
1152
+ const staticArr = staticStringArrayValues(rhs)
1153
+ const plhs = prep(lhs)
1154
+ const prhs = prep(rhs)
1155
+ if (depth === 0 && typeof plhs === 'string' && ctx.scope.globals.has(plhs)) {
1156
+ // First assignment fixes the global's representation + object schema.
1157
+ if (!ctx.scope.globalReps?.has(plhs)) {
1158
+ recordGlobalRep(plhs, prhs)
1159
+ if (Array.isArray(prhs) && prhs[0] === '{}') {
1160
+ const props = staticObjectProps(prhs.slice(1))
1161
+ if (props) ctx.schema.vars.set(plhs, ctx.schema.register(props.names))
1162
+ }
839
1163
  }
1164
+ // Static string/array facts hold only while every assignment is constant.
1165
+ if (!assignedStaticGlobals.has(plhs) && (staticStr != null || staticArr)) bindStaticGlobal(plhs, staticStr, staticArr)
1166
+ else deleteStaticGlobal(plhs)
1167
+ assignedStaticGlobals.add(plhs)
840
1168
  }
841
- // Function property assignment: fn.prop = arrow extract as top-level function fn$prop
842
- if (depth === 0 && Array.isArray(lhs) && lhs[0] === '.' && typeof lhs[1] === 'string'
843
- && hasFunc(lhs[1]) && Array.isArray(rhs) && rhs[0] === '=>') {
844
- const name = `${lhs[1]}$${lhs[2]}`
845
- if (defFunc(name, prep(rhs))) return ['=', prep(lhs), name]
1169
+ // Local object-literal assignment to a not-yet-shaped variable e.g. a `var`
1170
+ // that jzify hoisted into `let x; x = {…}`. Recording the schema here lets the
1171
+ // binding behave like `let x = {…}`: fixed-slot field access and for-in unroll.
1172
+ // First assignment fixes the shape (mirrors the global rule above).
1173
+ else if (typeof plhs === 'string' && Array.isArray(prhs) && prhs[0] === '{}'
1174
+ && !ctx.schema.vars.has(plhs)) {
1175
+ const props = staticObjectProps(prhs.slice(1))
1176
+ if (props) ctx.schema.vars.set(plhs, ctx.schema.register(props.names))
846
1177
  }
847
- return ['=', prep(lhs), prep(rhs)]
1178
+ return ['=', plhs, prhs]
848
1179
  },
849
1180
 
850
1181
  // try/catch/throw
@@ -877,16 +1208,14 @@ const handlers = {
877
1208
  },
878
1209
 
879
1210
  // Tagged template: tag`a${x}b` → tag(['a','b'], x)
880
- // Parser drops empty string segments; reinsert them to satisfy the strings.length === exprs.length + 1 invariant.
881
1211
  '``'(tag, ...parts) {
1212
+ const raw = staticStringExpr(['``', tag, ...parts])
1213
+ if (raw != null) return staticString(raw)
882
1214
  const strs = [], exprs = []
883
- let prev = false
884
1215
  for (const p of parts) {
885
- const isStr = Array.isArray(p) && p[0] == null && typeof p[1] === 'string'
886
- if (isStr) { strs.push(p); prev = true }
887
- else { if (!prev) strs.push([null, '']); exprs.push(p); prev = false }
1216
+ if (Array.isArray(p) && p[0] == null && typeof p[1] === 'string') strs.push(p)
1217
+ else exprs.push(p)
888
1218
  }
889
- if (!prev) strs.push([null, ''])
890
1219
  const arr = strs.length === 1 ? ['[]', strs[0]] : ['[]', [',', ...strs]]
891
1220
  const callArgs = exprs.length === 0 ? arr : [',', arr, ...exprs]
892
1221
  return prep(['()', tag, callArgs])
@@ -894,6 +1223,9 @@ const handlers = {
894
1223
 
895
1224
  // Import
896
1225
  'import'(fromNode) {
1226
+ // Bare side-effect: `import './sub.js'` → AST is ['import', [null, 'path']]
1227
+ if (Array.isArray(fromNode) && fromNode[0] == null && typeof fromNode[1] === 'string')
1228
+ return handlers['from'](null, fromNode)
897
1229
  if (!Array.isArray(fromNode) || fromNode[0] !== 'from')
898
1230
  return err('Dynamic import() not supported')
899
1231
  return handlers['from'](fromNode[1], fromNode[2])
@@ -1010,9 +1342,13 @@ const handlers = {
1010
1342
  err(`Unknown module '${mod}'. Provide it via { modules: { '${mod}': source } } or { imports: { '${mod}': {...} } }`)
1011
1343
  },
1012
1344
 
1013
- // === is == in jz (all comparisons are strict). Also handle typeof x === 'type' patterns.
1014
- '==='(a, b) { return prep(resolveTypeof(['==', a, b])) },
1015
- '!=='(a, b) { return prep(resolveTypeof(['!=', a, b])) },
1345
+ // `===`/`!==` keep strict semantics (no coercion); emit folds a statically-known
1346
+ // type mismatch to false and otherwise shares the loose `==`/`!=` same-type path.
1347
+ // resolveTypeof still collapses `typeof x === 'type'` to a compile-time check.
1348
+ // Prep operands directly (not via `prep` on the node) so the strict op survives
1349
+ // to emit instead of re-dispatching this handler forever.
1350
+ '==='(a, b) { return prepStrictEq('===', a, b) },
1351
+ '!=='(a, b) { return prepStrictEq('!==', a, b) },
1016
1352
 
1017
1353
  // Statements
1018
1354
  ';': (...stmts) => [';', ...stmts.map(prep).filter(x => x != null)],
@@ -1022,13 +1358,13 @@ const handlers = {
1022
1358
  // Block-scoped control flow: push scope for bodies so inner let/const shadows correctly
1023
1359
  'if': (cond, then, els) => {
1024
1360
  const c = prep(cond)
1025
- scopes.push(new Map()); const t = prep(then); scopes.pop()
1026
- if (els != null) { scopes.push(new Map()); const e = prep(els); scopes.pop(); return ['if', c, t, e] }
1361
+ pushScope(); const t = prep(then); popScope()
1362
+ if (els != null) { pushScope(); const e = prep(els); popScope(); return ['if', c, t, e] }
1027
1363
  return ['if', c, t]
1028
1364
  },
1029
1365
  'while': (cond, body) => {
1030
1366
  const c = prep(cond)
1031
- scopes.push(new Map()); const b = prep(body); scopes.pop()
1367
+ pushScope(); const b = prep(body); popScope()
1032
1368
  return ['while', c, b]
1033
1369
  },
1034
1370
 
@@ -1111,7 +1447,7 @@ const handlers = {
1111
1447
  for (const n of collectParamNames(raw)) fnScope.set(n, n)
1112
1448
 
1113
1449
  depth++
1114
- scopes.push(fnScope)
1450
+ pushScope(fnScope)
1115
1451
  funcLocalNames.push(new Set(collectParamNames(raw)))
1116
1452
 
1117
1453
  const nextParams = []
@@ -1133,6 +1469,14 @@ const handlers = {
1133
1469
  }
1134
1470
  }
1135
1471
  let preparedBody = prep(body)
1472
+ // An expression-bodied arrow returning an empty object literal — `() => ({})`
1473
+ // — preps to a bare `['{}']`, structurally identical to an empty block body.
1474
+ // The grouping `()` that marked it an expression is unwrapped by then, so
1475
+ // wrap it in an explicit `return` — otherwise downstream block/expression
1476
+ // classification (compile.js `isBlockBody`) misreads it as an empty block.
1477
+ if (!(Array.isArray(body) && body[0] === '{}')
1478
+ && Array.isArray(preparedBody) && preparedBody[0] === '{}' && preparedBody.length === 1)
1479
+ preparedBody = ['{}', [';', ['return', ['{}']]]]
1136
1480
  if (bodyPrefix.length) {
1137
1481
  const prefix = bodyPrefix.filter(x => x != null)
1138
1482
  if (Array.isArray(preparedBody) && preparedBody[0] === '{}' && Array.isArray(preparedBody[1]) && preparedBody[1][0] === ';')
@@ -1144,7 +1488,7 @@ const handlers = {
1144
1488
  }
1145
1489
  const inner = nextParams.length === 0 ? null : nextParams.length === 1 ? nextParams[0] : [',', ...nextParams]
1146
1490
  const result = ['=>', Array.isArray(params) && params[0] === '()' ? ['()', inner] : inner, preparedBody]
1147
- scopes.pop()
1491
+ popScope()
1148
1492
  funcLocalNames.pop()
1149
1493
  depth--
1150
1494
  return result
@@ -1176,6 +1520,8 @@ const handlers = {
1176
1520
  return ['?.()', prep(callee), ...items.map(prep)]
1177
1521
  },
1178
1522
  // Boolean literals NaN-box as f64 — typeof at runtime returns 'number'. Fold here so the JS-spec value survives.
1523
+ // Unresolvable bare refs fold to 'undefined' via staticTypeofString (spec §13.5.3) —
1524
+ // the only place a stray identifier doesn't ReferenceError.
1179
1525
  'typeof'(a) {
1180
1526
  if (Array.isArray(a) && a[0] == null && typeof a[1] === 'boolean') { includeForStringOnly(); return ['str', 'boolean'] }
1181
1527
  const known = staticTypeofString(a)
@@ -1191,7 +1537,17 @@ const handlers = {
1191
1537
  includeForNumericCoercion()
1192
1538
  return ['u+', na]
1193
1539
  }
1194
- return ['+', prep(a), prep(b)]
1540
+ const pa = prep(a), pb = prep(b)
1541
+ // Compile-time fold of literal string concat. The combined bytes flow
1542
+ // through the `str` emitter as a single literal — SSO if ≤4 ASCII (zero
1543
+ // heap), otherwise one dataDedup entry (still cheaper than runtime
1544
+ // __str_concat_raw + heap alloc). Bottom-up, so `'a' + 'b' + 'c'` folds
1545
+ // left-associatively into one literal.
1546
+ if (Array.isArray(pa) && pa[0] === 'str' && typeof pa[1] === 'string' &&
1547
+ Array.isArray(pb) && pb[0] === 'str' && typeof pb[1] === 'string') {
1548
+ return ['str', pa[1] + pb[1]]
1549
+ }
1550
+ return ['+', pa, pb]
1195
1551
  },
1196
1552
  '-'(a, b) {
1197
1553
  if (b === undefined) { const na = prep(a); return isLit(na) && typeof na[1] === 'number' ? [, -na[1]] : ['u-', na] }
@@ -1226,73 +1582,18 @@ const handlers = {
1226
1582
  '()'(callee, ...args) {
1227
1583
  // Grouping: (expr) → ['()', expr] with no args. Call: f() → ['()', 'f', null] with null arg.
1228
1584
  if (args.length === 0) return prep(callee)
1585
+ if (typeof callee === 'string' && PROHIBITED[callee]) err(PROHIBITED[callee])
1229
1586
 
1230
- if (isImportMetaProp(callee, 'resolve')) {
1231
- const callArgs = flatArgs(args).filter(a => a != null)
1232
- if (callArgs.length !== 1) err('`import.meta.resolve` requires one string literal argument')
1233
- const spec = stringValue(callArgs[0])
1234
- if (spec == null) err('`import.meta.resolve` supports only string literal arguments')
1235
- return staticString(resolveImportMeta(spec))
1236
- }
1587
+ // Compile-time folds: the callee names something resolvable now. Each fold
1588
+ // is gated by callee shape, so at most one of the three fires.
1589
+ const folded = foldImportMetaResolve(callee, args)
1590
+ ?? dispatchConstructorCall(callee, args)
1591
+ ?? foldNamespaceIntrospection(callee, args)
1592
+ if (folded !== undefined) return folded
1237
1593
 
1238
- const hasRealArgs = args.some(a => a != null)
1239
-
1240
- if (typeof callee === 'string') {
1241
- if (PROHIBITED[callee]) err(PROHIBITED[callee])
1242
- if (CTORS.includes(callee)) return handlers['new'](['()', callee, ...args])
1243
-
1244
- if (includeForNamedCall(callee)) {
1245
- if (callee === 'BigInt64Array' || callee === 'BigUint64Array') {
1246
- return ['()', callee, ...args.filter(a => a != null).map(prep)]
1247
- }
1248
- }
1249
-
1250
- const local = scopes.length && isDeclared(callee)
1251
- const resolved = local ? null : ctx.scope.chain[callee]
1252
- if (local) callee = resolveScope(callee)
1253
- else if (resolved?.includes('.')) callee = resolved
1254
- else if (resolved && hasFunc(resolved)) callee = resolved
1255
- else if (resolved && !resolved.includes('.')) {
1256
- if (hasModule(resolved) && !ctx.module.imports.some(i => i[3]?.[1] === `$${resolved}`)) includeModule(resolved)
1257
- }
1258
- else if (depth > 0 && !resolved && !ctx.func.exports[callee] && !ctx.module.imports.some(i => i[3]?.[1] === `$${callee}`)) {
1259
- includeForCallableValue()
1260
- }
1261
- } else if (Array.isArray(callee) && callee[0] === '.') {
1262
- const [, obj, prop] = callee
1263
- const key = typeof obj === 'string' && typeof prop === 'string' ? `${obj}.${prop}` : null
1264
- if (key && ctx.module.hostImports?.[obj]?.[prop]) {
1265
- const spec = ctx.module.hostImports[obj][prop]
1266
- const alias = `${obj}$${prop}`
1267
- addHostImport(obj, prop, alias, spec)
1268
- callee = alias
1269
- } else if (key && includeForNamedCall(key)) {
1270
- callee = key
1271
- } else if (includeForGenericMethod(prop)) {
1272
- callee = prep(callee)
1273
- } else {
1274
- const mod = ctx.scope.chain[obj]
1275
- if (typeof obj === 'string' && mod && !mod.includes('.') && hasModule(mod)) {
1276
- callee = (includeModule(mod), mod + '.' + prop)
1277
- } else {
1278
- callee = prep(callee)
1279
- }
1280
- }
1281
- } else {
1282
- includeForCallableValue()
1283
- callee = prep(callee)
1284
- }
1594
+ callee = resolveCallee(callee, args)
1595
+ args = renestSoleCommaArg(args)
1285
1596
 
1286
- // Drop trailing-comma sentinel inside a comma group: `f(a, b,)` parses as
1287
- // ['()', 'f', [',', a, b, null]] — without trimming, the trailing null
1288
- // becomes a [, 0] literal and inflates arguments.length.
1289
- if (args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',') {
1290
- let end = args[0].length
1291
- while (end > 1 && args[0][end - 1] == null) end--
1292
- if (end < args[0].length) {
1293
- args[0] = end === 2 ? args[0][1] : args[0].slice(0, end)
1294
- }
1295
- }
1296
1597
  const preppedArgs = args.filter(a => a != null).map(prep)
1297
1598
  for (const a of preppedArgs) {
1298
1599
  if (typeof a === 'string' && hasFunc(a)) {
@@ -1312,7 +1613,9 @@ const handlers = {
1312
1613
  const inner = args[0]
1313
1614
  includeForArrayLiteral()
1314
1615
  if (inner == null) return ['[']
1315
- if (Array.isArray(inner) && inner[0] === ',') { const items = inner.slice(1); if (items.length && items[items.length - 1] === null) items.pop(); return ['[', ...items.map(item => item == null ? [, JZ_NULL] : prep(item))] }
1616
+ // jessie consumes the trailing comma itself; every remaining `null` in the
1617
+ // element list is a genuine elision (`[,]` → length 1, `[1,,]` → length 2).
1618
+ if (Array.isArray(inner) && inner[0] === ',') { const items = inner.slice(1); return ['[', ...items.map(item => item == null ? [, undefined] : prep(item))] }
1316
1619
  return ['[', prep(inner)]
1317
1620
  }
1318
1621
  if (typeof args[0] === 'string' && ctx.module.namespaces?.[args[0]]) {
@@ -1332,9 +1635,9 @@ const handlers = {
1332
1635
 
1333
1636
  // Bare block statement: push scope for let/const shadowing
1334
1637
  '{'(inner) {
1335
- scopes.push(new Map())
1638
+ pushScope()
1336
1639
  const result = ['{', prep(inner)]
1337
- scopes.pop()
1640
+ popScope()
1338
1641
  return result
1339
1642
  },
1340
1643
 
@@ -1343,14 +1646,44 @@ const handlers = {
1343
1646
  // Detect block body vs object literal
1344
1647
  if (Array.isArray(inner) && STMT_OPS.has(inner[0])) {
1345
1648
  // Block body: push block scope for let/const shadowing
1346
- scopes.push(new Map())
1649
+ pushScope()
1347
1650
  const result = ['{}', prep(inner)]
1348
- scopes.pop()
1651
+ popScope()
1349
1652
  return result
1350
1653
  }
1351
1654
 
1352
1655
  includeForObjectLiteral()
1353
1656
  if (inner == null) return ['{}']
1657
+ const items = Array.isArray(inner) && inner[0] === ','
1658
+ ? inner.slice(1)
1659
+ : [inner]
1660
+
1661
+ // Computed keys: `{[k]: v}` where `k` isn't compile-time foldable. jz's
1662
+ // object layout is slot-based (fixed schema at the literal site), so a
1663
+ // truly-dynamic key can't slot in. Lower to the existing dict path:
1664
+ // {a:1, [k]:v, b:2} → ((__t) => (__t[k]=v, __t))({a:1, b:2})
1665
+ // Static-but-non-string keys still fold via `staticPropertyKey` below.
1666
+ const isComputed = p => Array.isArray(p) && p[0] === ':'
1667
+ && typeof p[1] !== 'string' && staticPropertyKey(p[1]) == null
1668
+ if (items.some(isComputed)) {
1669
+ const staticItems = items.filter(p => !isComputed(p))
1670
+ const computedItems = items.filter(isComputed)
1671
+ const tmp = `${T}o${ctx.func.uniq++}`
1672
+ // Body: comma sequence of dict-sets, terminated with the tmp itself.
1673
+ // Computed key shape from parser is `[':', ['[]', keyExpr], valExpr]` —
1674
+ // unwrap the `['[]', keyExpr]` to grab keyExpr directly.
1675
+ const assigns = computedItems.map(p => {
1676
+ const keyExpr = Array.isArray(p[1]) && p[1][0] === '[]' ? p[1][1] : p[1]
1677
+ return ['=', ['[]', tmp, keyExpr], p[2]]
1678
+ })
1679
+ const body = [',', ...assigns, tmp]
1680
+ const arrow = ['=>', ['()', tmp], body]
1681
+ const arg = staticItems.length === 1 ? ['{}', staticItems[0]]
1682
+ : staticItems.length ? ['{}', [',', ...staticItems]]
1683
+ : ['{}']
1684
+ return prep(['()', arrow, arg])
1685
+ }
1686
+
1354
1687
  // Process properties: shorthand 'x' → [':', 'x', 'x'], or [':', key, val] → prep val only
1355
1688
  const prop = p => {
1356
1689
  if (typeof p === 'string') return [':', p, prep(p)]
@@ -1361,13 +1694,6 @@ const handlers = {
1361
1694
  }
1362
1695
  return prep(p)
1363
1696
  }
1364
- // Drop trailing-comma artifacts: subscript represents `{a:1, b,}` as
1365
- // `[",", [":","a",1], "b", null]` — the trailing `null` would prep to a
1366
- // literal-0 entry, leaving the literal carrying a phantom slot and
1367
- // shifting any subsequent slot-position resolution.
1368
- const items = Array.isArray(inner) && inner[0] === ','
1369
- ? inner.slice(1).filter(p => p != null)
1370
- : [inner]
1371
1697
  let prepped = items.map(prop)
1372
1698
  // ES spec: duplicate keys allowed; key takes first-seen position, last-seen value.
1373
1699
  const lastValue = new Map()
@@ -1391,7 +1717,7 @@ const handlers = {
1391
1717
 
1392
1718
  // For loop
1393
1719
  'for'(head, body) {
1394
- scopes.push(new Map())
1720
+ pushScope()
1395
1721
  let r
1396
1722
  if (Array.isArray(head) && head[0] === ';') {
1397
1723
  let [, init, cond, step] = head
@@ -1444,30 +1770,54 @@ const handlers = {
1444
1770
  if (sid != null) {
1445
1771
  // Known schema → compile-time unrolling with string keys
1446
1772
  const keys = ctx.schema.list[sid]
1447
- if (!keys || !keys.length) { scopes.pop(); return null }
1773
+ if (!keys || !keys.length) { popScope(); return null }
1448
1774
  includeForKnownKeyIteration()
1449
- const stmts = []
1450
- for (let i = 0; i < keys.length; i++) {
1451
- stmts.push(i === 0
1452
- ? ['let', ['=', varName, [, keys[i]]]]
1453
- : ['=', varName, [, keys[i]]])
1454
- stmts.push(cloneNode(body))
1775
+ if (!hasLoopJump(body)) {
1776
+ // No break/continue flat unroll, no loop frame needed.
1777
+ const stmts = []
1778
+ for (let i = 0; i < keys.length; i++) {
1779
+ stmts.push(i === 0
1780
+ ? ['let', ['=', varName, [, keys[i]]]]
1781
+ : ['=', varName, [, keys[i]]])
1782
+ stmts.push(cloneNode(body))
1783
+ }
1784
+ r = prep([';', ...stmts])
1785
+ } else {
1786
+ // break/continue present → an unrolled loop still needs its frames.
1787
+ // Wrap each iteration in a labeled block (continue target) and the
1788
+ // whole run in an outer labeled block (break target): `break` exits
1789
+ // the construct, `continue` falls through to the next iteration.
1790
+ const brkL = `${T}fibrk${ctx.func.uniq++}`
1791
+ const decl = prep(['let', ['=', varName, [, keys[0]]]])
1792
+ const parts = [decl]
1793
+ for (let i = 0; i < keys.length; i++) {
1794
+ const contL = `${T}ficont${ctx.func.uniq++}`
1795
+ const iter = prep(i === 0
1796
+ ? cloneNode(body)
1797
+ : [';', ['=', varName, [, keys[i]]], cloneNode(body)])
1798
+ parts.push(['label', contL, retargetLoopJumps(iter, brkL, contL)])
1799
+ }
1800
+ r = ['label', brkL, [';', ...parts]]
1455
1801
  }
1456
- r = prep([';', ...stmts])
1457
1802
  } else {
1458
1803
  // Dynamic object → HASH runtime iteration
1459
1804
  includeForRuntimeKeyIteration()
1460
1805
  r = ['for-in', varName, prep(src), prep(body)]
1461
1806
  }
1462
1807
  } else {
1463
- r = ['for', prep(head), prep(body)]
1808
+ // Some parser/jzify shapes for `for (;;)` and `for (; cond; )` arrive
1809
+ // as a null or bare-condition head instead of the canonical
1810
+ // `[';', init, cond, step]` tuple. Normalize them before emit so they
1811
+ // remain ordinary for-loops, not malformed two-slot nodes.
1812
+ r = ['for', null, head == null ? null : prep(head), null, prep(body)]
1464
1813
  }
1465
- scopes.pop()
1814
+ popScope()
1466
1815
  return r
1467
1816
  },
1468
1817
 
1469
1818
  // Property access - resolve namespaces or object/array properties
1470
1819
  '.'(obj, prop) {
1820
+ prop = typeof prop === 'string' ? prop : staticPropertyKey(prop)
1471
1821
  if (prop === 'caller' || prop === 'callee') err('`.caller` and `.callee` are prohibited: deprecated stack introspection')
1472
1822
  if (prop === 'url' && isImportMeta(obj)) return staticString(importMetaUrl())
1473
1823
  const mod = ctx.scope.chain[obj]
@@ -1505,6 +1855,20 @@ const handlers = {
1505
1855
  }
1506
1856
  }
1507
1857
 
1858
+ // `new RegExp("pattern", "flags?")` with string-literal pattern → compile
1859
+ // like a regex literal `/pattern/flags`. Dynamic pattern is not supported
1860
+ // (would require a runtime regex interpreter). Reported as build blocker #6.
1861
+ if (name === 'RegExp') {
1862
+ const literalArgs = ctorArgs.filter(a => a != null)
1863
+ const pattern = staticStringExpr(literalArgs[0])
1864
+ if (pattern == null)
1865
+ err('new RegExp() requires a string-literal pattern; dynamic regex construction is not supported')
1866
+ const flags = literalArgs.length > 1 ? staticStringExpr(literalArgs[1]) : ''
1867
+ if (flags == null)
1868
+ err('new RegExp() flags must be a string literal')
1869
+ return prep(['//', pattern, flags || undefined])
1870
+ }
1871
+
1508
1872
  // Wrap multi-arg ctor arg lists back into a single comma-group — the '()' op
1509
1873
  // expects callArgs as a single element (possibly comma-grouped).
1510
1874
  const wrapArgs = (args) => args.length === 0 ? [null]
@@ -1672,7 +2036,7 @@ function prepareModule(specifier, source) {
1672
2036
  const mangled = `${prefix}$${localName}`
1673
2037
  moduleExports.set(exportName, mangled)
1674
2038
  const func = ctx.func.list.find(f => f.name === localName)
1675
- if (func) renameFunc(func, mangled)
2039
+ if (func) { renameFunc(func, mangled); func._modulePrefix = prefix }
1676
2040
  if (ctx.scope.globals.has(localName)) {
1677
2041
  const wat = ctx.scope.globals.get(localName).replace(`$${localName}`, `$${mangled}`)
1678
2042
  ctx.scope.globals.delete(localName)
@@ -1734,15 +2098,17 @@ function prepareModule(specifier, source) {
1734
2098
  }
1735
2099
 
1736
2100
  // Rename ALL non-exported functions created during this module's prep
1737
- // (fn property assignments like f32.parse, internal helpers like cleanInt)
2101
+ // (fn property assignments like f32.parse, internal helpers like cleanInt).
2102
+ // Funcs added by nested prepareModule calls are tagged with `_modulePrefix`
2103
+ // by their own pass; skip those so prefixes don't stack (`a$b$name`).
1738
2104
  for (let i = savedFuncCount; i < ctx.func.list.length; i++) {
1739
2105
  const func = ctx.func.list[i]
1740
2106
  if (func.raw || func.name.startsWith(prefix + '$')) continue
1741
- // Skip functions from sub-imports (already prefixed with another module's prefix)
1742
- if (func.name.includes('__') && func.name.includes('$')) continue
2107
+ if (func._modulePrefix && func._modulePrefix !== prefix) continue
1743
2108
  const mangled = `${prefix}$${func.name}`
1744
2109
  moduleExports.set(func.name, mangled)
1745
2110
  renameFunc(func, mangled)
2111
+ func._modulePrefix = prefix
1746
2112
  }
1747
2113
 
1748
2114
  // Add mangled non-exported globals to moduleExports for walk renaming
@@ -1774,6 +2140,8 @@ function prepareModule(specifier, source) {
1774
2140
  for (let i = savedFuncCount; i < ctx.func.list.length; i++) {
1775
2141
  const func = ctx.func.list[i]
1776
2142
  if (!func.body) continue
2143
+ // Sub-module funcs already had their own walk; parent's rename map doesn't apply.
2144
+ if (func._modulePrefix && func._modulePrefix !== prefix) continue
1777
2145
  const funcParams = new Set(func.sig?.params?.map(p => p.name) || [])
1778
2146
  walk(func.body, funcParams)
1779
2147
  if (func.defaults) for (const [k, v] of Object.entries(func.defaults)) func.defaults[k] = walk(v, funcParams)
@@ -1799,3 +2167,160 @@ function prepareModule(specifier, source) {
1799
2167
  ctx.module.resolvedModules.set(specifier, result)
1800
2168
  return result
1801
2169
  }
2170
+
2171
+ // =============================================================================
2172
+ // AST-level fusion passes (pre-resolution)
2173
+ // =============================================================================
2174
+ // Unlike src/optimize.js (a pure WAT IR→IR rewrite, post-emission), these
2175
+ // rewrites need the *raw, pre-resolution* AST shape — bindings still named,
2176
+ // arrow bodies still inline — so they run inside prepare(), before scope
2177
+ // resolution and emit. They mutate the AST in place; shape guards are strict
2178
+ // enough that misfires are impossible.
2179
+
2180
+ /** Sparse-read .map fusion: rewrite `const b = a.map(arrow); for(...; j<b.length; ...) USE(b[j])`
2181
+ * into a fused for-loop that inlines `arrow(a[j])` at the read site, eliminating the materialized
2182
+ * intermediate array. Only fires on shapes where every use of `b` is a numeric `b[idx]` read or a
2183
+ * `b.length` read, the arrow is pure with a single named param, and `b` is not referenced after the
2184
+ * consumer for-loop. Preserves observable behavior because the arrow's pure-expression body has no
2185
+ * order-dependent effects. */
2186
+ function fuseSparseMapReads(root) {
2187
+ walkSparse(root)
2188
+ }
2189
+ function walkSparse(node) {
2190
+ if (!Array.isArray(node)) return
2191
+ for (let i = 1; i < node.length; i++) walkSparse(node[i])
2192
+ if (node[0] === ';') tryFuseInBlock(node)
2193
+ }
2194
+ function tryFuseInBlock(seq) {
2195
+ for (let i = 1; i < seq.length - 1; i++) {
2196
+ const fused = tryFusePair(seq[i], seq[i + 1], seq, i)
2197
+ if (fused) {
2198
+ seq.splice(i, 2, ...fused)
2199
+ i-- // re-examine same position (chained fusions)
2200
+ }
2201
+ }
2202
+ }
2203
+ function tryFusePair(decl, forNode, seq, declIdx) {
2204
+ if (!Array.isArray(decl) || (decl[0] !== 'const' && decl[0] !== 'let')) return null
2205
+ if (decl.length !== 2) return null // single binding only
2206
+ const bind = decl[1]
2207
+ if (!Array.isArray(bind) || bind[0] !== '=' || typeof bind[1] !== 'string') return null
2208
+ const NAME = bind[1], rhs = bind[2]
2209
+ if (!Array.isArray(rhs) || rhs[0] !== '()') return null
2210
+ const callee = rhs[1]
2211
+ if (!Array.isArray(callee) || callee[0] !== '.' || callee[2] !== 'map') return null
2212
+ const RECV = callee[1]
2213
+ if (typeof RECV !== 'string' || RECV === NAME) return null
2214
+ const arrow = rhs[2]
2215
+ if (!Array.isArray(arrow) || arrow[0] !== '=>') return null
2216
+ // Single-name param only: `x => …` or `(x) => …`
2217
+ const ap = arrow[1]
2218
+ const PARAM = typeof ap === 'string' ? ap :
2219
+ (Array.isArray(ap) && ap[0] === '()' && typeof ap[1] === 'string' ? ap[1] : null)
2220
+ if (!PARAM || PARAM === NAME || PARAM === RECV) return null
2221
+ // Body: single-expression arrow only (block bodies skipped — could extend later).
2222
+ const aBody = arrow[2]
2223
+ if (Array.isArray(aBody) && aBody[0] === '{}') return null
2224
+ if (!isPureSparseArrowBody(aBody, PARAM)) return null
2225
+ // For-loop: ['for', [';', initStmt, cond, inc], body]
2226
+ if (!Array.isArray(forNode) || forNode[0] !== 'for' || forNode.length !== 3) return null
2227
+ const head = forNode[1]
2228
+ if (!Array.isArray(head) || head[0] !== ';' || head.length !== 4) return null
2229
+ const cond = head[2], forBody = forNode[2]
2230
+ // Verify `NAME` is used only as `NAME[idx]` or `NAME.length` inside cond+forBody.
2231
+ if (!hasOnlySparseUses(cond, NAME)) return null
2232
+ if (!hasOnlySparseUses(forBody, NAME)) return null
2233
+ if (!hasAnyIndexedRead(forBody, NAME) && !hasAnyIndexedRead(cond, NAME)) return null
2234
+ // `NAME` must not be read after the for-loop in the same block.
2235
+ for (let k = declIdx + 2; k < seq.length; k++) {
2236
+ if (refsName(seq[k], NAME)) return null
2237
+ }
2238
+ // RECV must not be reassigned inside the for-loop (would invalidate substitution).
2239
+ if (assignsName(forNode, RECV) || assignsName(forNode, NAME)) return null
2240
+ // PARAM must not collide with any binding inside forBody (otherwise substitution shadows wrongly).
2241
+ if (bindsName(forNode, PARAM)) return null
2242
+ // Apply substitution: NAME.length → RECV.length; NAME[idx] → arrowBody[PARAM ← RECV[idx]].
2243
+ const newCond = substSparse(cond, NAME, RECV, PARAM, aBody)
2244
+ const newBody = substSparse(forBody, NAME, RECV, PARAM, aBody)
2245
+ const newHead = [';', head[1], newCond, head[3]]
2246
+ return [['for', newHead, newBody]]
2247
+ }
2248
+ function isPureSparseArrowBody(n, PARAM) {
2249
+ if (typeof n === 'string') return true
2250
+ if (!Array.isArray(n)) return true
2251
+ const op = n[0]
2252
+ // Calls / new / assignments / increments are unsafe for repeated-substitution semantics.
2253
+ if (op === '()' || op === '?.()' || op === 'new' || op === '++' || op === '--') return false
2254
+ if (op === '=>') return false // nested closure is opaque
2255
+ if (typeof op === 'string' && op !== '=>' && op !== '===' && op !== '!==' && op !== '==' && op !== '!=' && op !== '<=' && op !== '>=' && op.endsWith('=') && op !== '=') return false
2256
+ if (op === '=') return false
2257
+ for (let i = 1; i < n.length; i++) if (!isPureSparseArrowBody(n[i], PARAM)) return false
2258
+ return true
2259
+ }
2260
+ function hasOnlySparseUses(n, NAME) {
2261
+ if (typeof n === 'string') return n !== NAME
2262
+ if (!Array.isArray(n)) return true
2263
+ const op = n[0]
2264
+ if (op === '[]' && n.length === 3 && n[1] === NAME) return hasOnlySparseUses(n[2], NAME) // NAME[idx] — idx must not reference NAME
2265
+ if (op === '.' && n[1] === NAME) {
2266
+ if (n[2] === 'length') return true
2267
+ return false // any other property access on NAME is opaque
2268
+ }
2269
+ for (let i = 1; i < n.length; i++) if (!hasOnlySparseUses(n[i], NAME)) return false
2270
+ return true
2271
+ }
2272
+ function hasAnyIndexedRead(n, NAME) {
2273
+ if (!Array.isArray(n)) return false
2274
+ if (n[0] === '[]' && n.length === 3 && n[1] === NAME) return true
2275
+ for (let i = 1; i < n.length; i++) if (hasAnyIndexedRead(n[i], NAME)) return true
2276
+ return false
2277
+ }
2278
+ function refsName(n, NAME) {
2279
+ if (typeof n === 'string') return n === NAME
2280
+ if (!Array.isArray(n)) return false
2281
+ for (let i = 1; i < n.length; i++) if (refsName(n[i], NAME)) return true
2282
+ return false
2283
+ }
2284
+ function assignsName(n, NAME) {
2285
+ if (!Array.isArray(n)) return false
2286
+ const op = n[0]
2287
+ if ((op === '=' || op === '++' || op === '--' ||
2288
+ (typeof op === 'string' && op.endsWith('=') && op !== '==' && op !== '===' && op !== '!=' && op !== '!==' && op !== '<=' && op !== '>='))
2289
+ && n[1] === NAME) return true
2290
+ for (let i = 1; i < n.length; i++) if (assignsName(n[i], NAME)) return true
2291
+ return false
2292
+ }
2293
+ function bindsName(n, NAME) {
2294
+ if (!Array.isArray(n)) return false
2295
+ const op = n[0]
2296
+ if ((op === 'let' || op === 'const')) {
2297
+ for (let i = 1; i < n.length; i++) {
2298
+ const bind = n[i]
2299
+ if (Array.isArray(bind) && bind[0] === '=' && bind[1] === NAME) return true
2300
+ }
2301
+ }
2302
+ if (op === '=>') {
2303
+ const p = n[1]
2304
+ if (p === NAME) return true
2305
+ if (Array.isArray(p)) {
2306
+ if (p[0] === '()' && p[1] === NAME) return true
2307
+ // skip deeper destructuring forms — conservative
2308
+ }
2309
+ }
2310
+ for (let i = 1; i < n.length; i++) if (bindsName(n[i], NAME)) return true
2311
+ return false
2312
+ }
2313
+ function substSparse(n, NAME, RECV, PARAM, arrowBody) {
2314
+ if (typeof n !== 'object' || n === null || !Array.isArray(n)) return n
2315
+ if (n[0] === '.' && n[1] === NAME && n[2] === 'length') return ['.', RECV, 'length']
2316
+ if (n[0] === '[]' && n.length === 3 && n[1] === NAME) {
2317
+ const idx = substSparse(n[2], NAME, RECV, PARAM, arrowBody)
2318
+ return cloneAndBind(arrowBody, PARAM, ['[]', RECV, idx])
2319
+ }
2320
+ return n.map((c, i) => i === 0 ? c : substSparse(c, NAME, RECV, PARAM, arrowBody))
2321
+ }
2322
+ function cloneAndBind(node, PARAM, replacement) {
2323
+ if (node === PARAM) return replacement
2324
+ if (!Array.isArray(node)) return node
2325
+ return node.map((c, i) => i === 0 ? c : cloneAndBind(c, PARAM, replacement))
2326
+ }