@asmlift/core 0.6.0 → 0.7.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 (74) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/contracts.ts +60 -11
  5. package/src/frontend/ssa.ts +1 -1
  6. package/src/frontend/thumb.ts +2 -2
  7. package/src/ir/alias.ts +24 -0
  8. package/src/ir/core.ts +8 -0
  9. package/src/ir/opcodes.ts +43 -7
  10. package/src/ir/simplify.ts +1 -1
  11. package/src/l3/address.ts +2 -2
  12. package/src/l3/advance.ts +373 -0
  13. package/src/l3/argbase.ts +4 -4
  14. package/src/l3/ast.ts +65 -21
  15. package/src/l3/basecse.ts +48 -28
  16. package/src/l3/coalesce.ts +9 -9
  17. package/src/l3/gates.ts +75 -1
  18. package/src/l3/hoist.ts +1 -1
  19. package/src/l3/homesplit.ts +13 -13
  20. package/src/l3/initfirst.ts +3 -3
  21. package/src/l3/inlinebase.ts +16 -16
  22. package/src/l3/mentions.ts +68 -5
  23. package/src/l3/mulfirst.ts +3 -3
  24. package/src/l3/nearbase.ts +4 -4
  25. package/src/l3/offmember.ts +5 -5
  26. package/src/l3/parkfirst.ts +6 -6
  27. package/src/l3/pollguard.ts +3 -3
  28. package/src/l3/ptrfield.ts +4 -4
  29. package/src/l3/regspell.ts +8 -8
  30. package/src/l3/reindex.ts +22 -17
  31. package/src/l3/scopebase.ts +28 -25
  32. package/src/l3/sinkinit.ts +7 -7
  33. package/src/l3/slotorder.ts +3 -3
  34. package/src/l3/storage.ts +1 -1
  35. package/src/l3/tailmerge.ts +2 -2
  36. package/src/l3/typing.ts +3 -3
  37. package/src/l3/unmerge.ts +483 -59
  38. package/src/l3/unreduce.ts +13 -13
  39. package/src/l3/volatileptr.ts +11 -11
  40. package/src/l3/volatileval.ts +11 -11
  41. package/src/l3/volstore.ts +16 -16
  42. package/src/l3/zerosub.ts +6 -6
  43. package/src/pattern/engine.ts +4 -4
  44. package/src/pipeline.ts +17 -5
  45. package/src/proto.ts +2 -2
  46. package/src/raise/const.ts +203 -3
  47. package/src/raise/divpow2.ts +2 -2
  48. package/src/raise/extscale.ts +342 -0
  49. package/src/raise/globalshape.ts +32 -12
  50. package/src/raise/gvn.ts +2 -2
  51. package/src/raise/magicdiv.ts +2 -2
  52. package/src/raise/memberarrays.ts +4 -4
  53. package/src/raise/narrowlocal.ts +18 -2
  54. package/src/raise/paramwidth.ts +24 -2
  55. package/src/raise/pre-recovery.ts +90 -25
  56. package/src/raise/retsink.ts +381 -15
  57. package/src/raise/shortcircuit.ts +595 -34
  58. package/src/raise/structs.ts +4 -4
  59. package/src/raise/tailsink.ts +126 -0
  60. package/src/rank-declare.ts +4 -4
  61. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  62. package/src/rank.ts +1148 -803
  63. package/src/structure/analysis.ts +87 -90
  64. package/src/structure/bitfields.ts +130 -30
  65. package/src/structure/globalaccess.ts +30 -4
  66. package/src/structure/namecoalesce.ts +32 -13
  67. package/src/structure/structure.ts +1415 -200
  68. package/src/structure/switch-recover.ts +100 -7
  69. package/src/symbols.ts +127 -6
  70. package/src/target.ts +155 -35
  71. package/src/trace.ts +1 -1
  72. package/src/variation-definitions.ts +1540 -0
  73. package/src/variation-gates.ts +89 -0
  74. package/src/variation-tokens.ts +355 -0
