jz 0.1.1 → 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,11 +22,10 @@
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, valTypeOf, typedElemCtor, extractParams, collectParamNames, classifyParam } from './analyze.js'
28
- import { staticPropertyKey } from './key.js'
29
- import { normalizeSource } from './source.js'
27
+ import { T, STMT_OPS, VAL, valTypeOf, typedElemCtor, extractParams, collectParamNames, classifyParam, observeNodeFacts, staticPropertyKey } from './analyze.js'
28
+ import { isFuncRef } from './ir.js'
30
29
  import {
31
30
  CTORS, TIMER_NAMES,
32
31
  hasModule, includeModule,
@@ -38,6 +37,30 @@ import {
38
37
 
39
38
  let depth = 0 // arrow nesting depth (0=top-level, >0=inside function)
40
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
+ }
41
64
 
42
65
  const hostReturnValType = spec => {
43
66
  if (!spec || typeof spec === 'function') return null
@@ -50,15 +73,51 @@ const hostReturnValType = spec => {
50
73
 
51
74
  const addHostImport = (mod, name, alias, spec) => {
52
75
  const nParams = typeof spec === 'function' ? spec.length : (spec?.params || 0)
53
- 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'])
54
81
  if (!ctx.module.imports.some(i => i[3]?.[1] === `$${alias}`)) {
55
- 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']]])
56
83
  }
57
84
  ctx.scope.chain[alias] = alias
58
85
  const vt = hostReturnValType(spec)
59
86
  if (vt) ctx.module.hostImportValTypes.set(alias, vt)
60
87
  }
61
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
+
62
121
  function recordGlobalValueFact(name, expr) {
63
122
  if (typeof name !== 'string') return
64
123
  const vt = valTypeOf(expr)
@@ -76,26 +135,25 @@ function recordModuleInitFacts(root) {
76
135
  hasFuncValue: false, timerNames: new Set(),
77
136
  maxDef: 0, maxCall: 0, hasRest: false, hasSpread: false,
78
137
  }
79
- const isLiteralStr = idx => Array.isArray(idx) && idx[0] === 'str' && typeof idx[1] === 'string'
80
138
  const visitFuncValue = (node) => {
81
139
  if (facts.hasFuncValue || !Array.isArray(node)) return
82
140
  const [op, ...args] = node
83
141
  if (op === '()') {
84
142
  for (let i = 1; i < args.length; i++) {
85
143
  const a = args[i]
86
- if (typeof a === 'string' && ctx.func.names.has(a)) { facts.hasFuncValue = true; return }
144
+ if (isFuncRef(a, ctx.func.names)) { facts.hasFuncValue = true; return }
87
145
  visitFuncValue(a)
88
146
  }
89
147
  return
90
148
  }
91
149
  if (op === '.' || op === '?.') {
92
- if (typeof args[0] === 'string' && ctx.func.names.has(args[0])) { facts.hasFuncValue = true; return }
150
+ if (isFuncRef(args[0], ctx.func.names)) { facts.hasFuncValue = true; return }
93
151
  visitFuncValue(args[0])
94
152
  return
95
153
  }
96
154
  if (op === '=>') { visitFuncValue(args[1]); return }
97
155
  for (const a of args) {
98
- if (typeof a === 'string' && ctx.func.names.has(a)) { facts.hasFuncValue = true; return }
156
+ if (isFuncRef(a, ctx.func.names)) { facts.hasFuncValue = true; return }
99
157
  visitFuncValue(a)
100
158
  }
101
159
  }
@@ -104,29 +162,8 @@ function recordModuleInitFacts(root) {
104
162
  if (typeof node === 'string' && TIMER_NAMES.has(node)) facts.timerNames.add(node)
105
163
  return
106
164
  }
107
- const [op, ...args] = node
108
- if (op === '[]') {
109
- const [obj, idx] = args
110
- if (!isLiteralStr(idx)) { facts.anyDyn = true; if (typeof obj === 'string') facts.dynVars.add(obj) }
111
- } else if (op === 'for-in') {
112
- facts.anyDyn = true
113
- if (typeof args[1] === 'string') facts.dynVars.add(args[1])
114
- } else if (op === '{}') {
115
- facts.hasSchemaLiterals = true
116
- } else if (op === '=>') {
117
- let fixedN = 0
118
- for (const r of extractParams(args[0])) {
119
- if (classifyParam(r).kind === 'rest') facts.hasRest = true
120
- else fixedN++
121
- }
122
- if (fixedN > facts.maxDef) facts.maxDef = fixedN
123
- } else if (op === '()') {
124
- const a = args[1]
125
- const callArgs = a == null ? [] : (Array.isArray(a) && a[0] === ',') ? a.slice(1) : [a]
126
- if (callArgs.some(x => Array.isArray(x) && x[0] === '...')) facts.hasSpread = true
127
- if (callArgs.length > facts.maxCall) facts.maxCall = callArgs.length
128
- }
129
- for (const a of args) walk(a)
165
+ observeNodeFacts(node, facts)
166
+ for (const a of node.slice(1)) walk(a)
130
167
  }
131
168
  visitFuncValue(root)
132
169
  walk(root)
@@ -144,18 +181,28 @@ function recordModuleInitFacts(root) {
144
181
  export default function prepare(node) {
145
182
  depth = 0
146
183
  scopes = []
184
+ funcLocalNames = [new Set()]
147
185
  includeModule('core')
186
+ normalizeIdents(node)
148
187
  fuseSparseMapReads(node)
149
188
  const ast = prep(node)
150
189
  // Top-level functions referenced as first-class values (e.g. `let o = { fn: g }`,
151
190
  // `arr.push(g)`, `return g`) need trampoline emission, which depends on the fn
152
191
  // module's closure.table machinery. defFunc paths don't trigger fn-module load,
153
192
  // so scan post-prep and include `fn` if any user func appears in a value position.
154
- 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) {
155
199
  const funcNames = new Set(ctx.func.list.map(f => f.name))
156
200
  const visit = (n) => {
157
201
  if (!Array.isArray(n)) return false
158
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
159
206
  if (op === '()') {
160
207
  // callee at args[0]: skip if it's a bare func name (direct call); recurse rest
161
208
  if (typeof args[0] !== 'string' || !funcNames.has(args[0])) {
@@ -173,10 +220,6 @@ export default function prepare(node) {
173
220
  if (typeof args[0] === 'string' && funcNames.has(args[0])) return true
174
221
  return visit(args[0])
175
222
  }
176
- if (op === '=>') {
177
- // body only — params are bindings, not refs
178
- return visit(args[1])
179
- }
180
223
  for (const a of args) {
181
224
  if (typeof a === 'string' && funcNames.has(a)) return true
182
225
  if (visit(a)) return true
@@ -204,6 +247,34 @@ export default function prepare(node) {
204
247
  includeForTimerRuntime()
205
248
  }
206
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
+ }
276
+ }
277
+
207
278
  return ast
208
279
  }
209
280
 
@@ -234,16 +305,28 @@ const renameFunc = (func, nextName) => {
234
305
  }
235
306
 
236
307
  /** Map JS typeof strings to jz type checks. Codes < 0 trigger specialized emitTypeofCmp paths. */
237
- 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
+ }
238
317
  function resolveTypeof(node) {
239
318
  const [op, a, b] = node
240
319
  // typeof x == 'string' → type check
241
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]]
242
323
  const code = TYPEOF_MAP[b[1]]
243
324
  if (code != null) return [op, ['typeof', a[1]], [, code]]
244
325
  }
245
326
  // 'string' == typeof x
246
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]]
247
330
  const code = TYPEOF_MAP[a[1]]
248
331
  if (code != null) return [op, ['typeof', b[1]], [, code]]
249
332
  }
