@asmlift/core 0.6.0 → 0.8.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.
Files changed (88) hide show
  1. package/README.md +48 -24
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/backend/pascal.ts +2 -2
  5. package/src/codegen-flags.ts +640 -0
  6. package/src/contracts.ts +60 -11
  7. package/src/frontend/disasm.ts +141 -11
  8. package/src/frontend/high-half.ts +149 -0
  9. package/src/frontend/mips.ts +458 -209
  10. package/src/frontend/ppc.ts +332 -67
  11. package/src/frontend/reloc-symbol.ts +109 -0
  12. package/src/frontend/splat.ts +56 -18
  13. package/src/frontend/ssa.ts +127 -30
  14. package/src/frontend/stackargs.ts +420 -0
  15. package/src/frontend/thumb.ts +209 -232
  16. package/src/ir/alias.ts +24 -0
  17. package/src/ir/core.ts +70 -3
  18. package/src/ir/opcodes.ts +52 -7
  19. package/src/ir/parse.ts +7 -1
  20. package/src/ir/simplify.ts +1 -1
  21. package/src/l3/address.ts +2 -2
  22. package/src/l3/advance.ts +373 -0
  23. package/src/l3/argbase.ts +6 -6
  24. package/src/l3/argcopy.ts +269 -0
  25. package/src/l3/ast.ts +110 -22
  26. package/src/l3/basecse.ts +50 -30
  27. package/src/l3/coalesce.ts +118 -61
  28. package/src/l3/gates.ts +75 -1
  29. package/src/l3/hoist.ts +1 -1
  30. package/src/l3/homesplit.ts +13 -13
  31. package/src/l3/initfirst.ts +3 -3
  32. package/src/l3/inlinebase.ts +16 -16
  33. package/src/l3/mentions.ts +68 -5
  34. package/src/l3/mulfirst.ts +3 -3
  35. package/src/l3/nearbase.ts +4 -4
  36. package/src/l3/offmember.ts +5 -5
  37. package/src/l3/parkfirst.ts +6 -6
  38. package/src/l3/pollguard.ts +3 -3
  39. package/src/l3/ptrfield.ts +4 -4
  40. package/src/l3/regspell.ts +8 -8
  41. package/src/l3/reindex.ts +22 -17
  42. package/src/l3/scopebase.ts +32 -29
  43. package/src/l3/sinkinit.ts +7 -7
  44. package/src/l3/slotorder.ts +3 -3
  45. package/src/l3/storage.ts +1 -1
  46. package/src/l3/tailmerge.ts +2 -2
  47. package/src/l3/tailret.ts +70 -0
  48. package/src/l3/typing.ts +3 -3
  49. package/src/l3/unmerge.ts +483 -59
  50. package/src/l3/unreduce.ts +15 -14
  51. package/src/l3/volatileptr.ts +11 -11
  52. package/src/l3/volatileval.ts +11 -11
  53. package/src/l3/volstore.ts +16 -16
  54. package/src/l3/zerosub.ts +6 -6
  55. package/src/mangle.ts +49 -0
  56. package/src/pattern/engine.ts +132 -17
  57. package/src/pipeline.ts +39 -16
  58. package/src/proto.ts +2 -2
  59. package/src/raise/const.ts +203 -3
  60. package/src/raise/divpow2.ts +2 -2
  61. package/src/raise/extscale.ts +345 -0
  62. package/src/raise/globalshape.ts +32 -12
  63. package/src/raise/gvn.ts +2 -2
  64. package/src/raise/magicdiv.ts +2 -2
  65. package/src/raise/memberarrays.ts +4 -4
  66. package/src/raise/narrowlocal.ts +18 -2
  67. package/src/raise/paramwidth.ts +133 -3
  68. package/src/raise/pre-recovery.ts +100 -25
  69. package/src/raise/retsink.ts +389 -19
  70. package/src/raise/shortcircuit.ts +595 -34
  71. package/src/raise/structs.ts +4 -4
  72. package/src/raise/tailsink.ts +141 -0
  73. package/src/rank-declare.ts +21 -13
  74. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  75. package/src/rank.ts +1176 -805
  76. package/src/structure/analysis.ts +87 -90
  77. package/src/structure/bitfields.ts +130 -30
  78. package/src/structure/globalaccess.ts +30 -4
  79. package/src/structure/namecoalesce.ts +32 -13
  80. package/src/structure/retspell.ts +95 -0
  81. package/src/structure/structure.ts +1425 -201
  82. package/src/structure/switch-recover.ts +101 -8
  83. package/src/symbols.ts +127 -6
  84. package/src/target.ts +374 -44
  85. package/src/trace.ts +28 -19
  86. package/src/variation-definitions.ts +1590 -0
  87. package/src/variation-gates.ts +92 -0
  88. package/src/variation-tokens.ts +356 -0
@@ -1,7 +1,7 @@
1
1
  // The `/livebase-block × /regionbase` PAIRING (rank.ts `/livebase-block/homesplit`): one base kept
2
2
  // at the function head and a SECOND base split into one local per region, in the same function.
3
3
  //
4
- // WHY IT IS NOT REACHABLE FROM EITHER LEVER. Both are whole-FUNCTION policies over the bases they
4
+ // WHY IT IS NOT REACHABLE FROM EITHER VARIATION. Both are whole-FUNCTION policies over the bases they
5
5
  // bind: `hoistBaseLocals` homes every key its table admits at one placement, and
6
6
  // `hoistScopedBases` splits every key its region rule admits. A function whose two bases want
7
7
  // OPPOSITE answers is spelled by neither. `synthetic:dmapoll` is that function and its endpoint is
@@ -19,9 +19,9 @@
19
19
  // over ONE input is the shape that would collide, and nothing here does it.
20
20
  //
21
21
  // WHICH KEY IS WITHHELD IS NOT DERIVABLE, so every admitted key is offered as its own candidate,
