jz 0.2.1 → 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
  }
@@ -363,26 +394,27 @@ const scalarizeTypedArrayLiteralSeq = (seq) => {
363
394
  if (!Array.isArray(stmt) || (stmt[0] !== 'let' && stmt[0] !== 'const') || stmt.length !== 2) continue
364
395
  const decl = stmt[1]
365
396
  if (!Array.isArray(decl) || decl[0] !== '=' || typeof decl[1] !== 'string') continue
366
- const len = fixedScalarTypedArrayLen(decl[2])
367
- if (len == null) continue
397
+ const fixed = fixedScalarTypedArray(decl[2])
398
+ if (fixed == null) continue
399
+ const { len, coerce } = fixed
368
400
  let hasSafeUse = false, hasUnsafeUse = false
369
401
  for (let j = 0; j < stmts.length; j++) {
370
402
  if (j === i) continue
371
403
  if (!mentionsName(stmts[j], decl[1])) continue
372
- const safe = safeScalarTypedArrayUse(stmts[j], decl[1], len)
404
+ const safe = safeScalarTypedArrayUse(stmts[j], decl[1], len, coerce)
373
405
  hasSafeUse ||= safe
374
406
  hasUnsafeUse ||= !safe
375
407
  }
376
- if (!hasSafeUse && hasUnsafeUse) continue
377
- if (!hasUnsafeUse) candidates.set(decl[1], { index: i, len, mirrored: false })
378
- 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 })
379
411
  }
380
412
  if (!candidates.size && !mirrored.size) return { node: changed ? [';', ...stmts] : seq, changed }
381
413
 
382
414
  const arrays = new Map()
383
415
  for (const [name, c] of [...candidates, ...mirrored]) {
384
416
  const slots = Array.from({ length: c.len }, (_, k) => `${name}${T}ta${ctx.func.uniq++}_${k}`)
385
- arrays.set(name, { len: c.len, slots, mirrored: c.mirrored })
417
+ arrays.set(name, { len: c.len, slots, mirrored: c.mirrored, coerce: c.coerce })
386
418
  }
387
419
 
388
420
  const out = []
@@ -404,7 +436,7 @@ const scalarizeTypedArrayLiteralSeq = (seq) => {
404
436
  }
405
437
  const unsafe = []
406
438
  for (const [name, arr] of arrays) {
407
- 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])
408
440
  }