@@ -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.
package/src/l3/reindex.ts CHANGED
@@ -1,11 +1,11 @@
1
- // asmlift L3 — the walk→index RE-SPELLING, a differ-ranked representation lever.
1
+ // asmlift L3 — the walk→index RESPELL VARIATION, differ-ranked.
2
2
  //
3
3
  // A compiler strength-reduces a source-level `arr[i]` loop into a pointer WALK (`*p; p += 1`),
4
4
  // so asmlift's faithful lift of the machine form emits the walk — but recompiling the walk
5
5
  // rarely reproduces the bytes the INDEXED source produced (different induction variable,
6
6
  // different regalloc). Which representation the source used is genuinely ambiguous from asm —
7
7
  // exactly the class of ambiguity asmlift resolves by CANDIDATES, not guesses (rank.ts: "types
8
- // are differ-ranked levers"). This module produces the indexed re-spelling of a structured
8
+ // are differ-ranked variations"). This module produces the indexed re-spelling of a structured
9
9
  // function; enumerateCandidates emits BOTH and the objdiff score referees.
10
10
  //
11
11
  // v1 SCOPE (decline over approximate): a loop is re-spelled only when ALL hold —
@@ -29,7 +29,7 @@
29
29
  // it (its statements are the loop-preceding subset of the else arm, now unconditionally hoisted).
30
30
  // The walk pointers KEEP their init and lose their step — gcc folds a loop-invariant pointer
31
31
  // local into addressing, so `p = B; … p[i]` and `B[i]` compile identically, and keeping the
32
- // local is what lets the `/volatile` lever qualify a numeric B. Several walk pointers share the
32
+ // local is what lets the `/volatile` variation qualify a numeric B. Several walk pointers share the
33
33
  // one counter (`dotprod`'s a/b pair). Its OWN rules, on top of the shared table below —
34
34
  // • the guard tests THE SAME var the counter is initialised from, against 0, in the sense that
35
35
  // skips the loop; the do-while exit is exactly `k != 0`;
@@ -227,7 +227,7 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
227
227
  },
228
228
  {
229
229
  id: 'global-counter',
230
- why: 'a global counter’s final value is observable to every other caller, ISR and translation unit',
230
+ why: 'a global counter’s final value is observable to every other caller, interrupt handler and translation unit',
231
231
  sound: true,
232
232
  guardedBy: 'reindex.test.ts: the counter may not be a GLOBAL',
233
233
  rejects: (c) => !c.kIsDeclared,
@@ -255,14 +255,14 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
255
255
  },
256
256
  {
257
257
  id: 'body-exit',
258
- why: 'the steps sat in the body tail, which a `continue` skips and a `for`’s inc does not',
258
+ why: 'the steps sat at the end of the body, which a `continue` skips and a `for`’s increment does not',
259
259
  sound: true,
260
260
  guardedBy: 'reindex.test.ts: a `break` in the body declines',
261
261
  rejects: (c) => c.coreHasExit,
262
262
  },
263
263
  {
264
264
  id: 'walk-base',
265
- why: 'the kept init must be a value the rewrite can leave standing a var, or a rematerializable address',
265
+ why: 'the kept init must be a value the rewrite can leave standing: a variable, or an address that can be loaded again',
266
266
  sound: true,
267
267
  guardedBy: 'reindex.test.ts: a walk pointer with no init ahead of the loop declines',
268
268
  rejects: (c) => c.badBases.length > 0,
@@ -290,7 +290,7 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
290
290
  },