22
- // LABELLED WITH THAT KEY, and the differ referees — the same posture `/scopebase` and `/regionbase`
23
- // take toward each other. The label carries `homeSplitTag(key)` because a candidate label is an
24
- // IDENTITY: `bench diff` and docs/ranked-repro.md compare candidates by it, so one label over two
22
+ // NAMED BY THAT KEY, and the differ referees — the same posture `/scopebase` and `/regionbase`
23
+ // take toward each other. The variation carries `homeSplitTag(key)` because a candidate's variations are its
24
+ // IDENTITY: `bench diff` and docs/ranked-repro.md compare candidates by them, so one name over two
25
25
  // withholds would hide a program swap from both. The withhold itself is DATA: one rejection
26
26
  // prepended to the caller's own admission table, in the `Gate<BaseKey>` type that table already
27
27
  // has, so `firstRejection` attributes a refusal to it, `without` ablates it, and the composed table
@@ -30,7 +30,7 @@
30
30
  // EXACTLY ONE KEY IS WITHHELD, and the arity is a claim rather than an oversight. A two-key withhold
31
31
  // exists only where the caller's table binds three — `homesplit-fan-cap` admits no more — and there
32
32
  // it is the three further pairs, each carrying the three respells rank.ts derives from one pipe:
33
- // NINE more candidates per axis point on those functions alone, before any shape product, and no
33
+ // NINE more candidates per structure setting on those functions alone, before any stacked variation, and no
34
34
  // row asks for one. `l3/volatileptr.ts`'s `volatileSubsetCandidates` enumerates every proper subset
35
35
  // under the same cap; it does that because a row demanded each of them. Widen this the same way,
36
36
  // on a row.
@@ -59,7 +59,7 @@ export const withholdingKey = (gates: readonly Gate<BaseKey>[], key: string): re
59
59
  ...gates,
60
60
  ];
61
61
 
62
- /** The withheld key as a LABEL token: `c:67109076 4 true` → `0x40000d4.4s`. Width and signedness
62
+ /** The withheld key as a variation's SUBJECT: `c:67109076 4 true` → `0x40000d4.4s`. Width and signedness
63
63
  * ride because they are part of the key — two keys over one address are two different spellings.
64
64
  *
65
65
  * PARSED BY THE KEY'S PRODUCER (`l3/basecse.ts`'s `parseBaseKey`, beside `keyOf`), because a base
@@ -68,9 +68,9 @@ export const withholdingKey = (gates: readonly Gate<BaseKey>[], key: string): re
68
68
  * separator rather than the type's. Split from the FRONT, the type reads as the width and the
69
69
  * width as the signedness; split from the END, the `a:` form comes out right and the `c:` form
70
70
  * runs `Number` over `67109076 <u16*>`, tagging every cast over a numeric base `0xNaN` and
71
- * collapsing distinct keys onto one label. No shipped table admits a cast base outside
71
+ * collapsing distinct keys onto one name. No shipped table admits a cast base outside
72
72
  * `/orderbase`, and `/orderbase` carries `pairings: false`, so no such key reaches this function
73
- * today — but a candidate LABEL is an identity (`bench diff` and docs/ranked-repro.md compare
73
+ * today — but a candidate's variations are its identity (`bench diff` and docs/ranked-repro.md compare
74
74
  * candidates by it), and one roster line is all that stands between the two. The sibling half of
75
75
  * the same hazard is already guarded in `splitHomeBases`, which translates a cast base to no
76
76
  * region key and declines. */
