jz 0.1.0 → 0.2.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
@@ -22,13 +22,45 @@
22
22
  * @module prepare
23
23
  */
24
24
 
25
- import { parse } from 'subscript/jessie'
25
+ import { parse } from 'subscript/feature/jessie'
26
26
  import { ctx, err, derive } from './ctx.js'
27
- import { T, STMT_OPS, VAL, extractParams, collectParamNames, classifyParam } from './analyze.js'
28
- import * as mods from '../module/index.js'
27
+ import { T, STMT_OPS, VAL, valTypeOf, typedElemCtor, extractParams, collectParamNames, classifyParam, observeNodeFacts, staticPropertyKey } from './analyze.js'
28
+ import { isFuncRef } from './ir.js'
29
+ import {
30
+ CTORS, TIMER_NAMES,
31
+ hasModule, includeModule,
32
+ includeForArrayAccess, includeForArrayLiteral, includeForArrayPattern, includeForCallableValue,
33
+ includeForGenericMethod, includeForKnownKeyIteration, includeForNamedCall, includeForNumericCoercion,
34
+ includeForObjectLiteral, includeForObjectPattern, includeForOp, includeForProperty, includeForRuntimeCtor,
35
+ includeForRuntimeKeyIteration, includeForStringOnly, includeForStringValue, includeForTimerRuntime,
36
+ } from './autoload.js'
29
37
 
30
38
  let depth = 0 // arrow nesting depth (0=top-level, >0=inside function)
31
39
  let scopes = [] // block scope stack: [{names: Set, renames: Map}]
40
+ // Per-arrow set of names already declared anywhere in the function body. Used
41
+ // to force a rename when the same identifier is declared in two sibling blocks
42
+ // (else-if arms, separate { ... } chunks): without renaming, both decls lower
43
+ // to the same WASM local, but downstream optimizations (directClosures) gate
44
+ // on per-decl `isReassigned`, not per-WASM-local — they'd read a stale binding.
45
+ let funcLocalNames = []
46
+
47
+ // ES spec: identifier with \uHHHH or \u{...} escape is equivalent to the decoded
48
+ // form. subscript preserves raw spelling in the AST; normalize once before prep.
49
+ const IDESC = /\\u\{([0-9a-fA-F]+)\}|\\u([0-9a-fA-F]{4})/g
50
+ const decodeIdent = s => s.includes('\\u')
51
+ ? s.replace(IDESC, (_, b, p) => String.fromCodePoint(parseInt(b || p, 16)))
52
+ : s
53
+
54
+ const normalizeIdents = node => {
55
+ if (!Array.isArray(node)) return
56
+ // Literal-value wrapper [null, X] / [undefined, X]: X is a value, not an identifier
57
+ if (node.length === 2 && node[0] == null) return
58
+ for (let i = 1; i < node.length; i++) {
59
+ const v = node[i]
60
+ if (typeof v === 'string') node[i] = decodeIdent(v)
61
+ else if (Array.isArray(v)) normalizeIdents(v)
62
+ }
63
+ }
32
64
 
