jz 0.2.0 → 0.3.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/plan.js CHANGED
@@ -24,17 +24,39 @@
24
24
  */
25
25
 
26
26
  import { ctx } from './ctx.js'
27
- import { T, VAL, analyzeBody, invalidateLocalsCache, staticObjectProps, staticPropertyKey, valTypeOf, typedElemCtor, typedElemAux, updateGlobalRep, collectProgramFacts } from './analyze.js'
27
+ import { T, VAL, ASSIGN_OPS, analyzeBody, invalidateLocalsCache, staticObjectProps, staticPropertyKey, valTypeOf, typedElemCtor, typedElemAux, updateGlobalRep, collectProgramFacts, extractParams } from './analyze.js'
28
28
  import { MAX_CLOSURE_ARITY } from './ir.js'
29
29
  import narrowSignatures, { specializeBimorphicTyped, refineDynKeys } from './narrow.js'
30
30
 
31
- const ASSIGN_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
32
31
  const CONTROL_TRANSFER = new Set(['return', 'throw', 'break', 'continue'])
33
32
  const LOOP_OPS = new Set(['for', 'while', 'do', 'do-while'])
34
- const SCALAR_TYPED_CTOR = 'new.Float64Array'
35
- const MAX_SCALAR_TYPED_ARRAY_LEN = 32
36
- const MAX_SCALAR_TYPED_LOOP_UNROLL = 16
37
- const MAX_SCALAR_TYPED_NESTED_UNROLL = 128
33
+ // Fixed-size typed arrays eligible for scalar replacement, mapped to the element
34
+ // store-coercion kind ('' = none, i.e. Float64Array's f64-identity). Excluded:
35
+ // Float32Array — store coercion is `Math.fround`, needs the `math` module pulled at plan time
36
+ // Uint32Array — element range [0, 2^32) exceeds what jz keeps as f64 after `x >>> 0` (i32-narrowed)
37
+ // Uint8ClampedArray — round-half-to-even clamp
38
+ // Coerced (truthy) types are scalarized only when fully local — any escape (passed
39
+ // to a call, `.buffer`/view aliasing, etc.) keeps the real allocation, since the
40
+ // mirror/fence path can't track writes through an alias that outlives the fence.
41
+ const SCALAR_TYPED_COERCE = {
42
+ 'new.Float64Array': '',
43
+ 'new.Int32Array': 'i32',
44
+ 'new.Int16Array': 'i16', 'new.Uint16Array': 'u16',
45
+ 'new.Int8Array': 'i8', 'new.Uint8Array': 'u8',
46
+ }
47
+ // AST for the store coercion a typed-array element does on write (`arr[i] = v`).
48
+ // All expressible with operators jz already lowers post-plan (no module deps).
49
+ const coerceAST = (kind, expr) => {
50
+ if (kind === 'i32') return ['|', expr, [null, 0]]
51
+ if (kind === 'i16') return ['>>', ['<<', expr, [null, 16]], [null, 16]]
52
+ if (kind === 'u16') return ['&', expr, [null, 0xffff]]
53
+ if (kind === 'i8') return ['>>', ['<<', expr, [null, 24]], [null, 24]]
54
+ if (kind === 'u8') return ['&', expr, [null, 0xff]]
55
+ return expr
56
+ }
57
+ const maxScalarTypedArrayLen = () => ctx.transform.optimize?.scalarTypedArrayLen ?? 32
58
+ const maxScalarTypedLoopUnroll = () => ctx.transform.optimize?.scalarTypedLoopUnroll ?? 16
59
+ const maxScalarTypedNestedUnroll = () => ctx.transform.optimize?.scalarTypedNestedUnroll ?? 128
38
60
 
39
61
  const isSeq = node => Array.isArray(node) && node[0] === ';'