@@ -418,6 +501,8 @@ function prep(node) {
418
501
  if (PROHIBITED[node]) err(PROHIBITED[node])
419
502
  // Boolean/Number as value → identity arrow (for .filter(Boolean), .map(Number) etc.)
420
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)
421
506
  const resolved = ctx.scope.chain[node]
422
507
  if (resolved?.includes('.')) return resolved
423
508
  // Cross-module import: mangled name (e.g. __util_js$clone)
@@ -462,6 +547,7 @@ export const GLOBALS = {
462
547
  Object: 'Object',
463
548
  Symbol: 'Symbol',
464
549
  JSON: 'JSON',
550
+ Date: 'Date',
465
551
  isNaN: 'number',
466
552
  isFinite: 'number',
467
553
  parseInt: 'number',
@@ -475,6 +561,34 @@ export const GLOBALS = {
475
561
  const patternItems = (node) => node?.[0] === ',' ? node.slice(1) : [node]
476
562
  const isDestructPattern = (node) => Array.isArray(node) && (node[0] === '[]' || node[0] === '{}')
477
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
+
478
592
  function pushPatternAssign(target, valueExpr, out, decls = null) {
479
593
  if (Array.isArray(target) && target[0] === '=') {
480
594
  pushPatternAssign(target[1], ['??', valueExpr, prep(target[2])], out, decls)
@@ -595,13 +709,18 @@ function prepDecl(op, ...inits) {
595
709
 
596
710
  if (!defFunc(name, normed)) {
597
711
  let declName = name
598
- // Block scope: rename if shadowing an outer declaration
599
- 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))) {
600
718
  declName = `${name}${T}${ctx.func.uniq++}`
601
719
  scopes[scopes.length - 1].set(name, declName)
602
720
  } else if (typeof name === 'string' && scopes.length > 0) {
603
721
  scopes[scopes.length - 1].set(name, name)
604
722
  }
723
+ if (typeof declName === 'string' && fnNames) fnNames.add(declName)
605
724
  // Track const for reassignment checks — only module-scope consts (depth 0)
606
725
  if (typeof declName === 'string' && depth === 0) {
607
726
  if (ctx.module.currentPrefix) {
@@ -613,10 +732,19 @@ function prepDecl(op, ...inits) {
613
732
  ctx.scope.consts.add(declName)
614
733
  if (Array.isArray(normed) && normed[0] === 'str' && typeof normed[1] === 'string')
615
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)
616
737
  } else if (op === 'let' && ctx.scope.consts?.has(declName)) {
617
738
  ctx.scope.consts.delete(declName)
618
739
  ctx.scope.constStrs?.delete(declName)
740
+ ctx.scope.shapeStrArrays?.delete(declName)
619
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])
620
748
  recordGlobalValueFact(declName, normed)
621
749
  }
622
750
  // Track object schemas (after prefix so schema is keyed to final name)
@@ -671,6 +799,9 @@ const handlers = {
671
799
  // Destructuring assignment: [a, ...r] = expr or ({x: a} = expr)
672
800
  // Distinguishing from index assignment: destructuring patterns have exactly one payload node.
673
801
  if (isDestructPattern(lhs) && lhs.length === 2) {
802
+ const scalar = scalarArrayDestruct(lhs, rhs)
803
+ if (scalar) return scalar
804
+
674
805
  const normed = prep(rhs)
675
806
  const tmp = `${T}d${ctx.func.uniq++}`
676
807
  const decls = [['=', tmp, normed]]
@@ -695,7 +826,7 @@ const handlers = {
695
826
  if (depth === 0 && Array.isArray(lhs) && lhs[0] === '.' && typeof lhs[1] === 'string'
696
827
  && hasFunc(lhs[1]) && Array.isArray(rhs) && rhs[0] === '=>') {
697
828
  const name = `${lhs[1]}$${lhs[2]}`
698
- if (defFunc(name, prep(rhs))) return null // extracted as function, no assignment needed
829
+ if (defFunc(name, prep(rhs))) return ['=', prep(lhs), name]
699
830
  }
700
831
  return ['=', prep(lhs), prep(rhs)]
701
832
  },
@@ -762,7 +893,7 @@ const handlers = {
762
893
  if (hostMod && Array.isArray(specifiers) && specifiers[0] === '{}') {
763
894
  const inner = specifiers[1]
764
895
  if (inner != null) {
765
- 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)
766
897
  const builtinItems = []
767
898
  for (const item of items) {
768
899
  const name = typeof item === 'string' ? item : item[1]
@@ -800,7 +931,7 @@ const handlers = {
800
931
  if (Array.isArray(remaining) && remaining[0] === '{}') {
801
932
  const inner = remaining[1]
802
933
  if (inner == null) return null
803
- 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)
804
935
  for (const item of items)
805
936
  if (typeof item === 'string') bind(item)
806
937
  else if (Array.isArray(item) && item[0] === 'as') bind(item[1], item[2])
@@ -831,7 +962,7 @@ const handlers = {
831
962
  if (Array.isArray(specifiers) && specifiers[0] === '{}') {
832
963
  const inner = specifiers[1]
833
964
  if (inner == null) return null
834
- 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)
835
966
  for (const item of items) {
836
967
  const name = typeof item === 'string' ? item : item[1]
837
968
  const alias = typeof item === 'string' ? item : item[2]
@@ -848,7 +979,7 @@ const handlers = {
848
979
  if (Array.isArray(specifiers) && specifiers[0] === '{}') {
849
980
  const inner = specifiers[1]
850
981
  if (inner == null) return null
851
- 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)
852
983
  for (const item of items) {
853
984
  const name = typeof item === 'string' ? item : item[1]
854
985
  const alias = typeof item === 'string' ? item : item[2]
@@ -890,6 +1021,34 @@ const handlers = {
890
1021
  for (const i of decl.slice(1))
891
1022
  if (Array.isArray(i) && i[0] === '=' && typeof i[1] === 'string')
892
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
+ }
893
1052
  // export { name1, name2 as alias } → register named exports
894
1053
  if (Array.isArray(decl) && decl[0] === '{}') {
895
1054
  const inner = decl[1]
@@ -937,6 +1096,7 @@ const handlers = {
937
1096
 
938
1097
  depth++
939
1098
  scopes.push(fnScope)
1099
+ funcLocalNames.push(new Set(collectParamNames(raw)))
940
1100
 
941
1101
  const nextParams = []
942
1102
  const bodyPrefix = []
@@ -969,6 +1129,7 @@ const handlers = {
969
1129
  const inner = nextParams.length === 0 ? null : nextParams.length === 1 ? nextParams[0] : [',', ...nextParams]
970
1130
  const result = ['=>', Array.isArray(params) && params[0] === '()' ? ['()', inner] : inner, preparedBody]
971
1131
  scopes.pop()
1132
+ funcLocalNames.pop()
972
1133
  depth--
973
1134
  return result
974
1135
  },
@@ -1001,6 +1162,8 @@ const handlers = {
1001
1162
  // Boolean literals NaN-box as f64 — typeof at runtime returns 'number'. Fold here so the JS-spec value survives.
1002
1163
  'typeof'(a) {
1003
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] }
1004
1167
  return ['typeof', prep(a)]
1005
1168
  },
1006
1169
 
@@ -1048,6 +1211,14 @@ const handlers = {
1048
1211
  // Grouping: (expr) → ['()', expr] with no args. Call: f() → ['()', 'f', null] with null arg.
1049
1212
  if (args.length === 0) return prep(callee)
1050
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
+
1051
1222
  const hasRealArgs = args.some(a => a != null)
1052
1223
 
1053
1224
  if (typeof callee === 'string') {
@@ -1060,11 +1231,13 @@ const handlers = {
1060
1231
  }
1061
1232
  }
1062
1233
 
1063
- const resolved = ctx.scope.chain[callee]
1064
- if (resolved?.includes('.')) 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
1065
1238
  else if (resolved && hasFunc(resolved)) callee = resolved
1066
1239
  else if (resolved && !resolved.includes('.')) {
1067
- 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)
1068
1241
  }
1069
1242
  else if (depth > 0 && !resolved && !ctx.func.exports[callee] && !ctx.module.imports.some(i => i[3]?.[1] === `$${callee}`)) {
1070
1243
  includeForCallableValue()
@@ -1094,6 +1267,16 @@ const handlers = {
1094
1267
  callee = prep(callee)
1095
1268
  }
1096
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
+ }
1097
1280
  const preppedArgs = args.filter(a => a != null).map(prep)
1098
1281
  for (const a of preppedArgs) {
1099
1282
  if (typeof a === 'string' && hasFunc(a)) {
@@ -1162,9 +1345,28 @@ const handlers = {
1162
1345
  }
1163
1346
  return prep(p)
1164
1347
  }
1165
- const result = Array.isArray(inner) && inner[0] === ','
1166
- ? ['{}', ...inner.slice(1).map(prop)]
1167
- : ['{}', 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]
1168
1370
  // Register schema so property access works for function params (duck typing)
1169
1371
  const props = result.slice(1).filter(p => Array.isArray(p) && p[0] === ':').map(p => p[1])
1170
1372
  if (props.length && ctx.schema.register) ctx.schema.register(props)
@@ -1178,15 +1380,19 @@ const handlers = {
1178
1380
  if (Array.isArray(head) && head[0] === ';') {
1179
1381
  let [, init, cond, step] = head
1180
1382
  // Hoist .length / .size / .byteLength from for-condition:
1181
- // `i < arr.length` → `let __len = arr.length; ... i < __len`
1182
- // All three return i32 (TYPED/ARRAY/STRING/BUFFER/SET/MAP), so the hoisted
1183
- // 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.
1184
1390
  if (cond && Array.isArray(cond) && (cond[0] === '<' || cond[0] === '<=' || cond[0] === '>' || cond[0] === '>=')) {
1185
1391
  const lenExpr = cond[0] === '<' || cond[0] === '<=' ? cond[2] : cond[1]
1186
1392
  if (Array.isArray(lenExpr) && lenExpr[0] === '.' &&
1187
1393
  (lenExpr[2] === 'length' || lenExpr[2] === 'size' || lenExpr[2] === 'byteLength')) {
1188
1394
  const lenVar = `${T}len${ctx.func.uniq++}`
1189
- const lenDecl = ['let', ['=', lenVar, lenExpr]]
1395
+ const lenDecl = ['let', ['=', lenVar, ['|', lenExpr, [, 0]]]]
1190
1396
  init = init ? [';', init, lenDecl] : lenDecl
1191
1397
  if (cond[0] === '<' || cond[0] === '<=') cond = [cond[0], cond[1], lenVar]
1192
1398
  else cond = [cond[0], lenVar, cond[2]]
@@ -1203,9 +1409,12 @@ const handlers = {
1203
1409
  const lenVar = `${T}len${ctx.func.uniq++}`
1204
1410
  const trivial = typeof src === 'string'
1205
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]]
1206
1415
  const decls = trivial
1207
- ? ['let', ['=', idx, [, 0]], ['=', lenVar, ['.', arrVar, 'length']]]
1208
- : ['let', ['=', arrVar, src], ['=', idx, [, 0]], ['=', lenVar, ['.', arrVar, 'length']]]
1416
+ ? ['let', ['=', idx, [, 0]], ['=', lenVar, lenE]]
1417
+ : ['let', ['=', arrVar, src], ['=', idx, [, 0]], ['=', lenVar, lenE]]
1209
1418
  const cond = ['<', idx, lenVar]
1210
1419
  const step = ['++', idx]
1211
1420
  const inner = [';', ['let', ['=', varName, ['[]', arrVar, idx]]], body]
@@ -1213,9 +1422,9 @@ const handlers = {
1213
1422
  } else if (Array.isArray(head) && head[0] === 'in') {
1214
1423
  // for (let k in obj) → unroll at compile time when schema known, else HASH runtime iteration
1215
1424
  const [, decl, src] = head
1216
- 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
1217
1426
  const srcName = typeof src === 'string' ? (ctx.scope.chain[src] || src) : null
1218
- const sid = typeof srcName === 'string' && ctx.schema.vars.get(srcName)
1427
+ const sid = typeof srcName === 'string' ? ctx.schema.vars.get(srcName) : null
1219
1428
  if (sid != null) {
1220
1429
  // Known schema → compile-time unrolling with string keys
1221
1430
  const keys = ctx.schema.list[sid]
@@ -1244,10 +1453,15 @@ const handlers = {
1244
1453
  // Property access - resolve namespaces or object/array properties
1245
1454
  '.'(obj, prop) {
1246
1455
  if (prop === 'caller' || prop === 'callee') err('`.caller` and `.callee` are prohibited: deprecated stack introspection')
1456
+ if (prop === 'url' && isImportMeta(obj)) return staticString(importMetaUrl())
1247
1457
  const mod = ctx.scope.chain[obj]
1248
1458
  // Only treat as module namespace if it's a known built-in module (not a mangled import name)
1249
- if (typeof obj === 'string' && mod && !mod.includes('.') && hasModule(mod))
1250
- 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
+ }
1251
1465
  // Source module namespace: import * as X → X.prop resolved to mangled name
1252
1466
  if (typeof obj === 'string' && ctx.module.namespaces?.[obj]) {
1253
1467
  const mangled = ctx.module.namespaces[obj].get(prop)
@@ -1265,6 +1479,15 @@ const handlers = {
1265
1479
  if (ctorArgs.length === 1 && Array.isArray(ctorArgs[0]) && ctorArgs[0][0] === ',')
1266
1480
  ctorArgs = ctorArgs[0].slice(1)
1267
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
+
1268
1491
  // Wrap multi-arg ctor arg lists back into a single comma-group — the '()' op
1269
1492
  // expects callArgs as a single element (possibly comma-grouped).
1270
1493
  const wrapArgs = (args) => args.length === 0 ? [null]
@@ -1420,7 +1643,7 @@ function prepareModule(specifier, source) {
1420
1643
  ctx.module.currentPrefix = prefix
1421
1644
 
1422
1645
  // Parse + prepare imported source (may trigger recursive imports)
1423
- let ast = parse(normalizeSource(source))
1646
+ let ast = parse(source)
1424
1647
  if (ctx.transform.jzify) ast = ctx.transform.jzify(ast)
1425
1648
  const savedDepth = depth; depth = 0
1426
1649
  const moduleInit = prep(ast)
@@ -1428,6 +1651,19 @@ function prepareModule(specifier, source) {
1428
1651
 
1429
1652
  // Collect exports: rename exported funcs with prefix
1430
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
+ }
1431
1667
  for (const name of Object.keys(ctx.func.exports)) {
1432
1668
  const val = ctx.func.exports[name]
1433
1669
  // Default export alias: export default existingName → map 'default' to that name's mangled form
@@ -1435,19 +1671,29 @@ function prepareModule(specifier, source) {
1435
1671
  // Will resolve after all named exports are mangled
1436
1672
  continue
1437
1673
  }
1438
- const mangled = `${prefix}$${name}`
1439
- moduleExports.set(name, mangled)
1440
- // Rename the function in ctx.func.list
1441
- const func = ctx.func.list.find(f => f.name === name)
1442
- if (func) renameFunc(func, mangled)
1443
- // Rename globals
1444
- if (ctx.scope.globals.has(name)) {
1445
- const wat = ctx.scope.globals.get(name).replace(`$${name}`, `$${mangled}`)
1446
- ctx.scope.globals.delete(name)
1447
- ctx.scope.globals.set(mangled, wat)
1448
- if (ctx.scope.userGlobals.has(name)) { ctx.scope.userGlobals.delete(name); ctx.scope.userGlobals.add(mangled) }
1449
- 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
1450
1695
  }
1696
+ exportLocal(name, name)
1451
1697
  }
1452
1698
  // Resolve default export alias after named exports are mangled
1453
1699
  if (typeof ctx.func.exports['default'] === 'string') {