33
65
  const hostReturnValType = spec => {
34
66
  if (!spec || typeof spec === 'function') return null
@@ -41,15 +73,102 @@ const hostReturnValType = spec => {
41
73
 
42
74
  const addHostImport = (mod, name, alias, spec) => {
43
75
  const nParams = typeof spec === 'function' ? spec.length : (spec?.params || 0)
44
- const params = Array(nParams).fill(['param', 'f64'])
76
+ // User-supplied imports carry NaN-boxed values via i64 (not f64) so V8 cannot
77
+ // canonicalize the NaN payload across the wasm↔JS function boundary —
78
+ // same hazard as env.print / __ext_*. Call sites wrap args with asI64()
79
+ // and unwrap the i64 return with f64.reinterpret_i64.
80
+ const params = Array(nParams).fill(['param', 'i64'])
45
81
  if (!ctx.module.imports.some(i => i[3]?.[1] === `$${alias}`)) {
46
- ctx.module.imports.push(['import', `"${mod}"`, `"${name}"`, ['func', `$${alias}`, ...params, ['result', 'f64']]])
82
+ ctx.module.imports.push(['import', `"${mod}"`, `"${name}"`, ['func', `$${alias}`, ...params, ['result', 'i64']]])
47
83
  }
48
84
  ctx.scope.chain[alias] = alias
49
85
  const vt = hostReturnValType(spec)
50
86
  if (vt) ctx.module.hostImportValTypes.set(alias, vt)
51
87
  }
52
88
 
89
+ const isImportMeta = node => Array.isArray(node) && node[0] === '.' && node[1] === 'import' && node[2] === 'meta'
90
+ const isImportMetaProp = (node, prop) => Array.isArray(node) && node[0] === '.' && isImportMeta(node[1]) && node[2] === prop
91
+ const stringValue = node => Array.isArray(node) && node[0] == null && typeof node[1] === 'string' ? node[1] : null
92
+ const flatArgs = args => args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',' ? args[0].slice(1) : args
93
+ const MUTATING_ARRAY_METHODS = new Set(['copyWithin', 'fill', 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'])
94
+
95
+ function stringArrayValues(expr) {
96
+ if (!Array.isArray(expr) || expr[0] !== '[' || expr.length === 1) return null
97
+ const out = []
98
+ for (const item of expr.slice(1)) {
99
+ if (!Array.isArray(item) || item[0] !== 'str' || typeof item[1] !== 'string') return null
100
+ out.push(item[1])
101
+ }
102
+ return out
103
+ }
104
+
105
+ function staticString(value) {
106
+ includeForStringValue()
107
+ return ['str', value]
108
+ }
109
+
110
+ function importMetaUrl() {
111
+ if (!ctx.transform.importMetaUrl) err('`import.meta.url` requires compile option `importMetaUrl`')
112
+ return ctx.transform.importMetaUrl
113
+ }
114
+
115
+ function resolveImportMeta(spec) {
116
+ const base = importMetaUrl()
117
+ try { return new URL(spec, base).href }
118
+ catch { err(`Cannot resolve import.meta specifier '${spec}' from '${base}'`) }
119
+ }
120
+
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
+ function recordModuleInitFacts(root) {
133
+ const facts = ctx.module.initFacts ||= {
134
+ dynVars: new Set(), anyDyn: false, hasSchemaLiterals: false,
135
+ hasFuncValue: false, timerNames: new Set(),
136
+ maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false,
137
+ }
138
+ const visitFuncValue = (node) => {
139
+ if (facts.hasFuncValue || !Array.isArray(node)) return
140
+ const [op, ...args] = node
141
+ if (op === '()') {
142
+ for (let i = 1; i < args.length; i++) {
143
+ const a = args[i]
144
+ if (isFuncRef(a, ctx.func.names)) { facts.hasFuncValue = true; return }
145
+ visitFuncValue(a)
146
+ }
147
+ return
148
+ }
149
+ if (op === '.' || op === '?.') {
150
+ if (isFuncRef(args[0], ctx.func.names)) { facts.hasFuncValue = true; return }
151
+ visitFuncValue(args[0])
152
+ return
153
+ }
154
+ if (op === '=>') { visitFuncValue(args[1]); return }
155
+ for (const a of args) {
156
+ if (isFuncRef(a, ctx.func.names)) { facts.hasFuncValue = true; return }
157
+ visitFuncValue(a)
158
+ }
159
+ }
160
+ const walk = (node) => {
161
+ if (!Array.isArray(node)) {
162
+ if (typeof node === 'string' && TIMER_NAMES.has(node)) facts.timerNames.add(node)
163
+ return
164
+ }
165
+ observeNodeFacts(node, facts)
166
+ for (const a of node.slice(1)) walk(a)
167
+ }
168
+ visitFuncValue(root)
169
+ walk(root)
170
+ }
171
+
53
172
  /**
54
173
  * @typedef {null|number|string|ASTNode[]} ASTNode
55
174
  */
@@ -62,18 +181,28 @@ const addHostImport = (mod, name, alias, spec) => {
62
181
  export default function prepare(node) {
63
182
  depth = 0
64
183
  scopes = []
184
+ funcLocalNames = [new Set()]
65
185
  includeModule('core')
186
+ normalizeIdents(node)
66
187
  fuseSparseMapReads(node)
67
188
  const ast = prep(node)
68
189
  // Top-level functions referenced as first-class values (e.g. `let o = { fn: g }`,
69
190
  // `arr.push(g)`, `return g`) need trampoline emission, which depends on the fn
70
191
  // module's closure.table machinery. defFunc paths don't trigger fn-module load,
71
192
  // so scan post-prep and include `fn` if any user func appears in a value position.
72
- if (!ctx.module.modules.fn && ctx.func.list.length) {
193
+ // Same scan also catches inline arrows that survive prep (e.g. `{ m: (x) => x }`)
194
+ // — defFunc only lifts arrows that are the direct RHS of a let/const/export default,
195
+ // and depth-0 arrows in any other position (object property, ternary arm, return
196
+ // value, ...) skip the depth>0 prep-time include, so they reach emit unsupported
197
+ // unless we catch them here.
198
+ if (!ctx.module.modules.fn) {
73
199
  const funcNames = new Set(ctx.func.list.map(f => f.name))
74
200
  const visit = (n) => {
75
201
  if (!Array.isArray(n)) return false
76
202
  const [op, ...args] = n
203
+ // Any inline arrow surviving prep is a closure value (defFunc-lifted ones
204
+ // are extracted from the AST into ctx.func.list).
205
+ if (op === '=>') return true
77
206
  if (op === '()') {
78
207
  // callee at args[0]: skip if it's a bare func name (direct call); recurse rest
79
208
  if (typeof args[0] !== 'string' || !funcNames.has(args[0])) {
@@ -91,10 +220,6 @@ export default function prepare(node) {
91
220
  if (typeof args[0] === 'string' && funcNames.has(args[0])) return true
92
221
  return visit(args[0])
93
222
  }
94
- if (op === '=>') {
95
- // body only — params are bindings, not refs
96
- return visit(args[1])
97
- }
98
223
  for (const a of args) {
99
224
  if (typeof a === 'string' && funcNames.has(a)) return true
100
225
  if (visit(a)) return true
@@ -103,13 +228,12 @@ export default function prepare(node) {
103
228
  }
104
229
  let needs = visit(ast)
105
230
  if (!needs) for (const f of ctx.func.list) if (f.body && visit(f.body)) { needs = true; break }
106
- if (!needs && ctx.module.moduleInits) for (const mi of ctx.module.moduleInits) if (visit(mi)) { needs = true; break }
107
- if (needs) includeModule('fn')
231
+ if (!needs && ctx.module.initFacts?.hasFuncValue) needs = true
232
+ if (needs) includeForCallableValue()
108
233
  }
109
234
 
110
235
  // Native timers: inline WASM timer queue when referenced (no host imports needed)
111
- const TIMER_NAMES = new Set(['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'])
112
- const usedTimers = new Set()
236
+ const usedTimers = new Set(ctx.module.initFacts?.timerNames || [])
113
237
  const scanTimers = (n) => {
114
238
  if (!Array.isArray(n)) {
115
239
  if (typeof n === 'string' && TIMER_NAMES.has(n)) usedTimers.add(n)
@@ -117,12 +241,38 @@ export default function prepare(node) {
117
241
  }
118
242
  for (let i = 0; i < n.length; i++) scanTimers(n[i])
119
243
  }
120
- const allNodes = [ast, ...ctx.func.list.map(f => f.body), ...(ctx.module.moduleInits || [])]
244
+ const allNodes = [ast, ...ctx.func.list.map(f => f.body)]
121
245
  for (const node of allNodes) scanTimers(node)
122
246
  if (usedTimers.size) {
123
- ctx.features.timers = true // Timer module included — compile.js needs to emit __timer_init
124
- includeModule('timer')
125
- includeModule('fn') // call_indirect dispatch for callbacks
247
+ includeForTimerRuntime()
248
+ }
249
+
250
+ // Invalidate shapeStrs for any module-level binding that's later assigned to.
251
+ // shapeStrs is "effectively-const string literals at module scope" — used by
252
+ // analyze.js's jsonConstString to enable shape inference on `let SRC = '{...}'`
253
+ // patterns (bench convention) without enabling the const-only static fold.
254
+ // The scan must skip `=` nodes that are children of `let`/`const`/`export` —
255
+ // those are decl-initializers, not reassignments.
256
+ if (ctx.scope.shapeStrs?.size || ctx.scope.shapeStrArrays?.size) {
257
+ const writes = new Set()
258
+ const scan = (n, inDecl) => {
259
+ if (!Array.isArray(n)) return
260
+ const [op, lhs] = n
261
+ if (op === '=' && typeof lhs === 'string' && !inDecl) writes.add(lhs)
262
+ if (op === '=' && Array.isArray(lhs) && lhs[0] === '[]' && typeof lhs[1] === 'string' && !inDecl) writes.add(lhs[1])
263
+ // Compound assigns desugar to `=`; increments emit as `++`/`--` post-prep.
264
+ if ((op === '++' || op === '--') && typeof lhs === 'string') writes.add(lhs)
265
+ if ((op === '++' || op === '--') && Array.isArray(lhs) && lhs[0] === '[]' && typeof lhs[1] === 'string') writes.add(lhs[1])
266
+ if (op === '()' && Array.isArray(lhs) && lhs[0] === '.' && typeof lhs[1] === 'string' && MUTATING_ARRAY_METHODS.has(lhs[2])) writes.add(lhs[1])
267
+ const childInDecl = (op === 'let' || op === 'const' || op === 'var' || op === 'export')
268
+ for (let i = 1; i < n.length; i++) scan(n[i], childInDecl)
269
+ }
270
+ scan(ast, false)
271
+ for (const f of ctx.func.list) if (f.body) scan(f.body, false)
272
+ for (const name of writes) {
273
+ ctx.scope.shapeStrs?.delete(name)
274
+ ctx.scope.shapeStrArrays?.delete(name)
275
+ }
126
276
  }
127
277
 
128
278
  return ast
@@ -146,73 +296,43 @@ function isDeclared(name) {
146
296
  return scopes.some(s => s.has(name))
147
297
  }
148
298
 
299
+ const hasFunc = name => ctx.func.names.has(name)
300
+
301
+ const renameFunc = (func, nextName) => {
302
+ ctx.func.names.delete(func.name)
303
+ func.name = nextName
304
+ ctx.func.names.add(nextName)
305
+ }
306
+
149
307
  /** Map JS typeof strings to jz type checks. Codes < 0 trigger specialized emitTypeofCmp paths. */
150
- const TYPEOF_MAP = { 'number': -1, 'string': -2, 'undefined': -3, 'boolean': -4, 'object': -5, 'function': -6 }
308
+ const TYPEOF_MAP = { 'number': -1, 'string': -2, 'undefined': -3, 'boolean': -4, 'object': -5, 'function': -6, 'bigint': -7 }
309
+ // Constant fold typeof for known builtin namespaces (e.g. Math.exp). prep(x) resolves Math.exp → 'math.exp'.
310
+ function staticTypeofString(x) {
311
+ // Bare callable global: parseInt, parseFloat, isNaN, isFinite, Error, BigInt, etc.
312
+ if (typeof x === 'string' && !ctx.func?.locals?.has(x) && GLOBALS[x] && ctx.core.emit?.[x]?.length > 0) return 'function'
313
+ const px = prep(x)
314
+ if (typeof px === 'string' && px.includes('.') && ctx.core.emit?.[px]?.length > 0) return 'function'
315
+ return null
316
+ }
151
317
  function resolveTypeof(node) {
152
318
  const [op, a, b] = node
153
319
  // typeof x == 'string' → type check
154
320
  if (Array.isArray(a) && a[0] === 'typeof' && Array.isArray(b) && b[0] == null && typeof b[1] === 'string') {
321
+ const known = staticTypeofString(a[1])
322
+ if (known != null) return [, op === '==' ? known === b[1] : known !== b[1]]
155
323
  const code = TYPEOF_MAP[b[1]]
156
324
  if (code != null) return [op, ['typeof', a[1]], [, code]]
157
325
  }
158
326
  // 'string' == typeof x
159
327
  if (Array.isArray(b) && b[0] === 'typeof' && Array.isArray(a) && a[0] == null && typeof a[1] === 'string') {
328
+ const known = staticTypeofString(b[1])
329
+ if (known != null) return [, op === '==' ? known === a[1] : known !== a[1]]
160
330
  const code = TYPEOF_MAP[a[1]]
161
331
  if (code != null) return [op, ['typeof', b[1]], [, code]]
162
332
  }
163
333
  return node
164
334
  }
165
335
 
166
- // Property-based narrowing: unambiguous prop names limit module load.
167
- // Map value is the exact module set for that property (always includes 'core').
168
- // Properties not in this table fall back to the generic '.' receiver-type heuristic.
169
- // Uses null-prototype dict so inherited names (constructor, toString) don't match.
170
- const PROP_MODULES = Object.assign(Object.create(null), {
171
- // array-only methods
172
- push: ['core', 'array'], pop: ['core', 'array'], shift: ['core', 'array'], unshift: ['core', 'array'],
173
- splice: ['core', 'array'], reverse: ['core', 'array'], sort: ['core', 'array'], fill: ['core', 'array'],
174
- map: ['core', 'array'], filter: ['core', 'array'], reduce: ['core', 'array'], reduceRight: ['core', 'array'],
175
- forEach: ['core', 'array'], find: ['core', 'array'], findIndex: ['core', 'array'],
176
- findLast: ['core', 'array'], findLastIndex: ['core', 'array'],
177
- every: ['core', 'array'], some: ['core', 'array'], flat: ['core', 'array'], flatMap: ['core', 'array'],
178
- join: ['core', 'array'], copyWithin: ['core', 'array'], at: ['core', 'array'],
179
- // string-only methods
180
- charAt: ['core', 'string'], charCodeAt: ['core', 'string'], codePointAt: ['core', 'string'],
181
- toUpperCase: ['core', 'string'], toLowerCase: ['core', 'string'], trim: ['core', 'string'],
182
- trimStart: ['core', 'string'], trimEnd: ['core', 'string'],
183
- split: ['core', 'string'], replace: ['core', 'string'], replaceAll: ['core', 'string'],
184
- repeat: ['core', 'string'], startsWith: ['core', 'string'], endsWith: ['core', 'string'],
185
- padStart: ['core', 'string'], padEnd: ['core', 'string'], normalize: ['core', 'string'],
186
- matchAll: ['core', 'string'], match: ['core', 'string'],
187
- substring: ['core', 'string'], substr: ['core', 'string'],
188
- // collection-only methods
189
- add: ['core', 'collection'], clear: ['core', 'collection'],
190
- // string + array (cross-type but not object/collection)
191
- slice: ['core', 'string', 'array'], concat: ['core', 'string', 'array'],
192
- indexOf: ['core', 'string', 'array'], lastIndexOf: ['core', 'string', 'array'],
193
- includes: ['core', 'string', 'array'],
194
- // string + array + typedarray (length shared across all sequence types)
195
- length: ['core', 'string', 'array', 'typedarray'],
196
- // Note: toString/toFixed/toPrecision/toExponential not narrowed — they only have
197
- // number-schema emitters; unknown receivers fall through to __ext_call (collection module).
198
- })
199
-
200
- const OP_MODULES = {
201
- // '.' handled inline (see '.' handler) for property-based narrowing
202
- '?.': ['core', 'string', 'collection'],
203
- '?.[]': ['core', 'array', 'collection'],
204
- '?.()': ['core', 'fn'],
205
- 'u+': ['number', 'string'],
206
- 'in': ['core', 'collection', 'string'],
207
- '==': ['core', 'string'],
208
- '!=': ['core', 'string'],
209
- 'typeof': ['core', 'string'],
210
- '[': ['core', 'array'],
211
- '{': ['core', 'object', 'string', 'collection'],
212
- '//': ['core', 'string', 'regex'],
213
- }
214
- const dict = obj => Object.assign(Object.create(null), obj)
215
-
216
336
  const cloneNode = (node) => {
217
337
  if (!Array.isArray(node)) return node
218
338
  const copy = node.map(cloneNode)
@@ -220,64 +340,6 @@ const cloneNode = (node) => {
220
340
  return copy
221
341
  }
222
342
 
223
- // Call-site module inclusion. Keyed by either a bare callee name (e.g. 'parseFloat')
224
- // or a dotted path ('console.log'). Value is the module set to load.
225
- // The lookup fires from the '()' handler before scope resolution.
226
- // Architectural note: fully eliminating this (auto-imports via jzify) was considered but
227
- // rejected — Object.fromEntries needs collection+string while o.x does not, so MOD_DEPS
228
- // transitive inclusion alone over-loads simple paths. This table pinpoints load per callee.
229
- const CALL_MODULES = dict({
230
- // Bare-identifier calls
231
- 'ArrayBuffer': ['core', 'typedarray'],
232
- 'DataView': ['core', 'typedarray'],
233
- 'BigInt64Array': ['core', 'typedarray'],
234
- 'BigUint64Array': ['core', 'typedarray'],
235
- 'parseFloat': ['number', 'string'],
236
- 'parseInt': ['number', 'string'],
237
- 'String': ['core', 'string', 'number'],
238
- 'Number': ['number', 'string'],
239
- 'Boolean': ['number'],
240
- 'TextEncoder': ['core', 'string'],
241
- 'TextDecoder': ['core', 'string'],
242
- 'Error': ['core', 'string'],
243
- 'BigInt': ['number'],
244
- // Namespace.method calls
245
- 'console.log': ['core', 'string', 'number', 'console'],
246
- 'console.warn': ['core', 'string', 'number', 'console'],
247
- 'console.error': ['core', 'string', 'number', 'console'],
248
- 'Object.fromEntries': ['collection', 'string'],
249
- 'Object.keys': ['string'],
250
- 'Object.entries': ['string'],
251
- 'Date.now': ['core', 'console'],
252
- 'performance.now': ['core', 'console'],
253
- 'String.fromCharCode': ['core', 'string'],
254
- 'String.fromCodePoint': ['core', 'string'],
255
- 'BigInt.asIntN': ['number'],
256
- 'BigInt.asUintN': ['number'],
257
- 'Float64Array.from': ['core', 'typedarray', 'array'],
258
- 'Float32Array.from': ['core', 'typedarray', 'array'],
259
- 'Int32Array.from': ['core', 'typedarray', 'array'],
260
- 'Uint32Array.from': ['core', 'typedarray', 'array'],
261
- 'Int16Array.from': ['core', 'typedarray', 'array'],
262
- 'Uint16Array.from': ['core', 'typedarray', 'array'],
263
- 'Int8Array.from': ['core', 'typedarray', 'array'],
264
- 'Uint8Array.from': ['core', 'typedarray', 'array'],
265
- 'ArrayBuffer.isView': ['core', 'typedarray'],
266
- })
267
-
268
- // Method-call inclusion for unknown-receiver cases (e.g. `(x) => x.toFixed(2)`).
269
- // Keyed by bare method name — fires only when receiver can't be resolved to a namespace.
270
- const GENERIC_METHOD_MODULES = dict({
271
- 'toString': ['core', 'string', 'number'],
272
- 'toFixed': ['core', 'string', 'number'],
273
- 'toPrecision': ['core', 'string', 'number'],
274
- 'toExponential': ['core', 'string', 'number'],
275
- })
276
-
277
- // Constructor names that users may call without `new` — `Float64Array(5)` → `new Float64Array(5)`.
278
- // Keeps user source ergonomic; these cannot be plain function calls in JS semantics anyway.
279
- const CTORS = ['Float64Array','Float32Array','Int32Array','Uint32Array','Int16Array','Uint16Array','Int8Array','Uint8Array','BigInt64Array','BigUint64Array','Set','Map']
280
-
281
343
  /** Sparse-read .map fusion: rewrite `const b = a.map(arrow); for(...; j<b.length; ...) USE(b[j])`
282
344
  * into a fused for-loop that inlines `arrow(a[j])` at the read site, eliminating the materialized
283
345
  * intermediate array. Pre-prep AST mutation; only fires on shapes where every use of `b` is a
@@ -427,7 +489,7 @@ function cloneAndBind(node, PARAM, replacement) {
427
489
  }
428
490
 
429
491
  function prep(node) {
430
- if (Array.isArray(node) && OP_MODULES[node[0]]) includeMods(...OP_MODULES[node[0]])
492
+ if (Array.isArray(node)) includeForOp(node[0])
431
493
  if (Array.isArray(node) && node.loc != null) ctx.error.loc = node.loc
432
494
  if (node == null) return [, 0] // null/undefined → 0 literal
433
495
  if (node === true) return [, 1]
@@ -438,7 +500,9 @@ function prep(node) {
438
500
  if (node in F64_CONSTANTS) return [, F64_CONSTANTS[node]]
439
501
  if (PROHIBITED[node]) err(PROHIBITED[node])
440
502
  // Boolean/Number as value → identity arrow (for .filter(Boolean), .map(Number) etc.)
441
- if (node === 'Boolean' || node === 'Number') { includeMods('core', 'fn'); return ['=>', 'x', 'x'] }
503
+ if (node === 'Boolean' || node === 'Number') { includeForCallableValue(); return ['=>', 'x', 'x'] }
504
+ // Block locals shadow module imports/globals, even when the local keeps the same name.
505
+ if (scopes.length && isDeclared(node)) return resolveScope(node)
442
506
  const resolved = ctx.scope.chain[node]
443
507
  if (resolved?.includes('.')) return resolved
444
508
  // Cross-module import: mangled name (e.g. __util_js$clone)
@@ -453,7 +517,7 @@ function prep(node) {
453
517
  if (op === 'void' && ctx.transform.strict) err('strict mode: `void` is prohibited. It diverges from JS by evaluating to 0.')
454
518
  if (op == null) {
455
519
  if (typeof args[0] === 'string') {
456
- includeMods('core', 'string', 'number')
520
+ includeForStringValue()
457
521
  return ['str', args[0]] // string literal
458
522
  }
459
523
  return [, args[0]] // number literal
@@ -483,6 +547,7 @@ export const GLOBALS = {
483
547
  Object: 'Object',
484
548
  Symbol: 'Symbol',
485
549
  JSON: 'JSON',
550
+ Date: 'Date',
486
551
  isNaN: 'number',
487
552
  isFinite: 'number',
488
553
  parseInt: 'number',
@@ -496,6 +561,34 @@ export const GLOBALS = {
496
561
  const patternItems = (node) => node?.[0] === ',' ? node.slice(1) : [node]
497
562
  const isDestructPattern = (node) => Array.isArray(node) && (node[0] === '[]' || node[0] === '{}')
498
563
 
564
+ const simpleArrayPatternItems = (pattern) => {
565
+ if (!Array.isArray(pattern) || pattern[0] !== '[]' || pattern.length !== 2) return null
566
+ const items = patternItems(pattern[1])
567
+ return items.every(item => typeof item === 'string') ? items : null
568
+ }
569
+
570
+ const arrayLiteralItems = (expr) => {
571
+ if (!Array.isArray(expr) || expr[0] !== '[]' || expr.length !== 2) return null
572
+ if (expr[1] == null) return []
573
+ const items = patternItems(expr[1])
574
+ return items.every(item => item != null && !(Array.isArray(item) && item[0] === '...')) ? items : null
575
+ }
576
+
577
+ function scalarArrayDestruct(pattern, rhs) {
578
+ const targets = simpleArrayPatternItems(pattern)
579
+ const values = arrayLiteralItems(rhs)
580
+ if (!targets || !values || targets.length !== values.length) return null
581
+
582
+ const decls = []
583
+ const assigns = []
584
+ for (let i = 0; i < targets.length; i++) {
585
+ const tmp = `${T}d${ctx.func.uniq++}`
586
+ decls.push(['=', tmp, prep(values[i])])
587
+ assigns.push(['=', targets[i], tmp])
588
+ }
589
+ return prep([';', ['let', ...decls], ...assigns])
590
+ }
591
+
499
592
  function pushPatternAssign(target, valueExpr, out, decls = null) {
500
593
  if (Array.isArray(target) && target[0] === '=') {
501
594
  pushPatternAssign(target[1], ['??', valueExpr, prep(target[2])], out, decls)
@@ -517,7 +610,7 @@ function expandDestruct(pattern, source, out, decls = null) {
517
610
  if (!isDestructPattern(pattern)) return
518
611
 
519
612
  if (pattern[0] === '[]') {
520
- includeMods('core', 'array', 'collection')
613
+ includeForArrayPattern()
521
614
  const items = patternItems(pattern[1])
522
615
  for (let j = 0; j < items.length; j++) {
523
616
  const item = items[j]
@@ -533,7 +626,7 @@ function expandDestruct(pattern, source, out, decls = null) {
533
626
  return
534
627
  }
535
628
 
536
- includeMods('core', 'object', 'string', 'collection')
629
+ includeForObjectPattern()
537
630
  const items = patternItems(pattern[1])
538
631
 
539
632
  // Collect explicit keys and detect rest pattern
@@ -592,6 +685,11 @@ function expandDestruct(pattern, source, out, decls = null) {
592
685
  function prepDecl(op, ...inits) {
593
686
  const rest = []
594
687
  for (const i of inits) {
688
+ if (Array.isArray(i) && i[0] === '()' && typeof i[1] === 'string' && Array.isArray(i[2]) && i[2][0] === '=' && isDestructPattern(i[2][1])) {
689
+ if (rest.length === 0 && inits.length === 1) return [';', [op, i[1]], prep(i[2])]
690
+ err('destructuring assignment after declaration must be a separate statement')
691
+ }
692
+
595
693
  if (!Array.isArray(i) || i[0] !== '=') { rest.push(i); continue }
596
694
  const [, name, init] = i, normed = prep(init)
597
695
 
@@ -611,13 +709,18 @@ function prepDecl(op, ...inits) {
611
709
 
612
710
  if (!defFunc(name, normed)) {
613
711
  let declName = name
614
- // Block scope: rename if shadowing an outer declaration
615
- if (typeof name === 'string' && scopes.length > 0 && isDeclared(name)) {
712
+ // Block scope: rename if shadowing an outer declaration, OR if a sibling
713
+ // block at the same arrow scope already declared this name (sibling
714
+ // blocks both lower to the same WASM local; see funcLocalNames comment).
715
+ const fnNames = funcLocalNames[funcLocalNames.length - 1]
716
+ const inCurrentBlock = scopes.length > 0 && scopes[scopes.length - 1].has(name)
717
+ if (typeof name === 'string' && scopes.length > 0 && (isDeclared(name) || (fnNames?.has(name) && !inCurrentBlock))) {
616
718
  declName = `${name}${T}${ctx.func.uniq++}`
617
719
  scopes[scopes.length - 1].set(name, declName)
618
720
  } else if (typeof name === 'string' && scopes.length > 0) {
619
721
  scopes[scopes.length - 1].set(name, name)
620
722
  }
723
+ if (typeof declName === 'string' && fnNames) fnNames.add(declName)
621
724
  // Track const for reassignment checks — only module-scope consts (depth 0)
622
725
  if (typeof declName === 'string' && depth === 0) {
623
726
  if (ctx.module.currentPrefix) {
@@ -629,10 +732,20 @@ function prepDecl(op, ...inits) {
629
732
  ctx.scope.consts.add(declName)
630
733
  if (Array.isArray(normed) && normed[0] === 'str' && typeof normed[1] === 'string')
631
734
  (ctx.scope.constStrs ||= new Map()).set(declName, normed[1])
735
+ const strs = stringArrayValues(normed)
736
+ if (strs) (ctx.scope.shapeStrArrays ||= new Map()).set(declName, strs)
632
737
  } else if (op === 'let' && ctx.scope.consts?.has(declName)) {
633
738
  ctx.scope.consts.delete(declName)
634
739
  ctx.scope.constStrs?.delete(declName)
740
+ ctx.scope.shapeStrArrays?.delete(declName)
635
741
  }
742
+ // Effectively-const string literals: shape inference for `let SRC = '{...}'`
743
+ // patterns (bench convention to defeat compile-time JSON.parse fold without
744
+ // losing schema knowledge). Recorded on init; post-prep scan removes any
745
+ // entry whose name is later assigned to.
746
+ if (Array.isArray(normed) && normed[0] === 'str' && typeof normed[1] === 'string')
747
+ (ctx.scope.shapeStrs ||= new Map()).set(declName, normed[1])
748
+ recordGlobalValueFact(declName, normed)
636
749
  }
637
750
  // Track object schemas (after prefix so schema is keyed to final name)
638
751
  if (typeof declName === 'string' && Array.isArray(normed) && normed[0] === '{}' && normed.length > 1) {
@@ -662,8 +775,7 @@ function prepDecl(op, ...inits) {
662
775
  const handlers = {
663
776
  // Spread operator: [...expr] in arrays, f(...args) in calls, {...obj} in objects
664
777
  '...'(expr) {
665
- // Spread is valid in arrays, calls, and objects - just prep the inner expression
666
- includeModule('array')
778
+ includeForArrayLiteral()
667
779
  return ['...', prep(expr)]
668
780
  },
669
781
 
@@ -673,6 +785,7 @@ const handlers = {
673
785
  'await': () => err('async/await not supported: WASM is synchronous'),
674
786
  'class': () => err('class not supported: use object literals'),
675
787
  'yield': () => err('generators not supported: use loops'),
788
+ 'debugger': () => null,
676
789
  'delete': () => err('delete not supported: object shape is fixed'),
677
790
  'in'(key, obj) { return ['in', prep(key), prep(obj)] },
678
791
  'instanceof': () => err('instanceof not supported: use typeof'),
@@ -686,6 +799,9 @@ const handlers = {
686
799
  // Destructuring assignment: [a, ...r] = expr or ({x: a} = expr)
687
800
  // Distinguishing from index assignment: destructuring patterns have exactly one payload node.
688
801
  if (isDestructPattern(lhs) && lhs.length === 2) {
802
+ const scalar = scalarArrayDestruct(lhs, rhs)
803
+ if (scalar) return scalar
804
+
689
805
  const normed = prep(rhs)
690
806
  const tmp = `${T}d${ctx.func.uniq++}`
691
807
  const decls = [['=', tmp, normed]]
@@ -708,9 +824,9 @@ const handlers = {
708
824
  }
709
825
  // Function property assignment: fn.prop = arrow → extract as top-level function fn$prop
710
826
  if (depth === 0 && Array.isArray(lhs) && lhs[0] === '.' && typeof lhs[1] === 'string'
711
- && ctx.func.list.some(f => f.name === lhs[1]) && Array.isArray(rhs) && rhs[0] === '=>') {
827
+ && hasFunc(lhs[1]) && Array.isArray(rhs) && rhs[0] === '=>') {
712
828
  const name = `${lhs[1]}$${lhs[2]}`
713
- if (defFunc(name, prep(rhs))) return null // extracted as function, no assignment needed
829
+ if (defFunc(name, prep(rhs))) return ['=', prep(lhs), name]
714
830
  }
715
831
  return ['=', prep(lhs), prep(rhs)]
716
832
  },
@@ -736,16 +852,12 @@ const handlers = {
736
852
  },
737
853
  'throw'(expr) { return ['throw', prep(expr)] },
738
854
 
739
- // Template literal: [``, part, ...] → chain of str_concat calls
740
- // First node is always a string (empty if template starts with ${...}) so concat dispatches correctly.
855
+ // Template literal: [``, part, ...] → fused single-allocation string concat.
741
856
  '`'(...parts) {
742
- includeMods('core', 'string', 'number')
857
+ includeForStringValue()
743
858
  const nodes = parts.map(p =>
744
859
  Array.isArray(p) && p[0] == null && typeof p[1] === 'string' ? ['str', p[1]] : prep(p))
745
- // Ensure first element is a string so concat chain starts with string dispatch
746
- if (nodes.length && !(Array.isArray(nodes[0]) && nodes[0][0] === 'str'))
747
- nodes.unshift(['str', ''])
748
- return nodes.reduce((acc, n) => ['()', ['.', acc, 'concat'], n])
860
+ return ['strcat', ...nodes]
749
861
  },
750
862
 
751
863
  // Tagged template: tag`a${x}b` → tag(['a','b'], x)
@@ -781,7 +893,7 @@ const handlers = {
781
893
  if (hostMod && Array.isArray(specifiers) && specifiers[0] === '{}') {
782
894
  const inner = specifiers[1]
783
895
  if (inner != null) {
784
- const items = Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]
896
+ const items = (Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]).filter(x => x != null)
785
897
  const builtinItems = []
786
898
  for (const item of items) {
787
899
  const name = typeof item === 'string' ? item : item[1]
@@ -794,7 +906,7 @@ const handlers = {
794
906
  }
795
907
  }
796
908
  if (builtinItems.length === 0) return null
797
- if (!mods[MOD_ALIAS[mod] || mod]) {
909
+ if (!hasModule(mod)) {
798
910
  const name = typeof builtinItems[0] === 'string' ? builtinItems[0] : builtinItems[0][1]
799
911
  err(`'${name}' not declared in host module '${mod}'`)
800
912
  }
@@ -805,7 +917,7 @@ const handlers = {
805
917
  }
806
918
 
807
919
  // Tier 1: Built-in module
808
- if (mods[MOD_ALIAS[mod] || mod]) {
920
+ if (hasModule(mod)) {
809
921
  includeModule(mod)
810
922
  const bind = (name, alias) => {
811
923
  const key = mod + '.' + name
@@ -819,7 +931,7 @@ const handlers = {
819
931
  if (Array.isArray(remaining) && remaining[0] === '{}') {
820
932
  const inner = remaining[1]
821
933
  if (inner == null) return null
822
- const items = Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]
934
+ const items = (Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]).filter(x => x != null)
823
935
  for (const item of items)
824
936
  if (typeof item === 'string') bind(item)
825
937
  else if (Array.isArray(item) && item[0] === 'as') bind(item[1], item[2])
@@ -850,7 +962,7 @@ const handlers = {
850
962
  if (Array.isArray(specifiers) && specifiers[0] === '{}') {
851
963
  const inner = specifiers[1]
852
964
  if (inner == null) return null
853
- const items = Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]
965
+ const items = (Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]).filter(x => x != null)
854
966
  for (const item of items) {
855
967
  const name = typeof item === 'string' ? item : item[1]
856
968
  const alias = typeof item === 'string' ? item : item[2]
@@ -867,7 +979,7 @@ const handlers = {
867
979
  if (Array.isArray(specifiers) && specifiers[0] === '{}') {
868
980
  const inner = specifiers[1]
869
981
  if (inner == null) return null
870
- const items = Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]
982
+ const items = (Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]).filter(x => x != null)
871
983
  for (const item of items) {
872
984
  const name = typeof item === 'string' ? item : item[1]
873
985
  const alias = typeof item === 'string' ? item : item[2]
@@ -909,6 +1021,34 @@ const handlers = {
909
1021
  for (const i of decl.slice(1))
910
1022
  if (Array.isArray(i) && i[0] === '=' && typeof i[1] === 'string')
911
1023
  ctx.func.exports[i[1]] = true
1024
+ // export { name, name as alias } from './mod' or export * from './mod'
1025
+ if (Array.isArray(decl) && decl[0] === 'from') {
1026
+ const mod = decl[2]?.[1]
1027
+ if (!mod || typeof mod !== 'string') return null
1028
+ // Source module re-export
1029
+ if (ctx.module.importSources?.[mod]) {
1030
+ const resolved = prepareModule(mod, ctx.module.importSources[mod])
1031
+ if (decl[1] === '*') {
1032
+ // export * from './mod' → register all exports
1033
+ for (const [name, mangled] of resolved.exports) {
1034
+ if (name !== 'default') ctx.func.exports[name] = mangled
1035
+ }
1036
+ } else if (Array.isArray(decl[1]) && decl[1][0] === '{}') {
1037
+ // export { a, b as c } from './mod'
1038
+ const inner = decl[1][1]
1039
+ if (inner == null) return null
1040
+ const items = (Array.isArray(inner) && inner[0] === ',' ? inner.slice(1) : [inner]).filter(x => x != null)
1041
+ for (const item of items) {
1042
+ const name = typeof item === 'string' ? item : item[1]
1043
+ const alias = typeof item === 'string' ? item : item[2]
1044
+ const mangled = resolved.exports.get(name)
1045
+ if (!mangled) err(`'${name}' is not exported from '${mod}'`)
1046
+ ctx.func.exports[alias] = mangled
1047
+ }
1048
+ }
1049
+ }
1050
+ return null
1051
+ }
912
1052
  // export { name1, name2 as alias } → register named exports
913
1053
  if (Array.isArray(decl) && decl[0] === '{}') {
914
1054
  const inner = decl[1]
@@ -930,7 +1070,7 @@ const handlers = {
930
1070
  if (Array.isArray(decl) && decl[0] === 'default') {
931
1071
  const val = decl[1]
932
1072
  // export default name → export existing name as 'default'
933
- if (typeof val === 'string' && (ctx.func.list.some(f => f.name === val) || ctx.scope.globals.has(val))) {
1073
+ if (typeof val === 'string' && (hasFunc(val) || ctx.scope.globals.has(val))) {
934
1074
  ctx.func.exports['default'] = val // alias
935
1075
  return null
936
1076
  }
@@ -949,13 +1089,14 @@ const handlers = {
949
1089
 
950
1090
  // Arrow: don't prep params. Track depth for nested function detection.
951
1091
  '=>': (params, body) => {
952
- if (depth > 0) { includeMods('core', 'fn') }
1092
+ if (depth > 0) { includeForCallableValue() }
953
1093
  const raw = extractParams(params)
954
1094
  const fnScope = new Map()
955
1095
  for (const n of collectParamNames(raw)) fnScope.set(n, n)
956
1096
 
957
1097
  depth++
958
1098
  scopes.push(fnScope)
1099
+ funcLocalNames.push(new Set(collectParamNames(raw)))
959
1100
 
960
1101
  const nextParams = []
961
1102
  const bodyPrefix = []
@@ -988,6 +1129,7 @@ const handlers = {
988
1129
  const inner = nextParams.length === 0 ? null : nextParams.length === 1 ? nextParams[0] : [',', ...nextParams]
989
1130
  const result = ['=>', Array.isArray(params) && params[0] === '()' ? ['()', inner] : inner, preparedBody]
990
1131
  scopes.pop()
1132
+ funcLocalNames.pop()
991
1133
  depth--
992
1134
  return result
993
1135
  },
@@ -1019,7 +1161,9 @@ const handlers = {
1019
1161
  },
1020
1162
  // Boolean literals NaN-box as f64 — typeof at runtime returns 'number'. Fold here so the JS-spec value survives.
1021
1163
  'typeof'(a) {
1022
- if (Array.isArray(a) && a[0] == null && typeof a[1] === 'boolean') { includeMods('core', 'string'); return ['str', 'boolean'] }
1164
+ if (Array.isArray(a) && a[0] == null && typeof a[1] === 'boolean') { includeForStringOnly(); return ['str', 'boolean'] }
1165
+ const known = staticTypeofString(a)
1166
+ if (known != null) { includeForStringOnly(); return ['str', known] }
1023
1167
  return ['typeof', prep(a)]
1024
1168
  },
1025
1169
 
@@ -1028,7 +1172,7 @@ const handlers = {
1028
1172
  if (b === undefined) {
1029
1173
  const na = prep(a)
1030
1174
  if (isLit(na) && typeof na[1] === 'number') return na
1031
- includeMods('number', 'string')
1175
+ includeForNumericCoercion()
1032
1176
  return ['u+', na]
1033
1177
  }
1034
1178
  return ['+', prep(a), prep(b)]
@@ -1060,36 +1204,43 @@ const handlers = {
1060
1204
  return ['//', pattern, flags]
1061
1205
  },
1062
1206
 
1063
- // auto-include math for ** operator
1064
- '**'(a, b) { includeModule('math'); return ['**', prep(a), prep(b)] },
1207
+ '**'(a, b) { return ['**', prep(a), prep(b)] },
1065
1208
 
1066
1209
  // Function call or grouping parens
1067
1210
  '()'(callee, ...args) {
1068
1211
  // Grouping: (expr) → ['()', expr] with no args. Call: f() → ['()', 'f', null] with null arg.
1069
1212
  if (args.length === 0) return prep(callee)
1070
1213
 
1214
+ if (isImportMetaProp(callee, 'resolve')) {
1215
+ const callArgs = flatArgs(args).filter(a => a != null)
1216
+ if (callArgs.length !== 1) err('`import.meta.resolve` requires one string literal argument')
1217
+ const spec = stringValue(callArgs[0])
1218
+ if (spec == null) err('`import.meta.resolve` supports only string literal arguments')
1219
+ return staticString(resolveImportMeta(spec))
1220
+ }
1221
+
1071
1222
  const hasRealArgs = args.some(a => a != null)
1072
1223
 
1073
1224
  if (typeof callee === 'string') {
1074
1225
  if (PROHIBITED[callee]) err(PROHIBITED[callee])
1075
1226
  if (CTORS.includes(callee)) return handlers['new'](['()', callee, ...args])
1076
1227
 
1077
- const mods = CALL_MODULES[callee]
1078
- if (mods) {
1079
- includeMods(...mods)
1228
+ if (includeForNamedCall(callee)) {
1080
1229
  if (callee === 'BigInt64Array' || callee === 'BigUint64Array') {
1081
1230
  return ['()', callee, ...args.filter(a => a != null).map(prep)]
1082
1231
  }
1083
1232
  }
1084
1233
 
1085
- const resolved = ctx.scope.chain[callee]
1086
- if (resolved?.includes('.')) callee = resolved
1087
- else if (resolved && ctx.func.list.some(f => f.name === resolved)) callee = resolved
1234
+ const local = scopes.length && isDeclared(callee)
1235
+ const resolved = local ? null : ctx.scope.chain[callee]
1236
+ if (local) callee = resolveScope(callee)
1237
+ else if (resolved?.includes('.')) callee = resolved
1238
+ else if (resolved && hasFunc(resolved)) callee = resolved
1088
1239
  else if (resolved && !resolved.includes('.')) {
1089
- if (!ctx.module.imports.some(i => i[3]?.[1] === `$${resolved}`)) includeModule(resolved)
1240
+ if (hasModule(resolved) && !ctx.module.imports.some(i => i[3]?.[1] === `$${resolved}`)) includeModule(resolved)
1090
1241
  }
1091
1242
  else if (depth > 0 && !resolved && !ctx.func.exports[callee] && !ctx.module.imports.some(i => i[3]?.[1] === `$${callee}`)) {
1092
- includeMods('core', 'fn')
1243
+ includeForCallableValue()
1093
1244
  }
1094
1245
  } else if (Array.isArray(callee) && callee[0] === '.') {
1095
1246
  const [, obj, prop] = callee
@@ -1099,29 +1250,37 @@ const handlers = {
1099
1250
  const alias = `${obj}$${prop}`
1100
1251
  addHostImport(obj, prop, alias, spec)
1101
1252
  callee = alias
1102
- } else if (key && CALL_MODULES[key]) {
1103
- includeMods(...CALL_MODULES[key])
1253
+ } else if (key && includeForNamedCall(key)) {
1104
1254
  callee = key
1105
- } else if (GENERIC_METHOD_MODULES[prop]) {
1106
- includeMods(...GENERIC_METHOD_MODULES[prop])
1255
+ } else if (includeForGenericMethod(prop)) {
1107
1256
  callee = prep(callee)
1108
1257
  } else {
1109
1258
  const mod = ctx.scope.chain[obj]
1110
- if (typeof obj === 'string' && mod && !mod.includes('.') && mods[MOD_ALIAS[mod] || mod]) {
1259
+ if (typeof obj === 'string' && mod && !mod.includes('.') && hasModule(mod)) {
1111
1260
  callee = (includeModule(mod), mod + '.' + prop)
1112
1261
  } else {
1113
1262
  callee = prep(callee)
1114
1263
  }
1115
1264
  }
1116
1265
  } else {
1117
- includeMods('core', 'fn')
1266
+ includeForCallableValue()
1118
1267
  callee = prep(callee)
1119
1268
  }
1120
1269
 
1270
+ // Drop trailing-comma sentinel inside a comma group: `f(a, b,)` parses as
1271
+ // ['()', 'f', [',', a, b, null]] — without trimming, the trailing null
1272
+ // becomes a [, 0] literal and inflates arguments.length.
1273
+ if (args.length === 1 && Array.isArray(args[0]) && args[0][0] === ',') {
1274
+ let end = args[0].length
1275
+ while (end > 1 && args[0][end - 1] == null) end--
1276
+ if (end < args[0].length) {
1277
+ args[0] = end === 2 ? args[0][1] : args[0].slice(0, end)
1278
+ }
1279
+ }
1121
1280
  const preppedArgs = args.filter(a => a != null).map(prep)
1122
1281
  for (const a of preppedArgs) {
1123
- if (typeof a === 'string' && ctx.func.list.some(f => f.name === a)) {
1124
- includeMods('core', 'fn'); break
1282
+ if (typeof a === 'string' && hasFunc(a)) {
1283
+ includeForCallableValue(); break
1125
1284
  }
1126
1285
  }
1127
1286
  const result = ['()', callee, ...preppedArgs]
@@ -1135,13 +1294,13 @@ const handlers = {
1135
1294
  '[]'(...args) {
1136
1295
  if (args.length === 1) {
1137
1296
  const inner = args[0]
1138
- includeMods('core', 'array')
1297
+ includeForArrayLiteral()
1139
1298
  if (inner == null) return ['[']
1140
1299
  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))] }
1141
1300
  return ['[', prep(inner)]
1142
1301
  }
1143
1302
  if (typeof args[0] === 'string' && ctx.module.namespaces?.[args[0]]) {
1144
- includeMods('core', 'string')
1303
+ includeForStringOnly()
1145
1304
  const key = prep(args[1])
1146
1305
  const exports = [...ctx.module.namespaces[args[0]].entries()]
1147
1306
  let fallback = [, undefined]
@@ -1151,7 +1310,7 @@ const handlers = {
1151
1310
  }
1152
1311
  return fallback
1153
1312
  }
1154
- includeMods('core', 'array', 'collection')
1313
+ includeForArrayAccess()
1155
1314
  return ['[]', prep(args[0]), prep(args[1])]
1156
1315
  },
1157
1316
 
@@ -1174,17 +1333,40 @@ const handlers = {
1174
1333
  return result
1175
1334
  }
1176
1335
 
1177
- includeMods('core', 'object')
1336
+ includeForObjectLiteral()
1178
1337
  if (inner == null) return ['{}']
1179
1338
  // Process properties: shorthand 'x' → [':', 'x', 'x'], or [':', key, val] → prep val only
1180
1339
  const prop = p => {
1181
1340
  if (typeof p === 'string') return [':', p, prep(p)]
1182
- if (Array.isArray(p) && p[0] === ':') return [':', p[1], prep(p[2])]
1341
+ if (Array.isArray(p) && p[0] === ':') {
1342
+ const key = typeof p[1] === 'string' ? p[1] : staticPropertyKey(p[1])
1343
+ if (key == null) err('computed property name not supported for fixed-shape object: use a compile-time string/number key')
1344
+ return [':', key, prep(p[2])]
1345
+ }
1183
1346
  return prep(p)
1184
1347
  }
1185
- const result = Array.isArray(inner) && inner[0] === ','
1186
- ? ['{}', ...inner.slice(1).map(prop)]
1187
- : ['{}', prop(inner)]
1348
+ // Drop trailing-comma artifacts: subscript represents `{a:1, b,}` as
1349
+ // `[",", [":","a",1], "b", null]` — the trailing `null` would prep to a
1350
+ // literal-0 entry, leaving the literal carrying a phantom slot and
1351
+ // shifting any subsequent slot-position resolution.
1352
+ const items = Array.isArray(inner) && inner[0] === ','
1353
+ ? inner.slice(1).filter(p => p != null)
1354
+ : [inner]
1355
+ let prepped = items.map(prop)
1356
+ // ES spec: duplicate keys allowed; key takes first-seen position, last-seen value.
1357
+ const lastValue = new Map()
1358
+ for (const p of prepped) if (Array.isArray(p) && p[0] === ':') lastValue.set(p[1], p[2])
1359
+ if (lastValue.size < prepped.filter(p => Array.isArray(p) && p[0] === ':').length) {
1360
+ const seen = new Set()
1361
+ prepped = prepped.filter(p => {
1362
+ if (!Array.isArray(p) || p[0] !== ':') return true
1363
+ if (seen.has(p[1])) return false
1364
+ seen.add(p[1])
1365
+ p[2] = lastValue.get(p[1])
1366
+ return true
1367
+ })
1368
+ }
1369
+ const result = ['{}', ...prepped]
1188
1370
  // Register schema so property access works for function params (duck typing)
1189
1371
  const props = result.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
1190
1372
  if (props.length && ctx.schema.register) ctx.schema.register(props)
@@ -1198,15 +1380,19 @@ const handlers = {
1198
1380
  if (Array.isArray(head) && head[0] === ';') {
1199
1381
  let [, init, cond, step] = head
1200
1382
  // Hoist .length / .size / .byteLength from for-condition:
1201
- // `i < arr.length` → `let __len = arr.length; ... i < __len`
1202
- // All three return i32 (TYPED/ARRAY/STRING/BUFFER/SET/MAP), so the hoisted
1203
- // local stays i32 once exprType narrows it.
1383
+ // `i < arr.length` → `let __len = arr.length | 0; ... i < __len`
1384
+ // The `| 0` forces i32 even for unknown-typed receivers (where __length
1385
+ // returns f64). NaN→0 via i32.trunc_sat matches JS semantics: a NaN bound
1386
+ // makes `i < NaN` false on both representations, so the loop is skipped
1387
+ // either way. Keeping the hoisted bound i32 lets the counter `i` stay i32
1388
+ // through the comparison and `i++`, eliminating the per-iteration
1389
+ // f64.convert_i32_s + f64.lt + f64.add + i32.trunc_sat_f64_s sequence.
1204
1390
  if (cond && Array.isArray(cond) && (cond[0] === '<' || cond[0] === '<=' || cond[0] === '>' || cond[0] === '>=')) {
1205
1391
  const lenExpr = cond[0] === '<' || cond[0] === '<=' ? cond[2] : cond[1]
1206
1392
  if (Array.isArray(lenExpr) && lenExpr[0] === '.' &&
1207
1393
  (lenExpr[2] === 'length' || lenExpr[2] === 'size' || lenExpr[2] === 'byteLength')) {
1208
1394
  const lenVar = `${T}len${ctx.func.uniq++}`
1209
- const lenDecl = ['let', ['=', lenVar, lenExpr]]
1395
+ const lenDecl = ['let', ['=', lenVar, ['|', lenExpr, [, 0]]]]
1210
1396
  init = init ? [';', init, lenDecl] : lenDecl
1211
1397
  if (cond[0] === '<' || cond[0] === '<=') cond = [cond[0], cond[1], lenVar]
1212
1398
  else cond = [cond[0], lenVar, cond[2]]
@@ -1223,9 +1409,12 @@ const handlers = {
1223
1409
  const lenVar = `${T}len${ctx.func.uniq++}`
1224
1410
  const trivial = typeof src === 'string'
1225
1411
  const arrVar = trivial ? src : `${T}arr${ctx.func.uniq++}`
1412
+ // Wrap .length in `| 0` so the hoisted bound is i32 even for unknown
1413
+ // receivers (same rationale as the for-cond hoist above).
1414
+ const lenE = ['|', ['.', arrVar, 'length'], [, 0]]
1226
1415
  const decls = trivial
1227
- ? ['let', ['=', idx, [, 0]], ['=', lenVar, ['.', arrVar, 'length']]]
1228
- : ['let', ['=', arrVar, src], ['=', idx, [, 0]], ['=', lenVar, ['.', arrVar, 'length']]]
1416
+ ? ['let', ['=', idx, [, 0]], ['=', lenVar, lenE]]
1417
+ : ['let', ['=', arrVar, src], ['=', idx, [, 0]], ['=', lenVar, lenE]]
1229
1418
  const cond = ['<', idx, lenVar]
1230
1419
  const step = ['++', idx]
1231
1420
  const inner = [';', ['let', ['=', varName, ['[]', arrVar, idx]]], body]
@@ -1233,14 +1422,14 @@ const handlers = {
1233
1422
  } else if (Array.isArray(head) && head[0] === 'in') {
1234
1423
  // for (let k in obj) → unroll at compile time when schema known, else HASH runtime iteration
1235
1424
  const [, decl, src] = head
1236
- const varName = Array.isArray(decl) && decl[0] === 'let' ? decl[1] : decl
1425
+ const varName = Array.isArray(decl) && (decl[0] === 'let' || decl[0] === 'const') ? decl[1] : decl
1237
1426
  const srcName = typeof src === 'string' ? (ctx.scope.chain[src] || src) : null
1238
- const sid = typeof srcName === 'string' && ctx.schema.vars.get(srcName)
1427
+ const sid = typeof srcName === 'string' ? ctx.schema.vars.get(srcName) : null
1239
1428
  if (sid != null) {
1240
1429
  // Known schema → compile-time unrolling with string keys
1241
1430
  const keys = ctx.schema.list[sid]
1242
1431
  if (!keys || !keys.length) { scopes.pop(); return null }
1243
- includeMods('core', 'string')
1432
+ includeForKnownKeyIteration()
1244
1433
  const stmts = []
1245
1434
  for (let i = 0; i < keys.length; i++) {
1246
1435
  stmts.push(i === 0
@@ -1251,7 +1440,7 @@ const handlers = {
1251
1440
  r = prep([';', ...stmts])
1252
1441
  } else {
1253
1442
  // Dynamic object → HASH runtime iteration
1254
- includeMods('core', 'string', 'collection')
1443
+ includeForRuntimeKeyIteration()
1255
1444
  r = ['for-in', varName, prep(src), prep(body)]
1256
1445
  }
1257
1446
  } else {
@@ -1263,24 +1452,22 @@ const handlers = {
1263
1452
 
1264
1453
  // Property access - resolve namespaces or object/array properties
1265
1454
  '.'(obj, prop) {
1455
+ if (prop === 'caller' || prop === 'callee') err('`.caller` and `.callee` are prohibited: deprecated stack introspection')
1456
+ if (prop === 'url' && isImportMeta(obj)) return staticString(importMetaUrl())
1266
1457
  const mod = ctx.scope.chain[obj]
1267
1458
  // Only treat as module namespace if it's a known built-in module (not a mangled import name)
1268
- if (typeof obj === 'string' && mod && !mod.includes('.') && mods[MOD_ALIAS[mod] || mod])
1269
- return includeModule(mod), mod + '.' + prop
1459
+ if (typeof obj === 'string' && mod && !mod.includes('.') && hasModule(mod)) {
1460
+ includeModule(mod)
1461
+ const key = mod + '.' + prop
1462
+ if (ctx.core.emit[key]?.length > 0) includeForCallableValue()
1463
+ return key
1464
+ }
1270
1465
  // Source module namespace: import * as X → X.prop resolved to mangled name
1271
1466
  if (typeof obj === 'string' && ctx.module.namespaces?.[obj]) {
1272
1467
  const mangled = ctx.module.namespaces[obj].get(prop)
1273
1468
  if (mangled) return mangled
1274
1469
  }
1275
- // Typed-array/buffer properties auto-include typedarray module
1276
- if (prop === 'byteLength' || prop === 'byteOffset' || prop === 'buffer') {
1277
- includeMods('core', 'typedarray')
1278
- }
1279
- // Narrowing: unambiguous property name limits module load; otherwise pessimistic.
1280
- // Receiver-literal narrowing would still need collection for __dyn_get_expr fallback
1281
- // on unknown props, so it saves nothing — stick with property-name narrowing only.
1282
- if (typeof prop === 'string' && PROP_MODULES[prop]) includeMods(...PROP_MODULES[prop])
1283
- else includeMods('core', 'object', 'array', 'string', 'collection')
1470
+ includeForProperty(prop)
1284
1471
  return ['.', prep(obj), prop]
1285
1472
  },
1286
1473
 
@@ -1292,20 +1479,21 @@ const handlers = {
1292
1479
  if (ctorArgs.length === 1 && Array.isArray(ctorArgs[0]) && ctorArgs[0][0] === ',')
1293
1480
  ctorArgs = ctorArgs[0].slice(1)
1294
1481
 
1482
+ if (name === 'URL') {
1483
+ const literalArgs = ctorArgs.filter(a => a != null)
1484
+ if (literalArgs.length === 2 && isImportMetaProp(literalArgs[1], 'url')) {
1485
+ const spec = stringValue(literalArgs[0])
1486
+ if (spec == null) err('`new URL(relative, import.meta.url)` supports only string literal relatives')
1487
+ return staticString(resolveImportMeta(spec))
1488
+ }
1489
+ }
1490
+
1295
1491
  // Wrap multi-arg ctor arg lists back into a single comma-group — the '()' op
1296
1492
  // expects callArgs as a single element (possibly comma-grouped).
1297
1493
  const wrapArgs = (args) => args.length === 0 ? [null]
1298
1494
  : args.length === 1 ? [prep(args[0])]
1299
1495
  : [[',', ...args.map(prep)]]
1300
- // TypedArray / buffer constructors
1301
- const typedArrays = ['Float64Array','Float32Array','Int32Array','Uint32Array','Int16Array','Uint16Array','Int8Array','Uint8Array','BigInt64Array','BigUint64Array','ArrayBuffer','DataView']
1302
- if (typedArrays.includes(name)) {
1303
- includeMods('core', 'typedarray')
1304
- return ['()', `new.${name}`, ...wrapArgs(ctorArgs)]
1305
- }
1306
- // Set/Map constructors
1307
- if (name === 'Set' || name === 'Map') {
1308
- includeMods('core', 'collection')
1496
+ if (includeForRuntimeCtor(name)) {
1309
1497
  return ['()', `new.${name}`, ...wrapArgs(ctorArgs)]
1310
1498
  }
1311
1499
 
@@ -1317,37 +1505,6 @@ const handlers = {
1317
1505
  }
1318
1506
  }
1319
1507
 
1320
- // Namespace → module mapping (namespaces that share a module)
1321
- const MOD_ALIAS = { Number: 'number', Array: 'array', Object: 'object', Symbol: 'symbol', JSON: 'json', BigInt: 'number', Error: 'core', TextEncoder: 'string', TextDecoder: 'string' }
1322
- /** Auto-inclusion graph: loading a module also loads its listed prerequisites.
1323
- * Not a strict ordering: module init() functions only register emitters/stdlib entries,
1324
- * so relative init order does not affect correctness — emitters are looked up lazily
1325
- * at compile time. Cycles (e.g. number ↔ string) are broken via the in-progress guard. */
1326
- const MOD_DEPS = {
1327
- number: ['core', 'string'],
1328
- string: ['core', 'number'],
1329
- array: ['core'],
1330
- object: ['core'],
1331
- collection: ['core', 'number'],
1332
- symbol: ['core'],
1333
- json: ['core', 'string', 'number', 'collection'],
1334
- console: ['core', 'string', 'number'],
1335
- regex: ['core', 'string', 'array'],
1336
- }
1337
-
1338
- const includeMods = (...names) => names.forEach(includeModule)
1339
-
1340
- /** Register a module and its transitive deps. Idempotent; cycle-safe via early-mark. */
1341
- function includeModule(name) {
1342
- const modName = MOD_ALIAS[name] || name
1343
- const init = mods[modName]
1344
- if (!init) return err(`Module not found: ${name}`)
1345
- if (ctx.module.modules[modName]) return
1346
- ctx.module.modules[modName] = true // mark before deps so cycles terminate
1347
- for (const dep of MOD_DEPS[modName] || []) includeModule(dep)
1348
- init(ctx) // modules receive ctx explicitly instead of importing it
1349
- }
1350
-
1351
1508
  /** Merge source schemas into target via Object.assign for compile-time schema inference. */
1352
1509
  function inferAssignSchema(callNode) {
1353
1510
  // After prep, args may be comma-grouped: ['()', callee, [',', target, s1, s2]]
@@ -1421,6 +1578,7 @@ function defFunc(name, node) {
1421
1578
  const funcInfo = { name, body, exported, sig, ...(hasDefaults && { defaults }) }
1422
1579
  if (hasRest.length) funcInfo.rest = hasRest[0] // track rest param name
1423
1580
  ctx.func.list.push(funcInfo)
1581
+ ctx.func.names.add(name)
1424
1582
  return true
1425
1583
  }
1426
1584
 
@@ -1493,6 +1651,19 @@ function prepareModule(specifier, source) {
1493
1651
 
1494
1652
  // Collect exports: rename exported funcs with prefix
1495
1653
  const moduleExports = new Map()
1654
+ const exportLocal = (exportName, localName) => {
1655
+ const mangled = `${prefix}$${localName}`
1656
+ moduleExports.set(exportName, mangled)
1657
+ const func = ctx.func.list.find(f => f.name === localName)
1658
+ if (func) renameFunc(func, mangled)
1659
+ if (ctx.scope.globals.has(localName)) {
1660
+ const wat = ctx.scope.globals.get(localName).replace(`$${localName}`, `$${mangled}`)
1661
+ ctx.scope.globals.delete(localName)
1662
+ ctx.scope.globals.set(mangled, wat)
1663
+ if (ctx.scope.userGlobals.has(localName)) { ctx.scope.userGlobals.delete(localName); ctx.scope.userGlobals.add(mangled) }
1664
+ if (ctx.scope.globalTypes.has(localName)) { ctx.scope.globalTypes.set(mangled, ctx.scope.globalTypes.get(localName)); ctx.scope.globalTypes.delete(localName) }
1665
+ }
1666
+ }
1496
1667
  for (const name of Object.keys(ctx.func.exports)) {
1497
1668
  const val = ctx.func.exports[name]
1498
1669
  // Default export alias: export default existingName → map 'default' to that name's mangled form
@@ -1500,19 +1671,29 @@ function prepareModule(specifier, source) {
1500
1671
  // Will resolve after all named exports are mangled
1501
1672
  continue
1502
1673
  }
1503
- const mangled = `${prefix}$${name}`
1504
- moduleExports.set(name, mangled)
1505
- // Rename the function in ctx.func.list
1506
- const func = ctx.func.list.find(f => f.name === name)
1507
- if (func) func.name = mangled
1508
- // Rename globals
1509
- if (ctx.scope.globals.has(name)) {
1510
- const wat = ctx.scope.globals.get(name).replace(`$${name}`, `$${mangled}`)
1511
- ctx.scope.globals.delete(name)
1512
- ctx.scope.globals.set(mangled, wat)
1513
- if (ctx.scope.userGlobals.has(name)) { ctx.scope.userGlobals.delete(name); ctx.scope.userGlobals.add(mangled) }
1514
- if (ctx.scope.globalTypes.has(name)) { ctx.scope.globalTypes.set(mangled, ctx.scope.globalTypes.get(name)); ctx.scope.globalTypes.delete(name) }
1674
+ // Re-export alias: export { x } from './mod' → pass through inner module's mangled name
1675
+ if (typeof val === 'string') {
1676
+ if (val.startsWith(prefix + '$')) {
1677
+ moduleExports.set(name, val)
1678
+ continue
1679
+ }
1680
+ // Re-export of a binding imported from another module: val already carries
1681
+ // that other module's prefix (e.g. `__c$x`). Renaming it under our own
1682
+ // prefix would break in-module call sites that still reference the
1683
+ // original mangled name. Pass through verbatim.
1684
+ if (val.includes('$') &&
1685
+ (ctx.func.list.some(f => f.name === val) || ctx.scope.globals.has(val))) {
1686
+ moduleExports.set(name, val)
1687
+ continue
1688
+ }
1689
+ if (ctx.func.list.some(f => f.name === val || f.name === `${prefix}$${val}`) || ctx.scope.globals.has(val) || ctx.scope.globals.has(`${prefix}$${val}`)) {
1690
+ exportLocal(name, val)
1691
+ continue
1692
+ }
1693
+ moduleExports.set(name, val)
1694
+ continue
1515
1695
  }
1696
+ exportLocal(name, name)
1516
1697
  }
1517
1698
  // Resolve default export alias after named exports are mangled
1518
1699
  if (typeof ctx.func.exports['default'] === 'string') {
@@ -1525,7 +1706,7 @@ function prepareModule(specifier, source) {
1525
1706
  const mangled = `${prefix}$${alias}`
1526
1707
  moduleExports.set('default', mangled)
1527
1708
  const func = ctx.func.list.find(f => f.name === alias)
1528
- if (func) func.name = mangled
1709
+ if (func) renameFunc(func, mangled)
1529
1710
  if (ctx.scope.globals.has(alias)) {
1530
1711
  const wat = ctx.scope.globals.get(alias).replace(`$${alias}`, `$${mangled}`)
1531
1712
  ctx.scope.globals.delete(alias)
@@ -1544,7 +1725,7 @@ function prepareModule(specifier, source) {
1544
1725
  if (func.name.includes('__') && func.name.includes('$')) continue
1545
1726
  const mangled = `${prefix}$${func.name}`
1546
1727
  moduleExports.set(func.name, mangled)
1547
- func.name = mangled
1728
+ renameFunc(func, mangled)
1548
1729
  }
1549
1730
 
1550
1731
  // Add mangled non-exported globals to moduleExports for walk renaming
@@ -1584,6 +1765,7 @@ function prepareModule(specifier, source) {
1584
1765
  if (moduleInit) {
1585
1766
  if (!ctx.module.moduleInits) ctx.module.moduleInits = []
1586
1767
  ctx.module.moduleInits.push(moduleInit)
1768
+ recordModuleInitFacts(moduleInit)
1587
1769
  }
1588
1770
 
1589
1771
  // Restore caller state