409
441
  if (unsafe.length) {
410
442
  for (const [name, arr] of unsafe) out.push(...scalarTypedArrayStores(name, arr))
@@ -464,7 +496,7 @@ function smallScalarTypedForTrip(init, cond, step) {
464
496
  if (constIntExpr(decl[2]) !== 0) return null
465
497
  if (!Array.isArray(cond) || cond[0] !== '<' || cond[1] !== name) return null
466
498
  const end = constIntExpr(cond[2])
467
- if (end == null || end < 0 || end > MAX_SCALAR_TYPED_LOOP_UNROLL) return null
499
+ if (end == null || end < 0 || end > maxScalarTypedLoopUnroll()) return null
468
500
  const stepOk = Array.isArray(step) && ((step[0] === '++' && step[1] === name) ||
469
501
  (step[0] === '-' && Array.isArray(step[1]) && step[1][0] === '++' && step[1][1] === name && constIntExpr(step[2]) === 1))
470
502
  return stepOk ? { name, end } : null
@@ -500,7 +532,7 @@ const unrollTypedArrayLoops = (node, names) => {
500
532
  }
501
533
  if (node[0] === 'for') {
502
534
  const trip = smallScalarTypedForTrip(node[1], node[2], node[3])
503
- 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() &&
504
536
  !hasControlTransfer(node[4]) && !containsDeclOf(node[4], trip.name) && !isReassigned(node[4], trip.name)) {
505
537
  const out = [';']
506
538
  for (let i = 0; i < trip.end; i++) {
@@ -529,8 +561,8 @@ const fixedTypedArraysInBody = (body) => {
529
561
  for (let i = 1; i < node.length; i++) {
530
562
  const d = node[i]
531
563
  if (!Array.isArray(d) || d[0] !== '=' || typeof d[1] !== 'string') continue
532
- const len = fixedScalarTypedArrayLen(d[2])
533
- if (len != null) out.set(d[1], { len })
564
+ const fixed = fixedScalarTypedArray(d[2])
565
+ if (fixed != null) out.set(d[1], fixed)
534
566
  }
535
567
  }
536
568
  for (let i = 1; i < node.length; i++) walk(node[i])
@@ -546,15 +578,15 @@ const scalarTypedParamCandidates = (func, sites, fixedByFunc) => {
546
578
  const cands = new Map()
547
579
  for (let i = 0; i < params.length; i++) {
548
580
  const pname = params[i].name
549
- let len = null, ok = true
581
+ let len = null, coerce = null, ok = true
550
582
  for (const site of sites) {
551
583
  const arg = site.argList[i]
552
584
  const fixed = typeof arg === 'string' ? fixedByFunc.get(site.callerFunc)?.get(arg) : null
553
585
  if (!fixed) { ok = false; break }
554
- if (len == null) len = fixed.len
555
- 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 }
556
588
  }
557
- if (ok && len != null) cands.set(pname, { len })
589
+ if (ok && len != null && len <= maxScalarTypedArrayLen()) cands.set(pname, { len, coerce })
558
590
  }
559
591
  if (!cands.size) return cands
560
592
  for (const site of sites) {
@@ -570,13 +602,14 @@ const scalarTypedParamCandidates = (func, sites, fixedByFunc) => {
570
602
  }
571
603
 
572
604
  const scalarizeTypedArrayParams = (func, paramCands) => {
573
- 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)
574
606
  for (const [name] of [...paramCands]) if (!hasScalarTypedArrayRead(func.body, name)) paramCands.delete(name)
575
607
  if (!paramCands.size) return { body: func.body, changed: false }
576
608
  const arrays = new Map()
577
609
  for (const [name, c] of paramCands) {
578
610
  arrays.set(name, {
579
611
  len: c.len,
612
+ coerce: c.coerce,
580
613
  slots: Array.from({ length: c.len }, (_, k) => `${name}${T}tap${ctx.func.uniq++}_${k}`),
581
614
  })
582
615
  }
@@ -1071,6 +1104,146 @@ const inlineHotInternalCalls = (programFacts, ast) => {
1071
1104
  return changed
1072
1105
  }
1073
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
+
1074
1247
  const restIndexExpr = (idx, restParams) => {
1075
1248
  const k = intLit(idx)
1076
1249
  if (k != null) return k >= 0 && k < restParams.length ? restParams[k] : [, undefined]
@@ -1228,11 +1401,23 @@ const materializeAutoBoxSchemas = (programFacts) => {
1228
1401
 
1229
1402
  const resolveClosureWidth = (programFacts) => {
1230
1403
  if (!ctx.closure.make) return
1231
- const { hasSpread, hasRest, maxCall, maxDef } = programFacts
1404
+ const { hasSpread, hasRest, maxCall, maxDef, valueUsed } = programFacts
1232
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
+ }
1233
1418
  ctx.closure.width = (hasSpread && hasRest)
1234
1419
  ? MAX_CLOSURE_ARITY
1235
- : 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))
1236
1421
  }
1237
1422
 
1238
1423
  const canSkipWholeProgramNarrowing = (programFacts) =>
@@ -1249,6 +1434,7 @@ export default function plan(ast) {
1249
1434
 
1250
1435
  let programFacts = collectProgramFacts(ast)
1251
1436
  if (inlineHotInternalCalls(programFacts, ast)) programFacts = collectProgramFacts(ast)
1437
+ if (inlineLocalLambdas()) programFacts = collectProgramFacts(ast)
1252
1438
  if (specializeFixedRestCalls(programFacts)) programFacts = collectProgramFacts(ast)
1253
1439
  if (scalarizeFunctionArrayLiterals()) programFacts = collectProgramFacts(ast)
1254
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)) {
@@ -1744,6 +1760,10 @@ function prepareModule(specifier, source) {
1744
1760
  if (!Array.isArray(node)) return typeof node === 'string' && !skip?.has(node) && moduleExports.has(node) ? moduleExports.get(node) : node
1745
1761
  if (node[0] === 'str' || node[0] == null || node[0] === '`' || node[0] === '//') return node
1746
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 }
1747
1767
  if (node[0] === '=>') {
1748
1768
  node[2] = walk(node[2], collectParamNames(extractParams(node[1]), new Set(skip)))
1749
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