@asmlift/core 0.5.0 → 0.6.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 (86) hide show
  1. package/README.md +22 -16
  2. package/package.json +1 -1
  3. package/src/backend/c.ts +1 -0
  4. package/src/backend/cfamily.ts +238 -167
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +194 -39
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +11 -0
  10. package/src/frontend/ppc.ts +43 -7
  11. package/src/frontend/ssa.ts +404 -29
  12. package/src/frontend/thumb.ts +2176 -686
  13. package/src/ir/alias.ts +54 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +337 -2
  16. package/src/ir/opcodes.ts +140 -21
  17. package/src/ir/parse.ts +19 -2
  18. package/src/ir/print.ts +27 -2
  19. package/src/ir/simplify.ts +190 -3
  20. package/src/ir/struct-names.ts +42 -0
  21. package/src/ir/verify.ts +43 -49
  22. package/src/l3/address.ts +62 -0
  23. package/src/l3/argbase.ts +2 -1
  24. package/src/l3/ast.ts +464 -57
  25. package/src/l3/basecse.ts +664 -76
  26. package/src/l3/coalesce.ts +429 -43
  27. package/src/l3/dce.ts +31 -9
  28. package/src/l3/gates.ts +21 -0
  29. package/src/l3/hoist.ts +293 -14
  30. package/src/l3/homesplit.ts +285 -0
  31. package/src/l3/initfirst.ts +301 -0
  32. package/src/l3/inlinebase.ts +193 -0
  33. package/src/l3/mentions.ts +113 -0
  34. package/src/l3/mulfirst.ts +42 -0
  35. package/src/l3/nearbase.ts +152 -0
  36. package/src/l3/offmember.ts +371 -0
  37. package/src/l3/parkfirst.ts +96 -0
  38. package/src/l3/pollguard.ts +154 -0
  39. package/src/l3/ptrfield.ts +227 -0
  40. package/src/l3/regspell.ts +110 -85
  41. package/src/l3/reindex.ts +715 -78
  42. package/src/l3/scopebase.ts +644 -218
  43. package/src/l3/sinkinit.ts +40 -0
  44. package/src/l3/slotorder.ts +123 -0
  45. package/src/l3/storage.ts +48 -0
  46. package/src/l3/symbol-refs.ts +41 -8
  47. package/src/l3/tailmerge.ts +15 -0
  48. package/src/l3/typing.ts +198 -9
  49. package/src/l3/unmerge.ts +263 -0
  50. package/src/l3/unreduce.ts +971 -0
  51. package/src/l3/volatileptr.ts +207 -0
  52. package/src/l3/volatileval.ts +130 -0
  53. package/src/l3/volstore.ts +229 -0
  54. package/src/l3/zerosub.ts +62 -0
  55. package/src/pattern/engine.ts +236 -13
  56. package/src/pipeline.ts +157 -56
  57. package/src/proto.ts +112 -14
  58. package/src/raise/arrays.ts +6 -1
  59. package/src/raise/divpow2.ts +2 -2
  60. package/src/raise/globalshape.ts +1038 -0
  61. package/src/raise/gvn.ts +33 -18
  62. package/src/raise/latch.ts +126 -0
  63. package/src/raise/memberarrays.ts +594 -0
  64. package/src/raise/narrow.ts +124 -0
  65. package/src/raise/narrowlocal.ts +556 -0
  66. package/src/raise/paramwidth.ts +179 -0
  67. package/src/raise/pre-recovery.ts +97 -14
  68. package/src/raise/recover.ts +56 -23
  69. package/src/raise/retsink.ts +210 -10
  70. package/src/raise/shortcircuit.ts +474 -74
  71. package/src/raise/struct-arrays.ts +19 -2
  72. package/src/raise/structs.ts +33 -3
  73. package/src/rank-axes.ts +630 -0
  74. package/src/rank-declare.ts +256 -0
  75. package/src/rank.ts +1723 -272
  76. package/src/structure/analysis.ts +1392 -141
  77. package/src/structure/bitfields.ts +332 -0
  78. package/src/structure/globalaccess.ts +274 -0
  79. package/src/structure/hazards.ts +411 -20
  80. package/src/structure/loops.ts +2 -49
  81. package/src/structure/namecoalesce.ts +435 -0
  82. package/src/structure/structure.ts +2678 -526
  83. package/src/structure/switch-recover.ts +616 -144
  84. package/src/symbols.ts +62 -1
  85. package/src/target.ts +367 -24
  86. package/src/trace.ts +111 -32