40
62
  const blockStmts = body => {
@@ -165,12 +187,14 @@ const scalarObjectProps = (expr) => {
165
187
  return props
166
188
  }
167
189
 
168
- const fixedScalarTypedArrayLen = (expr) => {
169
- if (typedElemCtor(expr) !== SCALAR_TYPED_CTOR) return null
190
+ const fixedScalarTypedArray = (expr) => {
191
+ const ctor = typedElemCtor(expr)
192
+ if (ctor == null || !(ctor in SCALAR_TYPED_COERCE)) return null
170
193
  const args = callArgs(expr)
171
194
  if (!args || args.length !== 1) return null
172
195
  const len = constIntExpr(args[0])
173
- return len != null && len >= 0 && len <= MAX_SCALAR_TYPED_ARRAY_LEN ? len : null
196
+ return len != null && len >= 0 && len <= maxScalarTypedArrayLen()
197
+ ? { len, coerce: SCALAR_TYPED_COERCE[ctor] } : null
174
198
  }
175
199
 
176
200
  const ASSIGN_TARGET_OPS = new Set(['=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '>>=', '<<=', '>>>=', '||=', '&&=', '??='])
@@ -254,7 +278,10 @@ const typedArraySlotIndex = (node, len) => {
254
278
  return idx != null && idx >= 0 && idx < len ? idx : null
255
279
  }
256
280
 
257
- const safeScalarTypedArrayUse = (node, name, len) => {
281
+ // `coerce` truthy the array's element type truncates on store (Int*/Uint* views),
282
+ // so in-place updates (`arr[i]++`, `arr[i] += x`) can't be a plain `slot`-op rewrite —
283
+ // reject them and only scalarize plain `arr[i] = v` writes and `arr[i]` reads.
284
+ const safeScalarTypedArrayUse = (node, name, len, coerce = '') => {
258
285
  if (typeof node === 'string') return node !== name
259
286
  if (!Array.isArray(node)) return true
260
287
  const op = node[0]
@@ -262,17 +289,18 @@ const safeScalarTypedArrayUse = (node, name, len) => {
262
289
  if ((op === '.' || op === '?.') && node[1] === name) return node[2] === 'length'
263
290
  if (op === '[]' && node[1] === name) return typedArraySlotIndex(node[2], len) != null
264
291
  if ((op === '++' || op === '--') && Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name)
265
- return typedArraySlotIndex(node[1][2], len) != null
292
+ return !coerce && typedArraySlotIndex(node[1][2], len) != null
266
293
  if (ASSIGN_TARGET_OPS.has(op)) {
267
294
  if (node[1] === name) return false
268
295
  if (Array.isArray(node[1]) && node[1][0] === '[]' && node[1][1] === name) {
296
+ if (coerce && op !== '=') return false
269
297
  if (typedArraySlotIndex(node[1][2], len) == null) return false
270
- for (let i = 2; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len)) return false
298
+ for (let i = 2; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len, coerce)) return false
271
299
  return true
272
300
  }
273
301
  }
274
302
  if (op === '...' && node[1] === name) return false
275
- for (let i = 1; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len)) return false
303
+ for (let i = 1; i < node.length; i++) if (!safeScalarTypedArrayUse(node[i], name, len, coerce)) return false
276
304
  return true
277
305
  }
278
306
 
@@ -297,8 +325,11 @@ const rewriteScalarTypedArrayUses = (node, arrays) => {
297
325
  return slot ? [op, slot] : node
298
326
  }
299
327
  if (ASSIGN_TARGET_OPS.has(op) && Array.isArray(node[1]) && node[1][0] === '[]' && arrays.has(node[1][1])) {
300
- const slot = slotFor(node[1][2], arrays.get(node[1][1]))
301
- return slot ? [op, slot, ...node.slice(2).map(part => rewriteScalarTypedArrayUses(part, arrays))] : node
328
+ const entry = arrays.get(node[1][1])
329
+ const slot = slotFor(node[1][2], entry)
330
+ if (!slot) return node
331
+ const rhs = node.slice(2).map(part => rewriteScalarTypedArrayUses(part, arrays))
332
+ return op === '=' && entry.coerce ? ['=', slot, coerceAST(entry.coerce, rhs[0])] : [op, slot, ...rhs]
302
333
  }
303
334
  return node.map((part, i) => i === 0 ? part : rewriteScalarTypedArrayUses(part, arrays))
304
335
  }
@@ -329,6 +360,24 @@ const collectScalarTypedArrayWrites = (node, name, len, out = new Set()) => {
329
360
  return out
330
361
  }
331
362
 
363
+ const hasScalarTypedArrayRead = (node, name) => {
364
+ if (!Array.isArray(node)) return false
365
+ const op = node[0]
366
+ const isTarget = target => Array.isArray(target) && target[0] === '[]' && target[1] === name
367
+ if ((op === '++' || op === '--') && isTarget(node[1])) return true
368
+ if (ASSIGN_TARGET_OPS.has(op)) {
369
+ if (isTarget(node[1])) {
370
+ if (op !== '=') return true
371
+ for (let i = 2; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
372
+ return false
373
+ }
374
+ }
375
+ if (op === '[]' && node[1] === name) return true
376
+ if (op === '=>') return false
377
+ for (let i = 1; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
378
+ return false
379
+ }
380
+
332
381
  const scalarizeTypedArrayLiteralSeq = (seq) => {
333
382
  if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
334
383
  let changed = false
@@ -345,26 +394,27 @@ const scalarizeTypedArrayLiteralSeq = (seq) => {
345
394
  if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
346
395
  const decl = stmt[1]
347
396
  if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
348
- const len = fixedScalarTypedArrayLen(decl[2])
349
- if (len == null) continue
397
+ const fixed = fixedScalarTypedArray(decl[2])
398
+ if (fixed == null) continue
399
+ const { len, coerce } = fixed
350
400
  let hasSafeUse = false, hasUnsafeUse = false
351
401
  for (let j = 0; j < stmts.length; j++) {
352
402
  if (j === i) continue
353
403
  if (!mentionsName(stmts[j], decl[1])) continue
354
- const safe = safeScalarTypedArrayUse(stmts[j], decl[1], len)
404
+ const safe = safeScalarTypedArrayUse(stmts[j], decl[1], len, coerce)
355
405
  hasSafeUse ||= safe
356
406
  hasUnsafeUse ||= !safe
357
407
  }
358
- if (!hasSafeUse && hasUnsafeUse) continue
359
- if (!hasUnsafeUse) candidates.set(decl[1], { index: i, len, mirrored: false })
360
- else mirrored.set(decl[1], { index: i, len, mirrored: true })
408
+ if (hasUnsafeUse && (!hasSafeUse || coerce)) continue
409
+ if (!hasUnsafeUse) candidates.set(decl[1], { index: i, len, coerce, mirrored: false })
410
+ else mirrored.set(decl[1], { index: i, len, coerce, mirrored: true })
361
411
  }
362
412
  if (!candidates.size && !mirrored.size) return { node: changed ? [';', ...stmts] : seq, changed }
363
413
 
364
414
  const arrays = new Map()
365
415
  for (const [name, c] of [...candidates, ...mirrored]) {
366
416
  const slots = Array.from({ length: c.len }, (_, k) => `${name}${T}ta${ctx.func.uniq++}_${k}`)
367
- arrays.set(name, { len: c.len, slots, mirrored: c.mirrored })
417
+ arrays.set(name, { len: c.len, slots, mirrored: c.mirrored, coerce: c.coerce })
368
418
  }
369
419
 
370
420
  const out = []
@@ -386,7 +436,7 @@ const scalarizeTypedArrayLiteralSeq = (seq) => {
386
436
  }
387
437
  const unsafe = []
388
438
  for (const [name, arr] of arrays) {
389
- if (arr.mirrored && mentionsName(stmts[i], name) && !safeScalarTypedArrayUse(stmts[i], name, arr.len)) unsafe.push([name, arr])
439
+ if (arr.mirrored && mentionsName(stmts[i], name) && !safeScalarTypedArrayUse(stmts[i], name, arr.len, arr.coerce)) unsafe.push([name, arr])
390
440
  }
391
441
  if (unsafe.length) {
392
442
  for (const [name, arr] of unsafe) out.push(...scalarTypedArrayStores(name, arr))
@@ -446,7 +496,7 @@ function smallScalarTypedForTrip(init, cond, step) {
446
496
  if (constIntExpr(decl[2]) !== 0) return null
447
497
  if (!Array.isArray(cond) || cond[0] !== '<' || cond[1] !== name) return null
448
498
  const end = constIntExpr(cond[2])
449
- if (end == null || end < 0 || end > MAX_SCALAR_TYPED_LOOP_UNROLL) return null
499
+ if (end == null || end < 0 || end > maxScalarTypedLoopUnroll()) return null
450
500
  const stepOk = Array.isArray(step) && ((step[0] === '++' && step[1] === name) ||
451
501
  (step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++' && step[1][1] === name && constIntExpr(step[2]) === 1))
452
502
  return stepOk ? { name, end } : null
@@ -482,7 +532,7 @@ const unrollTypedArrayLoops = (node, names) => {
482
532
  }
483
533
  if (node[0] === 'for') {
484
534
  const trip = smallScalarTypedForTrip(node[1], node[2], node[3])
485
- if (trip && containsTypedArrayAccess(node[4], names) && scalarTypedLoopBudget(node[4]) * trip.end <= MAX_SCALAR_TYPED_NESTED_UNROLL &&
535
+ if (trip && containsTypedArrayAccess(node[4], names) && scalarTypedLoopBudget(node[4]) * trip.end <= maxScalarTypedNestedUnroll() &&
486
536
  !hasControlTransfer(node[4]) && !containsDeclOf(node[4], trip.name) && !isReassigned(node[4], trip.name)) {
487
537
  const out = [';']
488
538
  for (let i = 0; i < trip.end; i++) {
@@ -511,8 +561,8 @@ const fixedTypedArraysInBody = (body) => {
511
561
  for (let i = 1; i < node.length; i++) {
512
562
  const d = node[i]
513
563
  if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
514
- const len = fixedScalarTypedArrayLen(d[2])
515
- if (len != null) out.set(d[1], { len })
564
+ const fixed = fixedScalarTypedArray(d[2])
565
+ if (fixed != null) out.set(d[1], fixed)
516
566
  }
517
567
  }
518
568
  for (let i = 1; i < node.length; i++) walk(node[i])
@@ -528,15 +578,15 @@ const scalarTypedParamCandidates = (func, sites, fixedByFunc) => {
528
578
  const cands = new Map()
529
579
  for (let i = 0; i < params.length; i++) {
530
580
  const pname = params[i].name
531
- let len = null, ok = true
581
+ let len = null, coerce = null, ok = true
532
582
  for (const site of sites) {
533
583
  const arg = site.argList[i]
534
584
  const fixed = typeof arg === 'string' ? fixedByFunc.get(site.callerFunc)?.get(arg) : null
535
585
  if (!fixed) { ok = false; break }
536
- if (len == null) len = fixed.len
537
- else if (len !== fixed.len) { ok = false; break }
586
+ if (len == null) { len = fixed.len; coerce = fixed.coerce }
587
+ else if (len !== fixed.len || coerce !== fixed.coerce) { ok = false; break }
538
588
  }
539
- if (ok && len != null) cands.set(pname, { len })
589
+ if (ok && len != null && len <= maxScalarTypedArrayLen()) cands.set(pname, { len, coerce })
540
590
  }
541
591
  if (!cands.size) return cands
542
592
  for (const site of sites) {
@@ -552,12 +602,14 @@ const scalarTypedParamCandidates = (func, sites, fixedByFunc) => {
552
602
  }
553
603
 
554
604
  const scalarizeTypedArrayParams = (func, paramCands) => {
555
- for (const [name, c] of [...paramCands]) if (!safeScalarTypedArrayUse(func.body, name, c.len)) paramCands.delete(name)
605
+ for (const [name, c] of [...paramCands]) if (!safeScalarTypedArrayUse(func.body, name, c.len, c.coerce)) paramCands.delete(name)
606
+ for (const [name] of [...paramCands]) if (!hasScalarTypedArrayRead(func.body, name)) paramCands.delete(name)
556
607
  if (!paramCands.size) return { body: func.body, changed: false }
557
608
  const arrays = new Map()
558
609
  for (const [name, c] of paramCands) {
559
610
  arrays.set(name, {
560
611
  len: c.len,
612
+ coerce: c.coerce,
561
613
  slots: Array.from({ length: c.len }, (_, k) => `${name}${T}tap${ctx.func.uniq++}_${k}`),
562
614
  })
563
615
  }
@@ -920,6 +972,7 @@ const inlineHotInternalCalls = (programFacts, ast) => {
920
972
  if (cfg && cfg.sourceInline === false) return false
921
973
 
922
974
  const fixedByFunc = new Map(ctx.func.list.map(func => [func, fixedTypedArraysInBody(func.body)]))
975
+ const typedByFunc = new Map(ctx.func.list.map(func => [func, analyzeBody(func.body).typedElems]))
923
976
  const sitesByCallee = new Map()
924
977
  for (const cs of programFacts.callSites) {
925
978
  const list = sitesByCallee.get(cs.callee)
@@ -942,8 +995,22 @@ const inlineHotInternalCalls = (programFacts, ast) => {
942
995
  return typeof arg === 'string' && fixedByFunc.get(site.callerFunc)?.has(arg)
943
996
  }))
944
997
  }
945
- const hasFixedTypedArrayHotSites = (func, sites) =>
946
- hasFixedTypedArraySites(func, sites) && sites.some(site => site.callerFunc?.body && containsNode(site.callerFunc.body, site.node))
998
+ const hasFullyFixedTypedArraySites = (func, sites) => {
999
+ const params = func.sig?.params || []
1000
+ if (!sites?.length) return false
1001
+ let sawTypedArg = false
1002
+ for (const site of sites) {
1003
+ const typed = typedByFunc.get(site.callerFunc)
1004
+ const fixed = fixedByFunc.get(site.callerFunc)
1005
+ for (let i = 0; i < params.length; i++) {
1006
+ const arg = site.argList[i]
1007
+ if (typeof arg !== 'string' || !typed?.has(arg)) continue
1008
+ sawTypedArg = true
1009
+ if (!fixed?.has(arg)) return false
1010
+ }
1011
+ }
1012
+ return sawTypedArg
1013
+ }
947
1014
 
948
1015
  const candidates = new Map()
949
1016
  for (const func of ctx.func.list) {
@@ -951,6 +1018,7 @@ const inlineHotInternalCalls = (programFacts, ast) => {
951
1018
  if (func.defaults && Object.keys(func.defaults).length) continue
952
1019
  const sites = sitesByCallee.get(func.name)
953
1020
  const fixedTypedArraySite = hasFixedTypedArraySites(func, sites)
1021
+ const fullyFixedTypedArraySite = hasFullyFixedTypedArraySites(func, sites)
954
1022
  if (!sites || sites.length < 1 || (!fixedTypedArraySite && sites.length > 2) || sites.length > 8) continue
955
1023
  const stmts = blockStmts(func.body)
956
1024
  // Expression-bodied arrow funcs (`(c) => expr`) have no block — body IS the
@@ -980,7 +1048,7 @@ const inlineHotInternalCalls = (programFacts, ast) => {
980
1048
  // loop carries most of the cost. Inlining them into a host that V8 can't
981
1049
  // tier up (e.g. a once-called wrapper) freezes the kernel in baseline.
982
1050
  // Keep them as standalone functions so V8 wasm tier-up can warm them.
983
- if (loopDepth(func.body, 0) >= 2 && !fixedTypedArraySite) continue
1051
+ if (loopDepth(func.body, 0) >= 2 && !fullyFixedTypedArraySite) continue
984
1052
  // Factory functions that allocate pointers (`new TypedArray`, `new Array`,
985
1053
  // object/array literals returned) break downstream pointer-ABI specialization
986
1054
  // when inlined: narrow.js can't trace the post-inline alias chain back to a
@@ -1036,6 +1104,146 @@ const inlineHotInternalCalls = (programFacts, ast) => {
1036
1104
  return changed
1037
1105
  }
1038
1106
 
1107
+ // === Inline non-escaping local lambdas ===
1108
+ // `const f = (a) => …; … f(x) …` → the lambda body substituted at each call
1109
+ // site. A non-escaping lambda's captured free vars are still in lexical scope at
1110
+ // the call site, so splicing the body in place preserves capture-by-reference
1111
+ // semantics while eliminating the closure object (no env pointer, no NaN-box, no
1112
+ // call_indirect). Mirrors inlineHotInternalCalls, scoped to one function body.
1113
+
1114
+ // True iff `name` appears textually anywhere in `node` (descending into nested
1115
+ // arrows; `.prop` / `:key` positions are literal names, not refs — skipped to
1116
+ // match cloneWithSubst's structure).
1117
+ const referencesName = (node, name) => {
1118
+ if (typeof node === 'string') return node === name
1119
+ if (!Array.isArray(node)) return false
1120
+ const op = node[0]
1121
+ if (op === 'str') return false
1122
+ if (op === '.' || op === '?.') return referencesName(node[1], name)
1123
+ if (op === ':') return referencesName(node[2], name)
1124
+ for (let i = 1; i < node.length; i++) if (referencesName(node[i], name)) return true
1125
+ return false
1126
+ }
1127
+
1128
+ // True iff every textual reference to `name` in `node` is the callee of a
1129
+ // `name(...)` call (i.e. the binding never escapes — never read as a value,
1130
+ // reassigned, captured by a nested lambda, or shadowed).
1131
+ const onlyCalledNotReferenced = (node, name) => {
1132
+ if (typeof node === 'string') return node !== name
1133
+ if (!Array.isArray(node)) return true
1134
+ const op = node[0]
1135
+ if (op === 'str') return true
1136
+ // A nested lambda touching `name` at all (capture or shadowing param) → bail.
1137
+ if (op === '=>') return !referencesName(node[1], name) && !referencesName(node[2], name)
1138
+ if (op === '()' && node[1] === name) {
1139
+ for (let i = 2; i < node.length; i++) if (!onlyCalledNotReferenced(node[i], name)) return false
1140
+ return true
1141
+ }
1142
+ if (op === '.' || op === '?.') return onlyCalledNotReferenced(node[1], name)
1143
+ if (op === ':') return onlyCalledNotReferenced(node[2], name)
1144
+ for (let i = 1; i < node.length; i++) if (!onlyCalledNotReferenced(node[i], name)) return false
1145
+ return true
1146
+ }
1147
+
1148
+ const bodyStmtList = body =>
1149
+ Array.isArray(body) && body[0] === '{}' ? blockStmts(body)
1150
+ : Array.isArray(body) && body[0] === ';' ? body.slice(1)
1151
+ : body == null ? [] : [body]
1152
+
1153
+ const removeStmts = (body, set) => {
1154
+ if (!Array.isArray(body)) return set.has(body) ? null : body
1155
+ if (body[0] === '{}') return ['{}', removeStmts(body[1], set) ?? [';']]
1156
+ if (body[0] === ';') {
1157
+ const kept = body.slice(1).filter(s => !set.has(s))
1158
+ return kept.length === 0 ? null : kept.length === 1 ? kept[0] : [';', ...kept]
1159
+ }
1160
+ return set.has(body) ? null : body
1161
+ }
1162
+
1163
+ // Lambda body must be a guaranteed-return shape inlinedBody can splice: ≤1
1164
+ // `return` (trailing, if a block), no throw/break/continue, no param mutation,
1165
+ // no nested lambda.
1166
+ const inlinableLambdaBody = (abody, params) => {
1167
+ if (scanBody(abody, n => n[0] === '=>')) return false
1168
+ if (scanBody(abody, n => n[0] === 'throw' || n[0] === 'break' || n[0] === 'continue')) return false
1169
+ let returns = 0
1170
+ scanBody(abody, n => { if (n[0] === 'return') returns++; return false })
1171
+ if (returns > 1) return false
1172
+ if (returns === 1) {
1173
+ const stmts = blockStmts(abody)
1174
+ if (!stmts || !stmts.length) return false
1175
+ const last = stmts[stmts.length - 1]
1176
+ if (!Array.isArray(last) || last[0] !== 'return') return false
1177
+ }
1178
+ return !mutatesAny(abody, new Set(params))
1179
+ }
1180
+
1181
+ const inlineLocalLambdasInBody = (getBody, setBody) => {
1182
+ const body = getBody()
1183
+ const stmts = bodyStmtList(body)
1184
+ if (stmts.length < 2) return false
1185
+
1186
+ // Collect `const f = ARROW` (single-decl), all-plain params, inlinable body.
1187
+ const decls = new Map()
1188
+ for (const stmt of stmts) {
1189
+ if (!Array.isArray(stmt) || stmt[0] !== 'const' || stmt.length !== 2) continue
1190
+ const d = stmt[1]
1191
+ if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
1192
+ const arrow = d[2]
1193
+ if (!Array.isArray(arrow) || arrow[0] !== '=>') continue
1194
+ const params = extractParams(arrow[1])
1195
+ if (!params.every(p => typeof p === 'string')) continue
1196
+ if (!inlinableLambdaBody(arrow[2], params)) continue
1197
+ decls.set(d[1], { stmt, arrow, params })
1198
+ }
1199
+ if (!decls.size) return false
1200
+
1201
+ // Drop any candidate whose body references another (or its own) candidate —
1202
+ // single-level inlining can't resolve such chains, and a still-referenced
1203
+ // candidate's decl can't be removed.
1204
+ for (let changed = true; changed;) {
1205
+ changed = false
1206
+ for (const [name, info] of decls) {
1207
+ if ([...decls.keys()].some(c => referencesName(info.arrow[2], c))) { decls.delete(name); changed = true }
1208
+ }
1209
+ }
1210
+ // Every other reference to the name must be a `name(...)` call.
1211
+ for (const [name, info] of [...decls]) {
1212
+ if (!stmts.every(s => s === info.stmt || onlyCalledNotReferenced(s, name))) decls.delete(name)
1213
+ }
1214
+ if (!decls.size) return false
1215
+
1216
+ const asFunc = info => ({ sig: { params: info.params.map(name => ({ name })) }, body: info.arrow[2] })
1217
+ const stmtCands = new Map(), exprCands = new Map()
1218
+ for (const [name, info] of decls)
1219
+ (Array.isArray(info.arrow[2]) && info.arrow[2][0] === '{}' ? stmtCands : exprCands).set(name, asFunc(info))
1220
+
1221
+ let out = body, didChange = false
1222
+ if (stmtCands.size) { const r = inlineInStmt(out, stmtCands); if (r.changed) { out = r.node; didChange = true } }
1223
+ if (exprCands.size) { const r = inlineInExpr(out, exprCands); if (r.changed) { out = r.node; didChange = true } }
1224
+ if (!didChange) return false
1225
+
1226
+ // Remove decls of candidates that are now fully consumed.
1227
+ const newStmts = bodyStmtList(out)
1228
+ const dead = new Set()
1229
+ for (const [name, info] of decls) {
1230
+ if (!newStmts.some(s => s !== info.stmt && referencesName(s, name))) dead.add(info.stmt)
1231
+ }
1232
+ if (dead.size) out = removeStmts(out, dead) ?? [';']
1233
+
1234
+ setBody(out)
1235
+ return true
1236
+ }
1237
+
1238
+ const inlineLocalLambdas = () => {
1239
+ let changed = false
1240
+ for (const func of ctx.func.list) {
1241
+ if (!func.body || func.raw) continue
1242
+ if (inlineLocalLambdasInBody(() => func.body, b => { func.body = b })) changed = true
1243
+ }
1244
+ return changed
1245
+ }
1246
+
1039
1247
  const restIndexExpr = (idx, restParams) => {
1040
1248
  const k = intLit(idx)
1041
1249
  if (k != null) return k >= 0 && k < restParams.length ? restParams[k] : [, undefined]
@@ -1193,11 +1401,23 @@ const materializeAutoBoxSchemas = (programFacts) => {
1193
1401
 
1194
1402
  const resolveClosureWidth = (programFacts) => {
1195
1403
  if (!ctx.closure.make) return
1196
- const { hasSpread, hasRest, maxCall, maxDef } = programFacts
1404
+ const { hasSpread, hasRest, maxCall, maxDef, valueUsed } = programFacts
1197
1405
  const floor = ctx.closure.floor ?? 0
1406
+ // A top-level function used as a first-class value gets a boundary trampoline
1407
+ // that forwards $__a0..$__a{arity-1} into it (emit.js). The uniform closure
1408
+ // ABI must therefore be at least as wide as any table-resident function's
1409
+ // fixed arity — maxDef only counts surviving `=>` literals, so lifted/hoisted
1410
+ // function definitions slip past it (their bodies are walked, their param
1411
+ // lists aren't). Without this, e.g. an arity-3 function used only via a
1412
+ // 1-arg indirect call emits `(local.get $__a2)` against a 2-param trampoline.
1413
+ let maxValueArity = 0
1414
+ if (valueUsed) for (const name of valueUsed) {
1415
+ const n = ctx.func.map.get(name)?.sig?.params?.length ?? 0
1416
+ if (n > maxValueArity) maxValueArity = n
1417
+ }
1198
1418
  ctx.closure.width = (hasSpread && hasRest)
1199
1419
  ? MAX_CLOSURE_ARITY
1200
- : Math.min(MAX_CLOSURE_ARITY, Math.max(maxCall, maxDef + (hasRest ? 1 : 0), floor))
1420
+ : Math.min(MAX_CLOSURE_ARITY, Math.max(maxCall, maxDef + (hasRest ? 1 : 0), maxValueArity, floor))
1201
1421
  }
1202
1422
 
1203
1423
  const canSkipWholeProgramNarrowing = (programFacts) =>
@@ -1214,6 +1434,7 @@ export default function plan(ast) {
1214
1434
 
1215
1435
  let programFacts = collectProgramFacts(ast)
1216
1436
  if (inlineHotInternalCalls(programFacts, ast)) programFacts = collectProgramFacts(ast)
1437
+ if (inlineLocalLambdas()) programFacts = collectProgramFacts(ast)
1217
1438
  if (specializeFixedRestCalls(programFacts)) programFacts = collectProgramFacts(ast)
1218
1439
  if (scalarizeFunctionArrayLiterals()) programFacts = collectProgramFacts(ast)
1219
1440
  if (scalarizeFunctionObjectLiterals()) programFacts = collectProgramFacts(ast)
package/src/prepare.js CHANGED
@@ -656,7 +656,10 @@ function expandDestruct(pattern, source, out, decls = null) {
656
656
  }
657
657
 
658
658
  if (Array.isArray(item) && item[0] === ':') {
659
- pushPatternAssign(item[2], ['.', source, item[1]], out, decls)
659
+ const key = item[1]
660
+ const computedKey = Array.isArray(key) && key[0] === '[]' && key.length === 2 ? key[1] : null
661
+ if (computedKey) includeForArrayAccess()
662
+ pushPatternAssign(item[2], computedKey ? ['[]', source, computedKey] : ['.', source, key], out, decls)
660
663
  continue
661
664
  }
662
665
  }
@@ -690,7 +693,20 @@ function prepDecl(op, ...inits) {
690
693
  err('destructuring assignment after declaration must be a separate statement')
691
694
  }
692
695
 
693
- if (!Array.isArray(i) || i[0] !== '=') { rest.push(i); continue }
696
+ if (!Array.isArray(i) || i[0] !== '=') {
697
+ let declName = i
698
+ if (depth === 0 && typeof declName === 'string') {
699
+ if (ctx.module.currentPrefix) {
700
+ declName = `${ctx.module.currentPrefix}$${declName}`
701
+ ctx.scope.chain[i] = declName
702
+ }
703
+ if (ctx.scope.globals.has(declName)) err(`'${declName}' conflicts with a compiler internal — choose a different name`)
704
+ ctx.scope.globals.set(declName, `(global $${declName} (mut f64) (f64.const 0))`)
705
+ ctx.scope.userGlobals.add(declName)
706
+ }
707
+ rest.push(declName)
708
+ continue
709
+ }
694
710
  const [, name, init] = i, normed = prep(init)
695
711
 
696
712
  if (isDestructPattern(name)) {
@@ -1475,6 +1491,7 @@ const handlers = {
1475
1491
  'new'(ctor, ...args) {
1476
1492
  let name = ctor, ctorArgs = args
1477
1493
  if (Array.isArray(ctor) && ctor[0] === '()') { name = ctor[1]; ctorArgs = ctor.slice(2) }
1494
+ if (name === 'Date' && ctorArgs.length === 1 && ctorArgs[0] == null) ctorArgs = []
1478
1495
  // Flatten comma-grouped args: [',', a, b, c] → [a, b, c]
1479
1496
  if (ctorArgs.length === 1 && Array.isArray(ctorArgs[0]) && ctorArgs[0][0] === ',')
1480
1497
  ctorArgs = ctorArgs[0].slice(1)
@@ -1743,6 +1760,10 @@ function prepareModule(specifier, source) {
1743
1760
  if (!Array.isArray(node)) return typeof node === 'string' && !skip?.has(node) && moduleExports.has(node) ? moduleExports.get(node) : node
1744
1761
  if (node[0] === 'str' || node[0] == null || node[0] === '`' || node[0] === '//') return node
1745
1762
  if (node[0] === ':') { node[2] = walk(node[2], skip); return node }
1763
+ // Static member access: `obj.prop` — only the receiver is a reference; the
1764
+ // property name is a literal key and must not be renamed even if it collides
1765
+ // with a module-scoped binding (e.g. `IMM.reftype` where `const reftype` exists).
1766
+ if (node[0] === '.' || node[0] === '?.') { node[1] = walk(node[1], skip); return node }
1746
1767
  if (node[0] === '=>') {
1747
1768
  node[2] = walk(node[2], collectParamNames(extractParams(node[1]), new Set(skip)))
1748
1769
  return node
package/src/vectorize.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { findBodyStart } from './ir.js'
2
+
1
3
  /**
2
4
  * Lane-local SIMD-128 vectorizer.
3
5
  *
@@ -41,18 +43,7 @@
41
43
  * `i < BOUND` guard handles the tail.
42
44
  */
43
45
 
44
- // Local copy of findBodyStart (mirrors optimize.js): index of first non-decl
45
- // child in a (func ...) form — past type/param/result/local declarations.
46
- function findBodyStart(fn) {
47
- for (let i = 2; i < fn.length; i++) {
48
- const c = fn[i]
49
- if (!Array.isArray(c)) continue
50
- if (c[0] === 'export' || c[0] === 'import' || c[0] === 'type' ||
51
- c[0] === 'param' || c[0] === 'result' || c[0] === 'local') continue
52
- return i
53
- }
54
- return fn.length
55
- }
46
+
56
47
 
57
48
  const isArr = Array.isArray
58
49