jz 0.2.1 → 0.3.1
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/README.md +18 -26
- package/cli.js +53 -8
- package/index.js +14 -1
- package/module/array.js +66 -9
- package/module/collection.js +68 -35
- package/module/core.js +65 -19
- package/module/date.js +5 -0
- package/module/function.js +2 -1
- package/module/json.js +66 -8
- package/module/number.js +127 -5
- package/module/object.js +88 -1
- package/module/string.js +3 -2
- package/module/typedarray.js +7 -1
- package/package.json +3 -3
- package/src/analyze.js +16 -4
- package/src/assemble.js +544 -0
- package/src/ast.js +160 -0
- package/src/auto-config.js +120 -0
- package/src/autoload.js +4 -1
- package/src/compile.js +22 -620
- package/src/ctx.js +33 -5
- package/src/emit.js +73 -165
- package/src/host.js +12 -0
- package/src/ir.js +13 -0
- package/src/jzify.js +348 -9
- package/src/narrow.js +2 -1
- package/src/optimize.js +298 -24
- package/src/plan.js +220 -34
- package/src/prepare.js +22 -2
- package/src/vectorize.js +3 -12
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
169
|
-
|
|
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 <=
|
|
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
|
-
|
|
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
|
|
301
|
-
|
|
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
|
|
367
|
-
if (
|
|
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
|
|
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 >
|
|
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 <=
|
|
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
|
|
533
|
-
if (
|
|
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
|
-
|
|
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] !== '=') {
|
|
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
|
-
|
|
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
|
|