@@ -0,0 +1,152 @@
1
+ // L3 re-spelling lever: NEIGHBOR absolute addresses derive from one shared base local.
2
+ //
3
+ // A cluster of raw-address accesses a few bytes apart is one object's cells: the compiler holds
4
+ // the object's base in a register and derives each cell (`add #72` / `add #74` off one pool
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
7
+ // re-spells every deref base in a cluster as an offset from a `u8 *` base local holding the
8
+ // cluster's lowest address, and the differ referees:
9
+ //
10
+ // *(u16 *)0x0300104A → u8 *b = (u8 *)0x03001048; *(u16 *)(b + 2)
11
+ //
12
+ // SCOPE (decline over approximate): cluster MEMBERSHIP comes from CONST deref bases only (a
13
+ // struct-pointer cast base and everything inside a dot-form field subtree keep their spelling —
14
+ // their stride is the struct's, not a byte's); a cluster needs at least two DISTINCT addresses
15
+ // within the target's declared derivation reach of its lowest (TargetDescription nearBaseSpan —
16
+ // beyond it the derive costs more than the pool word it saves); every access the walk visits
17
+ // rewrites, so a cluster splits only across the field-subtree and struct-pointer-cast
18
+ // boundaries. A member basecse
19
+ // already hoisted arrives as a `var` base and is invisible here — the reused-base and
20
+ // neighbor-base spellings stay separate candidates. Once a cluster HAS formed, a bare const
21
+ // VALUE inside its window re-spells too, as `(s32)(b + off)` — the address of a cell handed to
22
+ // something (a DMA source register) is the same derived add in the original, and the two
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
25
+ // assumes addresses below 2^31, true of every target that declares nearBaseSpan today). Declines
26
+ // (null) when no cluster forms.
27
+ import { baseConst } from './address';
28
+ import type { Expr, SFn } from './ast';
29
+ import { mapExprChildren, mapStmtExprs } from './ast';
30
+ import { type BaseInit, nameAllocator, placeBaseLocals } from './hoist';
31
+
32
+ /** `span` is the target's single-add-immediate derivation reach
33
+ * (TargetDescription.compilerBehaviors.nearBaseSpan) — a target that declares none never runs
34
+ * this lever. */
35
+ export function nearBaseClusters(sfn: SFn, span: number): SFn | null {
36
+ if (!Number.isFinite(span) || span < 0) {
37
+ return null; // a hostile span stalls the cluster window instead of shrinking it
38
+ }
39
+ // collect every DISTINCT const deref-base address
40
+ const addrs = new Set<number>();
41
+ const collect = (e: Expr): Expr => {
42
+ if (e.k === 'field') {
43
+ return e; // a dot-form subtree keeps its struct base — never collected, never rewritten
44
+ }
45
+ if (e.k === 'cast' && e.to.kind === 'ptr' && e.to.to.kind === 'struct') {
46
+ return e; // rewrite refuses these subtrees, so collecting under them would seed a cluster
47
+ }
48
+ const m = mapExprChildren(e, collect);
49
+ if (m.k === 'index') {
50
+ const c = baseConst(m.base);
51
+ if (c !== null) {
52
+ addrs.add(c);
53
+ }
54
+ }
55
+ return m;
56
+ };
57
+ for (const s of sfn.body) {
58
+ mapStmtExprs(s, collect);
59
+ }
60
+ // greedy clusters over the sorted addresses; only multi-member clusters rewrite
61
+ const sorted = [...addrs].sort((a, b) => a - b);
62
+ const baseOf = new Map<number, number>();
63
+ for (let i = 0; i < sorted.length;) {
64
+ const lo = sorted[i];
65
+ let j = i;
66
+ while (j < sorted.length && sorted[j] - lo <= span) {
67
+ j++;
68
+ }
69
+ if (j - i >= 2) {
70
+ for (let k = i; k < j; k++) {
71
+ baseOf.set(sorted[k], lo);
72
+ }
73
+ }
74
+ i = j;
75
+ }
76
+ if (baseOf.size === 0) {
77
+ return null;
78
+ }
79
+ const baseName = new Map<number, string>();
80
+ const fresh = nameAllocator(sfn); // the shared minting mechanism — collides with nothing in sfn
81
+ for (const lo of new Set(baseOf.values())) {
82
+ baseName.set(lo, fresh());
83
+ }
84
+ const derived = (lo: number, off: number): Expr =>
85
+ off === 0
86
+ ? { k: 'var', name: baseName.get(lo)! }
87
+ : { k: 'bin', op: '+', l: { k: 'var', name: baseName.get(lo)! }, r: { k: 'const', value: off } };
88
+ // the cluster (if any) whose window covers a bare const value
89
+ const windows = [...new Set(baseOf.values())];
90
+ const coveringLo = (v: number): number | undefined => windows.find((lo) => v >= lo && v - lo <= span);
91
+ const rewrite = (e: Expr): Expr => {
92
+ if (e.k === 'field') {
93
+ return e;
94
+ }
95
+ if (e.k === 'index') {
96
+ const c = baseConst(e.base);
97
+ const lo = c !== null ? baseOf.get(c) : undefined;
98
+ if (c !== null && lo !== undefined) {
99
+ // the base is replaced wholesale — its inner const must not reach the value path below
100
+ return { ...e, base: derived(lo, c - lo), idx: rewrite(e.idx) };
101
+ }
102
+ return { ...e, base: rewrite(e.base), idx: rewrite(e.idx) };
103
+ }
104
+ if (e.k === 'cast' && e.to.kind === 'ptr' && e.to.to.kind === 'struct') {
105
+ return e; // the struct-arrays base keeps its spelling — same refusal as baseConst's
106
+ }
107
+ if (e.k === 'const') {
108
+ const lo = coveringLo(e.value);
109
+ if (lo !== undefined) {
110
+ return {
111
+ k: 'cast',
112
+ to: { kind: 'int', width: 32, signed: true },
113
+ e: derived(lo, e.value - lo),
114
+ };
115
+ }
116
+ return e;
117
+ }
118
+ return mapExprChildren(e, rewrite);
119
+ };
120
+ const inits: BaseInit[] = [...baseName.entries()].map(([lo, name]) => ({
121
+ k: 'assign',
122
+ name,
123
+ value: {
124
+ k: 'cast',
125
+ to: { kind: 'ptr', to: { kind: 'int', width: 8, signed: false } },
126
+ e: { k: 'const', value: lo },
127
+ },
128
+ }));
129
+ const locals = [
130
+ ...sfn.locals,
131
+ ...[...baseName.values()].map((name) => ({
132
+ name,
133
+ type: { kind: 'ptr', to: { kind: 'int', width: 8, signed: false } } as SFn['locals'][number]['type'],
134
+ })),
135
+ ];
136
+ // The body rebuild is `l3/hoist.ts`'s, shared with the two other passes that place into the
137
+ // leading base-init run. The ORDERING is not: `prepend` returns before the first-use query, so
138
+ // this pass takes the rebuild and abstains from the policy (see `BaseInitPlacement`).
139
+ //
140
+ // The DEFAULT is `prepend` — the cluster bases go ABOVE a run already there rather than being
141
+ // merged into it in first-use order — and it rests on a row, not on a compiler fact. Placing
142
+ // them in first-use order instead turns `synthetic:dmafield` (won by
143
+ // `signed/livebase/volatile/nearbase/initfirst`) from a MATCH into diff:5, measured 2026-08-26.
144
+ // The reading that goes with it — a cluster base is reached at 2+ addresses by construction, so
145
+ // its pool word is not "first touched late" — explains why first-use order is not obviously
146
+ // right, not why prepending is; which order the source wrote is per-function knowledge the asm
147
+ // does not carry. So it is a DEFAULT and not a decision: `rank.ts` offers the sunk ordering
148
+ // beside it as `/nearbase/sinkinit`, and the differ settles which one a function wanted.
149
+ const rewritten = sfn.body.map((s) => mapStmtExprs(s, rewrite));
150
+ const { body } = placeBaseLocals({ ...sfn, locals, body: rewritten }, inits, 'prepend');
151
+ return { ...sfn, locals, body };
152
+ }
@@ -0,0 +1,371 @@
1
+ // L3 re-spelling: spell a leaf base's constant subscript as a struct MEMBER, so the offset stays
2
+ // in the instruction's displacement instead of folding into the address the compiler materializes.
3
+ //
4
+ // THE COMPILER FACT this exists for is the one `l3/basecse.ts`'s header states in the imperative
5
+ // and cannot act on: on a target that declares `compilerBehaviors.foldsConstAddrOffset`, a
6
+ // constant SUBSCRIPT folds into the literal the address materializes, while an aggregate MEMBER
7
+ // offset stays in the memory operand.
8
+ //
9
+ // ((u16 *)0x3003468)[7] → .word 0x3003476 + ldrh r0, [r0]
10
+ // ((struct S *)0x3003468)->m14 → .word 0x3003468 + ldrh r0, [r0, #0xe]
11
+ //
12
+ // Both denote the same cell. Which one the source wrote is not derivable from the C, but the ASM
13
+ // says which one the compiler was given: an offset that reached the load's own displacement got
14
+ // there because nothing folded it, and only a member (or a named base — that is `/basefold`'s
15
+ // spelling) leaves it there. `l3/ast.ts`'s `index.operandOff` carries that displacement down from
16
+ // the lift, because the fold at L3 (`idx = idxVal + off / width`, structure/structure.ts) makes
17
+ // the two indistinguishable afterwards.
18
+ //
19
+ // THE ROSTER ALREADY OFFERS THE OTHER SOURCE OF THE SAME SHAPE. `/basefold` reads the identical
20
+ // evidence and answers it with a NAMED BASE (`u16 *p = (u16 *)C; p[7]`), which also keeps the
21
+ // displacement. The member is the second source and was in no fan; the two are different C and
22
+ // different register pressure, so both ride and the differ referees.
23
+ //
24
+ // SCOPE — a LEAF base only (a bare `addr` or a bare `const`), which is `l3/basecse.ts`'s
25
+ // `isHoistableBase` population. A computed base (`((u16 *)v0)[8]`) is out: the address is already
26
+ // held somewhere, so nothing folded into a literal and the evidence says nothing — that shape is
27
+ // `/addr-home`'s (structure/analysis.ts). It is a GATE rather than a collection filter so the
28
+ // refusal is attributable: `firstRejection` names it.
29
+ //
30
+ // NO DEVICE-REGISTER REFUSAL, which is a priced decision and not an omission. A base inside the
31
+ // target's declared `deviceRegisters` window is admitted like any other, so this pass will offer
32
+ // `((struct Off0 *)&REG_DMA3SAD)->m8` — a struct declared over the register file, carrying no
33
+ // qualifier. Three measurements say a refusal there buys nothing for that price.
34
+ //
35
+ // REACH. Every device-window base reaching this table is already refused by `no-operand-off`
36
+ // (49 of them map-less, 2 map-ful, over all four checkouts — 1417 structured functions map-less,
37
+ // 398 map-ful): the DMA idiom writes offset 0 as well as offset 8, and offset 0 leaves no
38
+ // displacement. A window refusal removes ZERO further bases in either configuration.
39
+ //
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
42
+ // the tree this pass is handed is unqualified in both configurations. Nor is a tie-break lost —
43
+ // `deviceVolatileClaims` counts only qualifiers a tree already asserts, so an unqualified tree
44
+ // scores zero whichever way it is spelled.
45
+ //
46
+ // CONFIGURATION. A numeric window is read through `addrConst`, and a symbol map turns a device
47
+ // address into a named `addr` no range can see. So such a refusal fires map-less and not
48
+ // map-ful on the SAME function — and the real tier runs map-ful. A refusal a symbol map
49
+ // switches off is not a refusal.
50
+ //
51
+ // What referees it instead is the differ, and `offmember.test.ts` pins the price on real compiled
52
+ // asm: on a DMACNT spin that never touches DMASAD — the one shape `no-operand-off` does not cover
53
+ // — the member spelling is offered and LOSES, 52 candidates, best
54
+ // `unsigned/derived-home/livebase/volatile: 4` against `/offmember`'s 5. A row where it WINS over
55
+ // MMIO is what would earn a gate here, together with a window reading that survives a symbol map.
56
+ //
57
+ // NOT AN EXTENSION OF raise/struct-arrays.ts. That pass mints an element struct off the `mul`/`shl`
58
+ // STRIDE idiom in the machine code, and this shape has no scale at all — the subscript is a
59
+ // constant and the base is a leaf, so there is no stride to read. It is a SPELLING of a tree
60
+ // structuring already produced, which is what puts it in `l3/` beside the other re-spellings.
61
+ //
62
+ // SOUNDNESS. The member offset is `idx * width`, the access's own byte offset from the base — never
63
+ // `operandOff`, which is EVIDENCE about where the offset travelled and is only part of it when the
64
+ // address carried the rest. So the respelled node addresses the same cell by construction, and the
65
+ // one way it could not is a layout C cannot reproduce — a member C would seat somewhere other than
66
+ // where the asm read it. `unspellable-layout` refuses that, and it is the table's only sound gate.
67
+ //
68
+ // REFUSAL. `spellOperandMembers` returns `null` when no base was admitted, so a function with no
69
+ // eligible site contributes no candidate and costs nothing.
70
+ //
71
+ // WHAT THE DECLARATION GOVERNS, and it is narrower than it looks: the CONSTANT SUBSCRIPTS this
72
+ // pass grouped under one base expression, and nothing else. A sibling
73
+ // access through the same address that this pass has no member spelling for — a variable
74
+ // subscript, a `lead`-prefixed one — is left exactly as it was, keeping its own cast, and a
75
+ // struct-array element off the same numeric constant reaches the address through a different base
76
+ // expression and so a different key entirely. So one address really can leave here spelled two
77
+ // ways, and `offmember.test.ts` emits exactly this pair rather than describing it:
78
+ // `((struct Off0 *)50345232)->m16` beside `((s32 *)50345232)[a0]`.
79
+ //
80
+ // THAT IS UGLY AND IT IS NOT UNSOUND, and only the second half is a gate's business. Every
81
+ // access carries its own cast, so no access reads a byte it did not read before; the seating check
82
+ // below judges the accesses the declaration is built FROM, which are exactly the accesses
83
+ // respelled through it, so its argument is over a total population of what it governs. The
84
+ // contradiction is between two FICTIONAL TYPES over one address, which is a readability claim —
85
+ // `quality`'s clientele, not a gate's.
86
+ //
87
+ // AND THE TIGHTER RULE IS PRICED, so it is not re-derived from scratch. Refusing a base whose
88
+ // siblings this pass cannot spell removes `a:gCallbackQueue` from
89
+ // `kleod:ProcessInputAndUpdateEntities` (asserted by dumping that row's winning source both ways:
90
+ // 9 `struct Off*` declarations become 2, and the survivor is `gUnk_03004C20`) and COSTS that row
91
+ // score, while protecting no row anywhere in the artifact. NO BRACKET IS QUOTED, and that is a
92
+ // discipline rather than a gap: a bracket is a claim about the whole tree, so BOTH endpoints move
93
+ // whenever anything else does. Read the unablated endpoint off the committed artifact — where
94
+ // `kleod:ProcessInputAndUpdateEntities` scores 211 — and re-measure the ablated one before writing
95
+ // a pair anywhere.
96
+ // "PROTECTS NO ROW" IS A CENSUS AND NOT A SAMPLE, and the population is small because this is an
97
+ // extra GATE: it only ever removes candidates, so only a row whose winner carries `/offmember` can
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
100
+ // it grows with the corpus, so re-run it before repeating the result. It held eight rows besides
101
+ // `ProcessInputAndUpdateEntities` when the census ran, all unmoved with the rule on:
102
+ // `synthetic:basecell`, `synthetic:bgfixed`, `synthetic:foldsink`, `sa3:sub_802DFC8` and
103
+ // `sa3:sub_803213C` all still MATCH, `kleod:RollRandomLevelVariant` 18,
104
+ // `kleod:CountCollectedGems` 290, `kleod:UpdateWorldMapNodeAnim` 157, each the artifact's own
105
+ // number. A gate needs a row it protects. (One side effect worth recording: with the rule on, PI's
106
+ // winner becomes `unsigned/setup-args/no-ptr-elem/offmember` — an arm that wins no REAL row wins
107
+ // there. It is an ablation's artifact, not a reason to ship either.)
108
+ //
109
+ // ALL-OR-NOTHING PER FUNCTION, and that is a PRICE rather than a property. Every admitted base is
110
+ // respelled together in one candidate, so a function with two admitted bases where the target
111
+ // folded one and kept the other in the operand has no reachable spelling — the coverage hole
112
+ // `l3/ptrfield.ts` measures for its fields. `l3/basecse.ts` is NOT a second witness to it but the
113
+ // counterexample: its `UNFOLDED_GATES` cuts across `LIVEBASE_BLOCK_GATES` rather than refining it,
114
+ // so a partial answer IS reachable there — a proper nonempty subset on 13 of the agbcc rows the
115
+ // artifact carried when that census ran,
116
+ // one of three bases on `synthetic:dmascope` — and that is the way out of this hole too: not a
117
+ // per-base fork, but a SECOND PREDICATE separating the bases the differ has to choose between.
118
+ // The per-base fork is 2^n and the family's standing price for forking ten refusal sites per site
119
+ // was 1024x, so the arbitrary subsets stay unreachable until a row demands one.
120
+ // Measured over klonoa's 69
121
+ // lifting functions (lift → idioms → raise → structure, one pass per configuration): 36 admitted
122
+ // bases over 25 functions map-less and 38 over 24 map-ful, so the multi-base functions really are
123
+ // the majority of the surplus and a missing subset has somewhere to live. The other three
124
+ // checkouts admit 32 bases over 32 functions (af), 10 over 10 (marioparty3) and 2 over 2
125
+ // (snowboardkids2), all map-less.
126
+ import { nextStructIndex } from '../ir/struct-names';
127
+ import { type IrType, T, scalarTypeForAccess } from '../ir/types';
128
+ import type { Expr, SFn, StructType } from './ast';
129
+ import { mapExprChildren, mapStmtExprs, walkExprs } from './ast';
130
+ import { type Gate, firstRejection } from './gates';
131
+
132
+ /** One observed constant-subscript access through a base. */
133
+ interface Site {
134
+ /** the member's byte offset from the base — `idx * width`, the address the node denotes */
135
+ off: number;
136
+ width: number;
137
+ signed: boolean;
138
+ /** the displacement the instruction carried, when it carried one (l3/ast.ts `operandOff`) */
139
+ operandOff?: number;
140
+ }
141
+
142
+ /** The identity of an access's base, for grouping. Leaf bases key by value; everything else keys
143
+ * by its printed shape, so a non-leaf base still reaches the table and is refused by NAME. */
144
+ const baseKey = (e: Expr): string =>
145
+ e.k === 'addr' ? `a:${e.name}` : e.k === 'const' ? `c:${e.value}` : `x:${JSON.stringify(e)}`;
146
+
147
+ /** What the gates judge: one base and every constant-subscript access through it. */
148
+ export interface OffmemberBase {
149
+ key: string;
150
+ /** the base is a bare `addr`/`const` — the population whose address the compiler materializes */
151
+ leafBase: boolean;
152
+ /** some access through this base carries no memory-operand displacement, so nothing says its
153
+ * offset was ever anywhere but the address */
154
+ missingOperandOff: boolean;
155
+ /** some access's subscript is WIDER than the displacement the instruction carried — the address
156
+ * expression held the rest, and how that split maps back onto one member is not measured */
157
+ indexCarriesMore: boolean;
158
+ /** the accesses cannot be declared as a plain C struct seating each at its own offset */
159
+ unspellableLayout: boolean;
160
+ }
161
+
162
+ export const OFFMEMBER_GATES: readonly Gate<OffmemberBase>[] = [
163
+ {
164
+ id: 'non-leaf-base',
165
+ why: 'a computed base is already held somewhere, so nothing folded into a literal',
166
+ // A REACH argument, which is why it claims no soundness and owes no guard — and it is the
167
+ // only thing standing between this pass and a dropped qualifier, which the header's
168
+ // no-device-refusal paragraph is the other half of. A `/volatile` base reaches L3 as a CAST
169
+ // over the constant, so this gate excludes it as a side effect of excluding every computed
170
+ // base, and the natural widening ("a cast of a leaf const is still a leaf") would respell a
171
+ // qualified access as an unqualified member with no test failing. Nothing downstream would
172
+ // object. So the widening is forbidden HERE, at this gate, and a round that wants it owes a
173
+ // qualifier-preserving spelling first.
174
+ sound: false,
175
+ rejects: (c) => !c.leafBase,
176
+ },
177
+ {
178
+ id: 'no-operand-off',
179
+ why: 'an offset that never reached the instruction is not evidence of a member',
180
+ sound: false,
181
+ rejects: (c) => c.missingOperandOff,
182
+ },
183
+ {
184
+ id: 'index-carries-more',
185
+ why: 'the address carried part of the offset, and that decomposition is unmeasured',
186
+ sound: false,
187
+ rejects: (c) => c.indexCarriesMore,
188
+ },
189
+ {
190
+ id: 'unspellable-layout',
191
+ why: 'a struct C cannot seat at the observed offsets would address different bytes',
192
+ sound: true,
193
+ guardedBy: 'offmember.test.ts: a base whose accesses no plain struct can seat is refused',
194
+ rejects: (c) => c.unspellableLayout,
195
+ },
196
+ ];
197
+
198
+ /** The members this base is spelled through: one field per distinct offset. Shared by the seating
199
+ * PREDICATE and the layout BUILDER so the two cannot judge one field set and declare another —
200
+ * and, since `seatable` now refuses an offset whose views disagree on width OR signedness, every
201
+ * surviving offset has exactly one view and the pick below is an identity on an admitted base.
202
+ * It is kept because this runs on ABLATED tables too, where the gate is gone and something still
203
+ * has to choose; preferring the signed view keeps that choice the widest one. */
204
+ function membersOf(sites: readonly Site[]): Site[] {
205
+ const byOff = new Map<number, Site>();
206
+ for (const s of sites) {
207
+ const prev = byOff.get(s.off);
208
+ if (!prev || (s.signed && !prev.signed)) {
209
+ byOff.set(s.off, s);
210
+ }
211
+ }
212
+ return [...byOff.values()].sort((a, b) => a.off - b.off);
213
+ }
214
+
215
+ /** Can plain C seat every member at the offset the asm read it at, AND does the member read the
216
+ * same value the access did?
217
+ *
218
+ * FOUR ways it cannot, and the first three are the same defect — a field the declaration would
219
+ * place somewhere other than where the access reads: a NEGATIVE offset, which no member has; two
220
+ * views of ONE offset at different widths (a union); and two offsets whose byte ranges collide.
221
+ *
222
+ * THE FOURTH IS NOT ABOUT PLACEMENT, and it is the easy one to leave out: two views of one
223
+ * offset at one width but
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
226
+ * signedness at widths 1 and 2, so an unsigned read becomes sign-extending. That is a value
227
+ * change rather than a spelling change, and the differ can referee it only by luck: a masked or
228
+ * compared result compiles to the same bytes while the published C says something the asm does
229
+ * not. C spells it as a union, which is not a member, so it is refused exactly as the width union
230
+ * is. `l3/basecse.ts` keys `(base, width, signedness)` for the same reason, and `l3/typing.ts`
231
+ * states the rule in the imperative: signedness counts wherever the access extends.
232
+ *
233
+ * Natural ALIGNMENT is not among them, and that is an invariant rather than an omission: a
234
+ * member's offset here is the node's own `idx * width`, so it is a multiple of its width by
235
+ * construction and C's own alignment can always place it. A clause for it would have no
236
+ * inhabitant.
237
+ *
238
+ * This is what `unspellable-layout` asks, and the gate is the ONLY refusal — `layoutFor` below
239
+ * builds whatever it is handed, so ablating the gate really does emit the mislaid struct rather
240
+ * than quietly declining beside it. */
241
+ function seatable(sites: readonly Site[]): boolean {
242
+ let cursor = 0;
243
+ const views = new Map<number, Site>();
244
+ for (const s of sites) {
245
+ const v = views.get(s.off);
246
+ if (v !== undefined && (v.width !== s.width || v.signed !== s.signed)) {
247
+ return false;
248
+ }
249
+ views.set(s.off, s);
250
+ }
251
+ for (const m of membersOf(sites)) {
252
+ if (m.off < cursor) {
253
+ return false;
254
+ }
255
+ cursor = m.off + m.width;
256
+ }
257
+ return true;
258
+ }
259
+
260
+ /** The struct declaration for a base: each member at its own offset, gaps filled with an explicit
261
+ * `u8[N]` pad so the declaration reproduces the observed offsets on its own — the discipline
262
+ * raise/structs.ts and raise/struct-arrays.ts share. No trailing pad and no `size`: the spelling
263
+ * is `->m`, so `sizeof` is never taken. */
264
+ function layoutFor(name: string, sites: readonly Site[]): StructType {
265
+ const fields: StructType['fields'] = [];
266
+ let cursor = 0;
267
+ let pad = 0;
268
+ for (const m of membersOf(sites)) {
269
+ if (cursor < m.off) {
270
+ fields.push({ off: cursor, type: T.array(T.u(8), m.off - cursor), name: `_pad${pad++}` });
271
+ }
272
+ fields.push({ off: m.off, type: scalarTypeForAccess(m.width, m.signed), name: `m${m.off}` });
273
+ cursor = m.off + m.width;
274
+ }
275
+ return { name, fields };
276
+ }
277
+
278
+ /** Every CONSTANT-SUBSCRIPT `index` access, grouped by base, in first-appearance order — the
279
+ * accesses this pass has a member spelling for, which are exactly the ones the declaration it
280
+ * builds will govern. A sibling with a variable subscript or a `lead` is not one of them and is
281
+ * not counted: it keeps its own cast and this pass never rewrites it (see the header's
282
+ * "WHAT THE DECLARATION GOVERNS"). */
283
+ function collect(sfn: SFn): { order: string[]; sites: Map<string, Site[]> } {
284
+ const order: string[] = [];
285
+ const sites = new Map<string, Site[]>();
286
+ for (const e of walkExprs(sfn.body)) {
287
+ if (e.k !== 'index' || e.idx.k !== 'const' || e.lead !== undefined) {
288
+ continue;
289
+ }
290
+ const k = baseKey(e.base);
291
+ let list = sites.get(k);
292
+ if (!list) {
293
+ order.push(k);
294
+ list = [];
295
+ sites.set(k, list);
296
+ }
297
+ list.push({ off: e.idx.value * e.width, width: e.width, signed: e.signed, operandOff: e.operandOff });
298
+ }
299
+ return { order, sites };
300
+ }
301
+
302
+ /** The keys `gates` admits, with the struct each one is spelled through. */
303
+ function admit(
304
+ sfn: SFn,
305
+ gates: readonly Gate<OffmemberBase>[],
306
+ firstName: number,
307
+ ): Map<string, { struct: StructType; type: IrType }> {
308
+ const { order, sites } = collect(sfn);
309
+ const out = new Map<string, { struct: StructType; type: IrType }>();
310
+ let n = firstName;
311
+ for (const key of order) {
312
+ const list = sites.get(key)!;
313
+ const rejected = firstRejection(gates, {
314
+ key,
315
+ leafBase: key.startsWith('a:') || key.startsWith('c:'),
316
+ missingOperandOff: list.some((s) => s.operandOff === undefined),
317
+ indexCarriesMore: list.some((s) => s.operandOff !== undefined && s.operandOff !== s.off),
318
+ unspellableLayout: !seatable(list),
319
+ });
320
+ if (rejected === null) {
321
+ const struct = layoutFor(`Off${n}`, list);
322
+ out.set(key, { struct, type: T.ptr(T.struct(struct.name, struct.fields)) });
323
+ n++;
324
+ }
325
+ }
326
+ return out;
327
+ }
328
+
329
+ /** The gate table an ablation swaps out. Optional, so a caller gets the shipped table by default.
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. */
332
+ export interface OffmemberOpts {
333
+ readonly gates?: readonly Gate<OffmemberBase>[];
334
+ }
335
+
336
+ /** The census without the rewrite, for a caller comparing what two tables would admit. */
337
+ export function offmemberBases(sfn: SFn, opts: OffmemberOpts = {}): readonly string[] {
338
+ return [...admit(sfn, opts.gates ?? OFFMEMBER_GATES, 0).keys()];
339
+ }
340
+
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. */
343
+ export function spellOperandMembers(sfn: SFn, opts: OffmemberOpts = {}): SFn | null {
344
+ // Past EVERY `Off<N>` the tree already carries, never the first free one (`ir/struct-names.ts`,
345
+ // shared with the other two minters, which also records that this scan returns 0 on every
346
+ // corpus function). The prefix is what keeps this pass clear of raise/structs.ts's `Struct<N>`
347
+ // and raise/struct-arrays.ts's `Elem<N>`; the monotone scan is what keeps it clear of itself.
348
+ const seed = nextStructIndex(
349
+ (sfn.structs ?? []).map((s) => s.name),
350
+ 'Off',
351
+ );
352
+ const admitted = admit(sfn, opts.gates ?? OFFMEMBER_GATES, seed);
353
+ if (admitted.size === 0) {
354
+ return null;
355
+ }
356
+ const rewrite = (e: Expr): Expr => {
357
+ if (e.k === 'index' && e.idx.k === 'const' && e.lead === undefined) {
358
+ const hit = admitted.get(baseKey(e.base));
359
+ if (hit) {
360
+ return { k: 'field', base: { k: 'cast', to: hit.type, e: e.base }, name: `m${e.idx.value * e.width}` };
361
+ }
362
+ }
363
+ return mapExprChildren(e, rewrite);
364
+ };
365
+ const structs = [...(sfn.structs ?? []), ...[...admitted.values()].map((a) => a.struct)].sort((a, b) =>
366
+ a.name.localeCompare(b.name),
367
+ );
368
+ // `mapStmtExprs` recurses into nested statement lists, so one call per top-level statement
369
+ // covers the whole body.
370
+ return { ...sfn, structs, body: sfn.body.map((st) => mapStmtExprs(st, rewrite)) };
371
+ }
@@ -0,0 +1,96 @@
1
+ // L3 re-spelling lever: park incoming ARGUMENTS first in the entry straight-line prefix.
2
+ //
3
+ // A copy of an incoming parameter into a local (`v = a1`) reproduces the register park the
4
+ // compiler performed to free a caller-save register (`mov ip, r1`). The park instruction lifts
5
+ // to pure SSA aliasing — no op, no position — so the emitted order falls out of block emission
6
+ // (materialized statements first, edge copies last), while the compiler may have parked BEFORE
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
9
+ // sibling and the differ referees.
10
+ //
11
+ // SCOPE (decline over approximate): only plain assigns in the ENTRY straight-line prefix (the
12
+ // leading run of assigns) move; a park's RHS must be pure over PARAMETERS AND CONSTANTS through
13
+ // scalar nodes only (var/const/un/bin/cast — a memory read or a call would be re-scheduled, not
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
16
+ // parked register is; and a park never crosses a statement that writes a name it reads, reads or
17
+ // writes its destination (`&v` counts as touching v), or carries an effect. Relative order — of the
18
+ // parks and of everything else — is preserved. Declines (null) when nothing moves.
19
+ //
20
+ // WHAT THE DESTINATION TEST IS, stated because it is narrower than "a local": the target is checked
21
+ // against the PARAMS alone, so every other name an `assign` can carry qualifies — and structure.ts
22
+ // spells a store to a bare scalar GLOBAL as an `assign` like any other, so such a store parks too.
23
+ // The crossing checks are NAME-KEYED, so a crossed statement that reaches the destination (or a
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.
26
+ //
27
+ // The kmc hipress residual is this axis's OTHER projection — its keep-load renders first while
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.
30
+ import type { Expr, SFn, Stmt } from './ast';
31
+ import { exprChildren, exprHasEffect } from './ast';
32
+
33
+ type Assign = Extract<Stmt, { k: 'assign' }>;
34
+
35
+ // `addr` counts as touching its name: `foo(&v)` may read or write v through the pointer, so a
36
+ // park must treat it exactly like a direct read-and-write of v.
37
+ const readVars = (e: Expr, acc: Set<string> = new Set()): Set<string> => {
38
+ if (e.k === 'var' || e.k === 'addr') {
39
+ acc.add(e.name);
40
+ }
41
+ for (const c of exprChildren(e)) {
42
+ readVars(c, acc);
43
+ }
44
+ return acc;
45
+ };
46
+
47
+ const pureOverParams = (e: Expr, params: ReadonlySet<string>): boolean => {
48
+ switch (e.k) {
49
+ case 'var':
50
+ return params.has(e.name);
51
+ case 'const':
52
+ return true;
53
+ case 'un':
54
+ case 'cast':
55
+ return pureOverParams(e.e, params);
56
+ case 'bin':
57
+ return pureOverParams(e.l, params) && pureOverParams(e.r, params);
58
+ default:
59
+ return false;
60
+ }
61
+ };
62
+
63
+ export function parkParamsFirst(sfn: SFn): SFn | null {
64
+ const params = new Set(sfn.params.map((p) => p.name));
65
+ let n = 0;
66
+ while (n < sfn.body.length && sfn.body[n].k === 'assign') {
67
+ n++;
68
+ }
69
+ const prefix = sfn.body.slice(0, n) as Assign[];
70
+ const parks: Assign[] = [];
71
+ const rest: Assign[] = [];
72
+ for (const st of prefix) {
73
+ // `rest` is exactly what this park would cross — a REFUSED earlier park is in it, so a later
74
+ // park is re-checked against it like any other crossed statement.
75
+ if (!params.has(st.name) && pureOverParams(st.value, params)) {
76
+ const reads = readVars(st.value);
77
+ if (
78
+ rest.every(
79
+ (c) =>
80
+ !exprHasEffect(c.value) && // a call can write anything an escaped address reaches
81
+ !reads.has(c.name) &&
82
+ c.name !== st.name &&
83
+ !readVars(c.value).has(st.name),
84
+ )
85
+ ) {
86
+ parks.push(st);
87
+ continue;
88
+ }
89
+ }
90
+ rest.push(st);
91
+ }
92
+ if (parks.length === 0 || parks.every((p, i) => prefix[i] === p)) {
93
+ return null; // nothing to move, or the parks already lead the prefix
94
+ }
95
+ return { ...sfn, body: [...parks, ...rest, ...sfn.body.slice(n)] };
96
+ }