291
291
  {
292
292
  id: 'leftover-walk',
293
- why: 'a leftover outlives the deleted step, and its skip-arm twin reads a pointer that path never set',
293
+ why: 'a statement left outside the deleted step still reads the walk pointer, which a path that skipped the step never set',
294
294
  sound: true,
295
295
  guardedBy: 'reindex.test.ts: a leftover mentioning a walk pointer declines',
296
296
  rejects: (c) => c.leakyLeftovers > 0,
@@ -367,13 +367,18 @@ function reindexExpr(e: Expr, walk: WalkLoop, iv: string): Expr | null {
367
367
  ? { k: 'var', name: iv }
368
368
  : { k: 'bin', op: '+', l: { k: 'var', name: iv }, r: e.idx };
369
369
  // NOTE: this rebuilds the node from parts, so any field not named here is DROPPED — and the
370
- // `index` node has exactly three optional ones (ast.ts): `lead`, `operandOff`, `baseOrdered`.
371
- // `lead` is declined above (the deref side); it cannot arrive on the base side either, since
372
- // `walk.base` is a local pointer and structuring only ever puts `lead` on an array GLOBAL's
373
- // own name. Dropping the other two is right rather than merely harmless: `baseOrdered` is
374
- // stamped per SYMBOL on an order-licensed GLOBAL, which a local var base never is, and
375
- // `operandOff`'s readers (l3/basecse.ts, l3/offmember.ts) ask about a CONSTANT subscript,
376
- // which the rewritten `i`/`i + k` index never is.
370
+ // `index` node has exactly five optional ones (ast.ts): `lead`, `baseElem`, `operandOff`,
371
+ // `baseOrdered`, `baseAdvanced`. `lead` is declined above (the deref side); it cannot arrive on
372
+ // the base side either, since `walk.base` is a local pointer and structuring only ever puts
373
+ // `lead` on an array GLOBAL's own name. Dropping the other four is right rather than merely
374
+ // harmless: `baseElem` states the element type of a base the C type walk cannot type, and the
375
+ // rebuilt base is `{k:'var', name: walk.base}` — a LOCAL pointer, whose declared type the walk
376
+ // reads straight out of the print env, so a statement about the base it replaced would describe
377
+ // a different base and the walk answers without one; `baseOrdered` is stamped per SYMBOL on an
378
+ // order-licensed GLOBAL, which a local var base never is; `operandOff`'s readers
379
+ // (l3/basecse.ts, l3/offmember.ts) ask about a CONSTANT subscript, which the rewritten
380
+ // `i`/`i + k` index never is; and `baseAdvanced` describes an address the machine reached from
381
+ // a CONSTANT one, which a walk pointer's is not.
377
382
  return { k: 'index', base: { k: 'var', name: walk.base }, idx, width: e.width, signed: e.signed };
378
383
  }
379
384
  let failed = false;
@@ -653,8 +658,8 @@ function tryExprWalk(
653
658
  * one loop re-spelled, or null (no candidate) when nothing fired — callers emit the extra
654
659
  * candidate only on non-null. Pure: never mutates the input SFn. `keptWalks` collects the names
655
660
  * of the pointers each fired loop kept as its base — v1 the walk's base (a param lands in the
656
- * set too, inertly: the volatile lever marks only declared locals), v2 the walk pointers
657
- * themselves — the locals the /indexed/volatile product (rank.ts) narrows the volatile lever
661
+ * set too, inertly: the volatile variation marks only declared locals), v2 the walk pointers
662
+ * themselves — the locals the /indexed/volatile composition (rank.ts) narrows the volatile variation
658
663
  * to. A v3 loop contributes nothing: it DELETES its pointer, and its base is qualified through
659
664
  * the /livebase pairings instead. `gates` is the shared countdown admission table — a parameter
660
665
  * so a test can ablate one entry and re-run the real pass. */
@@ -666,7 +671,7 @@ export function reindexWalks(
666
671
  const ptrVars = new Map<string, IrType>();
667
672
  const declTypes = declaredTypes(sfn);
668
673
  // BOTH volatility facts (ast.ts SFn.locals): the object-volatile counter, and the pointer whose
669
- // POINTEE is volatile — which is the one the `/volatile` lever mints, and the one a walk carries.
674
+ // POINTEE is volatile — which is the one the `/volatile` variation mints, and the one a walk carries.
670
675
  const volatileLocals = new Set(
671
676
  sfn.locals.filter((l) => l.volatile === true || l.pointeeVolatile === true).map((l) => l.name),
672
677
  );