@@ -79,7 +79,7 @@ export function homeSplitTag(key: string): string {
79
79
  const base = leaf.startsWith('c:') ? `0x${Number(leaf.slice(2)).toString(16)}` : leaf.slice(leaf.indexOf(':') + 1);
80
80
  // The cast's element type stays in the token — it is part of the key's identity, since two casts
81
81
  // over one symbol are two locals that stride differently. Whitespace is squeezed defensively
82
- // rather than because any type spells one: a label is one whitespace-free word everywhere it is
82
+ // rather than because any type spells one: a variation is one whitespace-free word everywhere it is
83
83
  // read, and that has to hold whatever `typeToString` grows.
84
84
  const type = castType === null ? '' : `<${castType}>`;
85
85
  return `${`${base}${type}`.replace(/\s+/g, '')}.${width}${signed ? 's' : 'u'}`;
@@ -98,13 +98,13 @@ export interface HomeSplitFanCtx {
98
98
  export const HOMESPLIT_FAN_GATES: readonly Gate<HomeSplitFanCtx>[] = [
99
99
  {
100
100
  id: 'homesplit-degenerate',
101
- why: 'withholding the only hoistable key is `/regionbase`, and withholding none is `/livebase-block`',
101
+ why: 'with one hoistable base there is nothing new: withholding it is `regionbase`, and withholding none is `livebase-block`',
102
102
  sound: false,
103
103
  rejects: (c) => c.hoistableKeys < 2,
104
104
  },
105
105
  {
106
106
  id: 'homesplit-fan-cap',
107
- why: 'one candidate per hoistable key, times the volatile products the whole cost of the axis',
107
+ why: 'more than three hoistable bases would add a candidate for each, times the volatile pairings',
108
108
  sound: false,
109
109
  rejects: (c) => c.hoistableKeys > 3,
110
110
  },
@@ -143,13 +143,13 @@ export interface HomeSplitCtx {
143
143
  export const HOMESPLIT_GATES: readonly Gate<HomeSplitCtx>[] = [
144
144
  {
145
145
  id: 'homesplit-no-region',
146
- why: 'a withheld key the region rule declines to split leaves the spelling the primary carries',
146
+ why: 'a base held back from the function-top local that no per-region local takes instead leaves the spelling the default carries',
147
147
  sound: false,
148
148
  rejects: (c) => !c.withheldSplits,
149
149
  },
150
150
  {
151
151
  id: 'homesplit-drops-device-volatile',
152
- why: 'a device read left inline is qualified by neither /volatile nor /vol-store',
152
+ why: 'a device read left inline would carry neither `volatile` nor `vol-store`',
153
153
  sound: false,
154
154
  rejects: (c) => c.inlineDeviceRead,
155
155
  },
@@ -1,10 +1,10 @@
1
- // L3 re-spelling lever: a loop INIT moves above the guard that encloses it, and the guard reads
1
+ // L3 respell variation: a loop INIT moves above the guard that encloses it, and the guard reads
2
2
  // the initialized variable.
3
3
  //
4
4
  // `for (i = 0; i < n; i++)` compiles with the init BEFORE the zero-trip test (`mov r3,#0` then
5
5
  // `cmp r3, r5`), while `if (0 < n) { i = 0; do … }` compiles with the init behind the branch.
6
6
  // Both source forms lift to the SAME IR — a const has no position — so which the original spelled
7
- // is not recoverable; this lever emits the init-first sibling and the differ referees:
7
+ // is not recoverable; this variation emits the init-first sibling and the differ referees:
8
8
  //
9
9
  // if (0 < n) { v = 0; … } → v = 0; if (v < n) { … }
10
10
  //
@@ -61,7 +61,7 @@ const stripWideIntCast = (e: Expr): Expr =>
61
61
 
62
62
  /** Every deref rooted at a var through casts only. The var-root rule is a TWO-WORLD argument, not a
63
63
  * volatility proof: a deref through a plain-declared pointer local may still be MMIO, but the
64
- * /volatile axis enumerates the qualified sibling — where this lever refuses — so both worlds reach
64
+ * /volatile variation enumerates the qualified sibling — where this variation refuses — so both worlds reach
65
65
  * the differ and collapsing reads here is the plain world's own premise. A raw `*(u16 *)CONST` deref
66
66
  * has NO local for /volatile to qualify, so no sibling carries the volatile world and the collapse
67
67
  * would silently discard it. */
@@ -1,4 +1,4 @@
1
- // L3 re-spelling lever: DELETE a pointer local holding a CONSTANT address and spell each access
1
+ // L3 respell variation: DELETE a pointer local holding a CONSTANT address and spell each access
2
2
  // through it as the cast constant (`*(u16 *)0x4000208` rather than `p = (u16 *)0x4000208; *p`).
3
3
  //
4
4
  // The local is structure/analysis.ts's value-home spelling for a `const` with 2+ consumers that
@@ -25,34 +25,34 @@
25
25
  // separate at 11 against 12 on their own, and are BYTE-IDENTICAL once the slot qualifier is
26
26
  // there too — the shape that matches (agbcc 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit
27
27
  // -fhex-asm -fprologue-bugfix`; `.s` diff empty, `.o` identical under cmp). So rank.ts emits
28
- // the qualified spelling as a second OUTPUT of this lever, `/volatile` narrowed to the locals
28
+ // the qualified spelling as a second OUTPUT of this variation, `/volatile` narrowed to the locals
29
29
  // it deletes. Each cast this mints carries the qualifier; a use whose width does not stride the
30
30
  // declared pointee renders through the C-family printer's reinterpret cast instead, which
31
31
  // carries it too (backend/cfamily.ts) — in C the access takes the OUTER type, so a plain cast
32
32
  // there would spell exactly the silent drop this paragraph exists to prevent.
33
33
  //
34
34
  // GATE (INLINEBASE_GATES) — the local must be all of: pointer-typed; initialized by a bare
35
- // NONZERO `const` (a `(T *)base` CAST initializer is l3/basecse.ts's reuse hoist, whose own lever
35
+ // NONZERO `const` (a `(T *)base` CAST initializer is l3/basecse.ts's reuse hoist, whose own variation
36
36
  // family owns that question; `0` is NULL, never an address, which is also the sibling qualifier
37
- // lever's rule); assigned exactly once, by a statement at the body's TOP LEVEL that no earlier
37
+ // variation's rule); assigned exactly once, by a statement at the body's TOP LEVEL that no earlier
38
38
  // statement's mention precedes; never address-taken; used only as the base of an `index`, at 2+
39
39
  // sites (one use is not the reused address this exists for); not object-`volatile` (a
40
40
  // `T *volatile p` has no inhabitant, and cfamily.ts prints that flag in the pointee's position);
41
41
  // and not `frame` (a slot is an asm fact — see the SFn.locals doc). Anything else, and nothing
42
42
  // qualifying at all, DECLINES (null) rather than approximating.
43
43
  //
44
- // A RE-SPELLING RATHER THAN A STRUCTURING AXIS, which is a cost choice and not the
45
- // underdetermination one (docs/level-tower.md, "a third fork sits inside the ranked population").
44
+ // A RESPELL VARIATION RATHER THAN A STRUCTURE VARIATION, which is a cost decision and not the
45
+ // underdetermination one (docs/level-tower.md, "a third fork sits inside the ranked variations").
46
46
  // The question — does a `const` with 2+ consumers live across a call in a named local — is the one
47
47
  // `/reread-globals`, `/addr-home`, `/expr-home` and `/derived-home` each answer as a
48
- // STRUCTURING_AXES entry. An axis here would double the enumeration on every function it admits;
48
+ // STRUCTURE_VARIATIONS entry. A structure variation here would double the enumeration on every function it admits;
49
49
  // substituting on the already-homed tree costs 766 candidates over 47058 (+1.6%) across the 33 of
50
50
  // 69 klonoa functions that lift with no symbol map.
51
51
  //
52
52
  // KNOWN GAP, and it is the price of that choice rather than an oversight: only `index` bases are
53
53
  // re-spelled, so the same L2 home passed to a callee or standing as a `field` base is out of
54
54
  // reach — `otherUses` refuses it. Reaching those needs the un-homed tree, which is
55
- // structure/analysis.ts's decision; the day a row demands one, this becomes the axis.
55
+ // structure/analysis.ts's decision; the day a row demands one, this becomes a structure variation.
56
56
  //
57
57
  // The idiom it recovers is every GBA project's register macro: `*(vu16 *)0x4000208` is what
58
58
  // `REG_IME` expands to, so the deleted local is not merely an undone home.
@@ -71,20 +71,20 @@ interface BaseCtx {
71
71
  export const INLINEBASE_GATES: readonly Gate<BaseCtx>[] = [
72
72
  {
73
73
  id: 'non-pointer',
74
- why: 'the lever re-spells an address; a scalar value home is a different question',
74
+ why: 'only a local holding an address is substituted; a local holding a scalar value is a different question',
75
75
  sound: false,
76
76
  rejects: (c) => !c.isPointer,
77
77
  },
78
78
  {
79
79
  id: 'object-volatile',
80
- why: 'the substitution carries the POINTEE flag, so an object-volatile pointer would lose its own',
80
+ why: 'the substitution keeps only the `volatile` of what the pointer points to, so a pointer that is itself volatile would lose its own',
81
81
  sound: true,
82
82
  guardedBy: 'inlinebase.test.ts: an object-volatile or frame local declines',
83
83
  rejects: (c) => c.objectVolatile,
84
84
  },
85
85
  {
86
86
  id: 'frame',
87
- why: 'a slot the asm materialized is an asm fact, not a spelling to undo',
87
+ why: 'a stack slot the assembly used is a fact about the function, not a spelling to undo',
88
88
  sound: false,
89
89
  rejects: (c) => c.hasFrame,
90
90
  },
@@ -104,7 +104,7 @@ export const INLINEBASE_GATES: readonly Gate<BaseCtx>[] = [
104
104
  },
105
105
  {
106
106
  id: 'null-base',
107
- why: '`0` is NULL, never an address the sibling qualifier lever (volatileptr.ts) refuses it too',
107
+ why: 'a base of `0` is a null pointer, never an address, and `volatile` refuses it too',
108
108
  sound: false,
109
109
  rejects: (c) => c.m.constValue === 0,
110
110
  },
@@ -126,12 +126,12 @@ export const INLINEBASE_GATES: readonly Gate<BaseCtx>[] = [
126
126
  id: 'other-uses',
127
127
  why: 'a use the substitution cannot reach would name the deleted local',
128
128
  sound: true,
129
- guardedBy: 'inlinebase.test.ts: a use that is not an `index` base is outside what the lever re-spells',
129
+ guardedBy: 'inlinebase.test.ts: a use that is not an `index` base is outside what the variation re-spells',
130
130
  rejects: (c) => c.m.otherUses !== 0,
131
131
  },
132
132
  {
133
133
  id: 'single-use',
134
- why: 'one use is not the reused address this lever exists for',
134
+ why: 'one use is not the reused address this variation exists for',
135
135
  sound: false,
136
136
  rejects: (c) => c.m.baseUses < 2,
137
137
  },
@@ -164,8 +164,8 @@ function plan(sfn: SFn): Map<string, Extract<Expr, { k: 'cast' }>> {
164
164
  return out;
165
165
  }
166
166
 
167
- /** Which locals this lever would delete — rank.ts narrows `/volatile` to exactly these before
168
- * pairing, so the qualified output never qualifies a pointer the lever leaves standing. */
167
+ /** Which locals this variation would delete — rank.ts narrows `/volatile` to exactly these before
168
+ * pairing, so the qualified output never qualifies a pointer the variation leaves standing. */
169
169
  export function inlinableConstBases(sfn: SFn): string[] {
170
170
  return [...plan(sfn).keys()];
171
171
  }
@@ -1,6 +1,6 @@
1
1
  // What one L3 tree does to each of its locals, counted once.
2
2
  //
3
- // Levers ask overlapping versions of the question and would disagree if each walked the tree
3
+ // Respell variations ask overlapping versions of the question and would disagree if each walked the tree
4
4
  // its own way: l3/inlinebase.ts needs the SHAPE of every use (only an `index` base is re-spellable,
5
5
  // and the single assignment must be a top-level `const` nothing mentions earlier), while
6
6
  // l3/volatileval.ts needs the COUNTS, to check the tree still performs every access the machine
@@ -14,7 +14,7 @@
14
14
  // oversight: a POSITION added to `index` reaches the generic vocabulary for free and this walk not
15
15
  // at all. Every position is enumerated below and pinned by
16
16
  // test/array-rank-guards.test.ts, beside the generic helpers it cannot speak for.
17
- import { type Expr, type SFn, type Stmt, exprChildren, stmtChildren, stmtExprs } from './ast';
17
+ import { type Expr, type SFn, type Stmt, exprChildren, stmtChildren, stmtExprs, walkExprs } from './ast';
18
18
 
19
19
  export interface Mentions {
20
20
  /** assignments to the name, at any nesting */
@@ -24,7 +24,7 @@ export interface Mentions {
24
24
  /** the bare-`const` value that assignment stores, or null if it stores anything else */
25
25
  constValue: number | null;
26
26
  addrTaken: number;
27
- /** uses as the `base` of an `index` node — the only use shape a base lever can re-spell */
27
+ /** uses as the `base` of an `index` node — the only use shape a base-hoisting variation can re-spell */
28
28
  baseUses: number;
29
29
  /** every other read */
30
30
  otherUses: number;
@@ -53,7 +53,7 @@ function walkExpr(e: Expr, visit: (x: Expr, isIndexBase: boolean) => void, isInd
53
53
  if (e.k === 'index') {
54
54
  walkExpr(e.base, visit, true);
55
55
  // `lead` — a multidimensional global's LEADING subscripts — is an ordinary value position, so
56
- // a name mentioned there is a real read. Missing it does not cost a candidate: it lets a lever
56
+ // a name mentioned there is a real read. Missing it does not cost a candidate: it lets a variation
57
57
  // DELETE a local the body still names.
58
58
  for (const l of e.lead ?? []) {
59
59
  walkExpr(l, visit, false);
@@ -67,7 +67,7 @@ function walkExpr(e: Expr, visit: (x: Expr, isIndexBase: boolean) => void, isInd
67
67
  }
68
68
 
69
69
  /** Every mention of every local, keyed by name. Locals only — a param or a global name is not in
70
- * the map, and a lever asking about one gets `undefined` rather than a zeroed record. */
70
+ * the map, and a pass asking about one gets `undefined` rather than a zeroed record. */
71
71
  export function localMentions(sfn: SFn): Map<string, Mentions> {
72
72
  const t = new Map<string, Mentions>(sfn.locals.map((l) => [l.name, blank()]));
73
73
  const seen = (name: string, at: number): Mentions | undefined => {
@@ -111,3 +111,66 @@ export function localMentions(sfn: SFn): Map<string, Mentions> {
111
111
  sfn.body.forEach((s, i) => stmt(s, i, true));
112
112
  return t;
113
113
  }
114
+
115
+ /** THE ONE WALK behind `mentionsAnyLocal` and `mentionedLocals` below: which of `names` anything
116
+ * under `stmts` still NAMES — as an assignment TARGET (which carries no expression, so no walk
117
+ * over values can see it), as a read, or as an address. `first` returns at the first hit it
118
+ * reaches, which is all the boolean caller needs.
119
+ *
120
+ * The question a pass that DELETES a declaration has to answer, and it lives here rather than in
121
+ * the deleting pass for the reason this file's header states about its own walk: a second walk
122
+ * over the node vocabulary is how a new node kind becomes a silent undercount, and beside
123
+ * `localMentions` a divergence is at least visible. This one is answered over a SUBTREE, so it
124
+ * cannot be derived from the counts above — `localMentions` is keyed to `sfn.locals` across the
125
+ * whole body, and l3/unmerge.ts's whole point is that those counts are sampled before any
126
+ * rewriting and go stale.
127
+ *
128
+ * TOTAL over the vocabulary by construction: `assign` is the only `Stmt` carrying a bare name and
129
+ * `var`/`addr` the only `Expr`s, and both walks are derived from `stmtChildren`/`stmtExprs` — so a
130
+ * `for`'s init and inc, a `switch`'s scrutinee, its cases and its default are all covered. */
131
+ function scanMentions(stmts: readonly Stmt[], names: ReadonlySet<string>, first: boolean): Set<string> {
132
+ // TWO FLAT SWEEPS, not one expression walk per nesting level. `walkExprs` already descends
133
+ // `stmtChildren` (ast.ts), so calling it per statement from inside a recursion that ALSO
134
+ // descends re-walks every nested expression once per enclosing level — quadratic in the nesting
135
+ // depth, 301 `has` calls at depth 24 where one pass needs 25. Small at today's call sites (one
136
+ // rewritten subtree per un-merge site, one dropped-locals set per respelled tree), but this is a
137
+ // SHARED helper and its cost belongs in its contract.
138
+ const found = new Set<string>();
139
+ const body = [...stmts] as Stmt[];
140
+ const stack: Stmt[] = [...body];
141
+ while (stack.length > 0) {
142
+ const s = stack.pop()!;
143
+ if (s.k === 'assign' && names.has(s.name)) {
144
+ found.add(s.name);
145
+ if (first) {
146
+ return found;
147
+ }
148
+ }
149
+ stack.push(...stmtChildren(s));
150
+ }
151
+ for (const e of walkExprs(body)) {
152
+ if ((e.k === 'var' || e.k === 'addr') && names.has(e.name)) {
153
+ found.add(e.name);
154
+ if (first) {
155
+ return found;
156
+ }
157
+ }
158
+ }
159
+ return found;
160
+ }
161
+
162
+ /** true when anything under `stmts` still names one of `names`. See `scanMentions` above. */
163
+ export function mentionsAnyLocal(stmts: readonly Stmt[], names: ReadonlySet<string>): boolean {
164
+ return scanMentions(stmts, names, true).size > 0;
165
+ }
166
+
167
+ /** WHICH of `names` the tree still mentions — the same walk `mentionsAnyLocal` answers "any" over,
168
+ * told to keep going, for the caller that has to NAME the survivors.
169
+ *
170
+ * It exists so that contracts.ts's `assertNoOrphanedLocals` — the loud backstop for exactly the
171
+ * mistake this predicate guards — does not carry a THIRD hand-rolled copy of the node vocabulary.
172
+ * The boolean cannot serve it (the diagnostic needs the set) and one call per dropped name would
173
+ * be a walk per name. */
174
+ export function mentionedLocals(stmts: readonly Stmt[], names: ReadonlySet<string>): Set<string> {
175
+ return scanMentions(stmts, names, false);
176
+ }
@@ -1,10 +1,10 @@
1
- // L3 re-spelling lever: put the PRODUCT operand first in a commutative `+`.
1
+ // L3 respell variation: put the PRODUCT operand first in a commutative `+`.
2
2
  //
3
3
  // structure.ts's def-order rule spells commutative operands in EVALUATION order, which recovers
4
4
  // gcc's left-to-right source order. IDO and mwcc break the correspondence for exactly one shape:
5
5
  // in `a*b + c` they SCHEDULE the independent load of `c` above the product's `mflo`/`mullw`, so
6
6
  // the machine add reads (c, product) and def order re-spells the source's product-first sum as
7
- // c-first. Which order the source used is not recoverable from positions there — so this lever
7
+ // c-first. Which order the source used is not recoverable from positions there — so this variation
8
8
  // emits the product-first sibling and the differ referees (verified byte-identical against IDO
9
9
  // on the bg_area row; the def-order spelling stays in the list for sources that really were
10
10
  // c-first).
@@ -12,7 +12,7 @@
12
12
  // SCOPE (decline over approximate): a `+` is flipped only when exactly ONE side is a product
13
13
  // (`bin('*')` at the root, casts looked through) — two products or none leave nothing to anchor
14
14
  // the flip on. A side carrying an effect (a call, a marker) never moves — evaluation order of
15
- // the operands is what the lever edits. Declines (null) when no `+` changes, so no duplicate
15
+ // the operands is what the variation edits. Declines (null) when no `+` changes, so no duplicate
16
16
  // candidate.
17
17
  import type { Expr, SFn } from './ast';
18
18
  import { exprHasEffect, mapExprChildren, mapStmtExprs } from './ast';
@@ -1,9 +1,9 @@
1
- // L3 re-spelling lever: NEIGHBOR absolute addresses derive from one shared base local.
1
+ // L3 respell variation: NEIGHBOR absolute addresses derive from one shared base local.
2
2
  //
3
3
  // A cluster of raw-address accesses a few bytes apart is one object's cells: the compiler holds
4
4
  // the object's base in a register and derives each cell (`add #72` / `add #74` off one pool
5
5
  // word, a halfword offset beyond the load range forcing the add, the in-range word staying
6
- // `[rN, #112]`), where a per-cell spelling anchors one pool constant per address. This lever
6
+ // `[rN, #112]`), where a per-cell spelling anchors one pool constant per address. This variation
7
7
  // re-spells every deref base in a cluster as an offset from a `u8 *` base local holding the
8
8
  // cluster's lowest address, and the differ referees:
9
9
  //
@@ -21,7 +21,7 @@
21
21
  // VALUE inside its window re-spells too, as `(s32)(b + off)` — the address of a cell handed to
22
22
  // something (a DMA source register) is the same derived add in the original, and the two
23
23
  // spellings are value-equal by construction, so the differ referees — including an integer that
24
- // only coincidentally lands in the window, which is the stated cost of the lever (the `s32` cast
24
+ // only coincidentally lands in the window, which is the stated cost of the variation (the `s32` cast
25
25
  // assumes addresses below 2^31, true of every target that declares nearBaseSpan today). Declines
26
26
  // (null) when no cluster forms.
27
27
  import { baseConst } from './address';
@@ -31,7 +31,7 @@ import { type BaseInit, nameAllocator, placeBaseLocals } from './hoist';
31
31
 
32
32
  /** `span` is the target's single-add-immediate derivation reach
33
33
  * (TargetDescription.compilerBehaviors.nearBaseSpan) — a target that declares none never runs
34
- * this lever. */
34
+ * this variation. */
35
35
  export function nearBaseClusters(sfn: SFn, span: number): SFn | null {
36
36
  if (!Number.isFinite(span) || span < 0) {
37
37
  return null; // a hostile span stalls the cluster window instead of shrinking it
@@ -38,7 +38,7 @@
38
38
  // displacement. A window refusal removes ZERO further bases in either configuration.
39
39
  //
40
40
  // PREMISE. It would be refusing a dropped qualifier, and there is none to drop: `/volatile`
41
- // wraps the base in a CAST that `non-leaf-base` refuses, so the two levers cannot compose and
41
+ // wraps the base in a CAST that `non-leaf-base` refuses, so the two variations cannot compose and
42
42
  // the tree this pass is handed is unqualified in both configurations. Nor is a tie-break lost —
43
43
  // `deviceVolatileClaims` counts only qualifiers a tree already asserts, so an unqualified tree
44
44
  // scores zero whichever way it is spelled.
@@ -96,7 +96,7 @@
96
96
  // "PROTECTS NO ROW" IS A CENSUS AND NOT A SAMPLE, and the population is small because this is an
97
97
  // extra GATE: it only ever removes candidates, so only a row whose winner carries `/offmember` can
98
98
  // move at all. THAT POPULATION IS A QUERY, not a count to keep in step by hand — the rows of
99
- // `apps/benchmark/results/results.json` whose `asmlift.candidateLabel` contains `offmember` — and
99
+ // `apps/benchmark/results/results.json` whose `asmlift.winnerVariations` contains `offmember` — and
100
100
  // it grows with the corpus, so re-run it before repeating the result. It held eight rows besides
101
101
  // `ProcessInputAndUpdateEntities` when the census ran, all unmoved with the rule on:
102
102
  // `synthetic:basecell`, `synthetic:bgfixed`, `synthetic:foldsink`, `sa3:sub_802DFC8` and
@@ -222,7 +222,7 @@ function membersOf(sites: readonly Site[]): Site[] {
222
222
  * THE FOURTH IS NOT ABOUT PLACEMENT, and it is the easy one to leave out: two views of one
223
223
  * offset at one width but
224
224
  * DIFFERENT SIGNEDNESS (`ldrb` and `ldrsb` at the same address). One member has one type, so
225
- * respelling both through it changes what one of the two READS — `scalarTypeForAccess` honours
225
+ * spelling both through it changes what one of the two READS — `scalarTypeForAccess` honours
226
226
  * signedness at widths 1 and 2, so an unsigned read becomes sign-extending. That is a value
227
227
  * change rather than a spelling change, and the differ can referee it only by luck: a masked or
228
228
  * compared result compiles to the same bytes while the published C says something the asm does
@@ -328,7 +328,7 @@ function admit(
328
328
 
329
329
  /** The gate table an ablation swaps out. Optional, so a caller gets the shipped table by default.
330
330
  * The pass needs nothing else from the target: the fold this exists for is `foldsConstAddrOffset`
331
- * and rank.ts asks that before offering the axis at all. */
331
+ * and rank.ts asks that before offering the variation at all. */
332
332
  export interface OffmemberOpts {
333
333
  readonly gates?: readonly Gate<OffmemberBase>[];
334
334
  }
@@ -339,7 +339,7 @@ export function offmemberBases(sfn: SFn, opts: OffmemberOpts = {}): readonly str
339
339
  }
340
340
 
341
341
  /** Re-spell every admitted base's constant subscripts as members of a synthesized struct.
342
- * `null` when nothing is admitted — the axis then contributes no candidate. */
342
+ * `null` when nothing is admitted — the variation then contributes no candidate. */
343
343
  export function spellOperandMembers(sfn: SFn, opts: OffmemberOpts = {}): SFn | null {
344
344
  // Past EVERY `Off<N>` the tree already carries, never the first free one (`ir/struct-names.ts`,
345
345
  // shared with the other two minters, which also records that this scan returns 0 on every
@@ -1,18 +1,18 @@
1
- // L3 re-spelling lever: park incoming ARGUMENTS first in the entry straight-line prefix.
1
+ // L3 respell variation: park incoming ARGUMENTS first in the entry straight-line prefix.
2
2
  //
3
3
  // A copy of an incoming parameter into a local (`v = a1`) reproduces the register park the
4
4
  // compiler performed to free a caller-save register (`mov ip, r1`). The park instruction lifts
5
5
  // to pure SSA aliasing — no op, no position — so the emitted order falls out of block emission
6
6
  // (materialized statements first, edge copies last), while the compiler may have parked BEFORE
7
7
  // any of those statements ran (hipress homes its counter in ip before loading a byte into the
8
- // vacated r1). Both orders are legitimate C for the same asm; this lever emits the park-first
8
+ // vacated r1). Both orders are legitimate C for the same asm; this variation emits the park-first
9
9
  // sibling and the differ referees.
10
10
  //
11
11
  // SCOPE (decline over approximate): only plain assigns in the ENTRY straight-line prefix (the
12
12
  // leading run of assigns) move; a park's RHS must be pure over PARAMETERS AND CONSTANTS through
13
13
  // scalar nodes only (var/const/un/bin/cast — a memory read or a call would be re-scheduled, not
14
14
  // re-spelled), so a constant initializer qualifies as a park and a param leaf is NOT required —
15
- // what this lever moves is the leading run's ORDER, and a constant is as free of position as a
15
+ // what this variation moves is the leading run's ORDER, and a constant is as free of position as a
16
16
  // parked register is; and a park never crosses a statement that writes a name it reads, reads or
17
17
  // writes its destination (`&v` counts as touching v), or carries an effect. Relative order — of the
18
18
  // parks and of everything else — is preserved. Declines (null) when nothing moves.
@@ -22,11 +22,11 @@
22
22
  // spells a store to a bare scalar GLOBAL as an `assign` like any other, so such a store parks too.
23
23
  // The crossing checks are NAME-KEYED, so a crossed statement that reaches the destination (or a
24
24
  // name the park reads) through an ALIAS rather than by name is invisible to them — the same
25
- // name-keyed model every lever at this level defers aliasing to.
25
+ // name-keyed model every respell variation at this level defers aliasing to.
26
26
  //
27
- // The kmc hipress residual is this axis's OTHER projection — its keep-load renders first while
27
+ // The kmc hipress residual is this variation's OTHER projection — its keep-load renders first while
28
28
  // gcc2.7.2 schedules it last — so a second inhabitant consolidates both into one entry-prefix
29
- // ordering lever rather than growing a sibling.
29
+ // ordering variation rather than growing a sibling.
30
30
  import type { Expr, SFn, Stmt } from './ast';
31
31
  import { exprChildren, exprHasEffect } from './ast';
32
32
 
@@ -1,4 +1,4 @@
1
- // L3 poll-shape re-spelling levers: `pollGuards` regrows an empty bottom-tested loop's guard;
1
+ // L3 poll-shape respell variations: `pollGuards` regrows an empty bottom-tested loop's guard;
2
2
  // `pollReads` folds a materialized poll's re-read back into its while condition. Each carries
3
3
  // its own trace argument below.
4
4
  //
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // For an empty body the two forms compile to the SAME instructions — gcc collapses the guard
8
8
  // into the bottom test late (jump optimization), AFTER flow has counted the guard's reads — so
9
- // the choice leaves no instruction trace, only a register-allocation ripple: the extra
9
+ // the decision leaves no instruction trace, only a register-allocation ripple: the extra
10
10
  // source-level read raises the condition operands' ref counts, which re-orders the allocator's
11
11
  // priorities for the WHOLE function (the busy-wait's base landing in a low reg vs `ip`). Which
12
12
  // form the source spelled is unrecoverable from the bytes; both are emitted and the differ
@@ -34,7 +34,7 @@ export function pollGuards(sfn: SFn): SFn | null {
34
34
  return changed ? { ...sfn, body } : null;
35
35
  }
36
36
 
37
- // L3 re-spelling lever: a materialized POLL re-reads in its own condition.
37
+ // L3 respell variation: a materialized POLL re-reads in its own condition.
38
38
  //
39
39
  // v = dma[2]; while ((v & BUSY) != 0) { v = dma[2]; } → while ((dma[2] & BUSY) != 0) {}
40
40
  //
@@ -1,4 +1,4 @@
1
- // L3 re-spelling lever: declare a recovered WORD field a POINTER (`void *field_4;` rather than
1
+ // L3 respell variation: declare a recovered WORD field a POINTER (`void *field_4;` rather than
2
2
  // `s32 field_4;`), and cast at each read.
3
3
  //
4
4
  // raise/structs.ts recovers a field's type from the ACCESS WIDTH alone — a 4-byte load is `s32`,
@@ -34,13 +34,13 @@
34
34
  // each use computes the same value it did. Nothing else moves. THE 32-BIT ASSUMPTION IS ASSERTED,
35
35
  // NOT CHECKED — there is no pointer-width field on TargetDescription to check it against, and the
36
36
  // assumption is already tower-wide (`l3/typing.ts`'s `ptrElemBytes` returns 4 for any pointee). On
37
- // a 64-bit target this lever would change a struct's LAYOUT rather than only its spelling, so the
37
+ // a 64-bit target this variation would change a struct's LAYOUT rather than only its spelling, so the
38
38
  // width field is what that target's first row must add, and this note is where to start.
39
39
  //
40
40
  // IT FLIPS EVERY ADMITTED FIELD AT ONCE, and its own paragraph above says the knowledge is
41
41
  // PER-FIELD — so the subset enumeration `l3/volatileptr.ts` does for exactly this reason
42
- // (`volatileSubsetCandidates`, capped at three locals) is the shape this lever will eventually
43
- // want. It is not built yet because nothing demands it: swept over 834 corpus trees, the lever
42
+ // (`volatileSubsetCandidates`, capped at three locals) is the shape this variation will eventually
43
+ // want. It is not built yet because nothing demands it: swept over 834 corpus trees, the variation
44
44
  // fires on 42, and 34 of those have a single admitted field. The six 2-field trees and the two
45
45
  // 4-field ones (`sa3:sa2__sub_8083504` flips Struct0.field_8/12 and Struct2.field_8/12 together)
46
46
  // are where 1 of 3 and 1 of 15 non-empty subsets is reachable. A row that needs one of the missing
@@ -1,4 +1,4 @@
1
- // asmlift L3 — the REGISTER-COPY re-spelling, a differ-ranked representation lever (the fourth,
1
+ // asmlift L3 — the REGISTER-COPY respell variation, differ-ranked like the three before it (the fourth,
2
2
  // after signedness / branch sense / walk-vs-index).
3
3
  //
4
4
  // A compiler's register allocation leaves SOURCE-visible footprints the coalescing structurer
@@ -26,7 +26,7 @@
26
26
  // a bin operand materializes into its own fresh local first (`v = 128 << 9; … v / x …`) —
27
27
  // the register the compiler staged the constant in.
28
28
  // R3 — return assign-back: a non-var return expression lands in a fresh local first
29
- // (`r = E; return r`). Emitted as a SEPARATE variant (with/without R3) when R1/R2 fired:
29
+ // (`r = E; return r`). Emitted as a SEPARATE result (with/without R3) when R1/R2 fired:
30
30
  // which tail the source spelled is itself ambiguous.
31
31
  import { IrType, T } from '../ir/types';
32
32
  import { Expr, SFn, Stmt, exprEquals, mapExprChildren } from './ast';
@@ -124,8 +124,8 @@ function isConstExpr(e: Expr): boolean {
124
124
  * var and only the fresh spelling exists — so the tails are a 1- OR 2-element list and the SECOND
125
125
  * spelling is not at a fixed index.
126
126
  *
127
- * This field exists because the caller labels these, and a label is the instrument every census
128
- * in this repo reads (it is a `bench diff` FIELD). Index a `['/regcopy', '/regcopy-ret',
127
+ * This field exists because the caller names these, and a candidate's variations are the instrument
128
+ * every census in this repo reads (a `bench diff` FIELD). Index a `['/regcopy', '/regcopy-ret',
129
129
  * '/regcopy-ret-fresh']` table by POSITION instead and an R1-less function publishes its fresh
130
130
  * tail as `/regcopy-ret` — the dead-var-reuse name on the spelling that has no dead var to reuse
131
131
  * — and a census over the token `/regcopy-ret-fresh` then censuses nothing wherever R1 declines,
@@ -133,7 +133,7 @@ function isConstExpr(e: Expr): boolean {
133
133
  export type RegcopyTail = 'none' | 'reuse' | 'fresh';
134
134
 
135
135
  export interface RegcopySpelling {
136
- /** which R3 tail this variant carries — `none` is the un-tailed base */
136
+ /** which R3 tail this result carries — `none` is the un-tailed base */
137
137
  tail: RegcopyTail;
138
138
  sfn: SFn;
139
139
  }
@@ -213,7 +213,7 @@ function renameSubexpr(e: Expr, E: Expr, name: string): Expr {
213
213
  return mapExprChildren(e, (c) => renameSubexpr(c, E, name));
214
214
  }
215
215
 
216
- /** Apply the register-copy re-spelling. Returns 0–3 variants — the base, plus the R3 tail in each
216
+ /** Apply the register-copy re-spelling. Returns 0–3 results — the base, plus the R3 tail in each
217
217
  * spelling that exists (reuse needs R1 to have fired, fresh always does) — and an EMPTY list when
218
218
  * nothing fired. Pure — never mutates the input. */
219
219
  export function registerishSpellings(sfn: SFn): RegcopySpelling[] {
@@ -247,7 +247,7 @@ export function registerishSpellings(sfn: SFn): RegcopySpelling[] {
247
247
  if (m) {
248
248
  const { v, E, updArm, upd, cond } = m;
249
249
  // GUARDS BEFORE ALLOCATION (a declined shape must leave no residue — the leaked
250
- // dead `w` even perturbed the live-name count this lever exists to reproduce):
250
+ // dead `w` even perturbed the live-name count this variation exists to reproduce):
251
251
  // • E and the cond pure; the cond's non-E operand must NOT mention v (a clamp's
252
252
  // `if (a < v)` would compare against the POST-assignment v — reproduced);
253
253
  // • the copy w carries E's RENDERED type, not v's declared one (retyping a u32
@@ -327,7 +327,7 @@ export function registerishSpellings(sfn: SFn): RegcopySpelling[] {
327
327
  }
328
328
  const base: SFn = { ...sfn, locals: [...locals], body: afterR2 };
329
329
 
330
- // R3 variants: the tail assign-back — a non-var return lands in a local first. WHICH local is
330
+ // R3 results: the tail assign-back — a non-var return lands in a local first. WHICH local is
331
331
  // itself allocator-ambiguous (gcc 2.9 wanted R1's dead value var — live-name-count sensitive;
332
332
  // another allocator may want the fresh one), so BOTH tails are emitted as candidates rather
333
333
  // than asserting one compiler's preference; the source dedupe collapses them when identical.