@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
package/src/symbols.ts CHANGED
@@ -90,7 +90,12 @@ export interface SymbolPointee {
90
90
 
91
91
  /** One declared type in a signature — width, signedness, pointer-ness. Deliberately the same
92
92
  * vocabulary a struct member uses, so a parameter and a field of the same C type describe
93
- * identically. `size: null` = the DWARF did not size it. */
93
+ * identically. `size: null` = the DWARF did not size it.
94
+ *
95
+ * NO POINTEE, and the absence is UPSTREAM's rather than a shape asmlift dropped:
96
+ * `@gba-kit/debug-info`'s `TypeFacts` — what a `FunctionSignature`'s params are made of — is
97
+ * exactly these three fields. {@link SymbolInfo.pointee} exists only for a symbol AT AN ADDRESS.
98
+ * Widening this is priced in docs/level-tower.md and pinned in test/param-pointee-axis.test.ts. */
94
99
  export interface SymbolTypeFacts {
95
100
  size: number | null;
96
101
  signed: boolean | null;
@@ -202,6 +207,17 @@ export function isArrayField(f: SymbolStructField): boolean {
202
207
  return f.elemSize !== undefined;
203
208
  }
204
209
 
210
+ /** THE one test for "is this field a POINTER", and it is a test of TWO facts. The flag alone is
211
+ * not enough: {@link symbolFieldType} declares a pointer only at `size === 4`, so a `pointer`
212
+ * member of any other size declares as a scalar cell or a byte array — and a consumer trusting
213
+ * the flag alone would spell pointer arithmetic on a value the very declaration beside it calls a
214
+ * `u16`. The two answers must be the SAME answer, for the same reason declaredFields and the
215
+ * synthesis must: core reasoning about a member as something the declaration does not declare is
216
+ * non-compiling C. */
217
+ export function isPtrField(f: SymbolStructField): boolean {
218
+ return f.pointer === true && f.size === 4;
219
+ }
220
+
205
221
  /** THE one test for "is this field a bitfield" — the PRESENCE of `bitWidth` (see the field doc).
206
222
  * The exact (offset,size) scalar-field rules must exclude these: a 7-bit field whose bits span
207
223
  * 2 bytes carries `size: 2` and would otherwise match a plain u16 read at its offset. */
@@ -539,3 +555,48 @@ export function symbolMapFromJson(obj: Record<string, SymbolInfo[]>): SymbolMap
539
555
  }
540
556
  return map;
541
557
  }
558
+
559
+ const HEX_KEY = /^(0x)?[0-9a-fA-F]+$/;
560
+
561
+ /** STRUCTURAL VALIDATION of the vendored-map JSON, in core because BOTH readers of that format
562
+ * need it and a second hand-rolled copy is how they come to disagree about what a map is: the
563
+ * webapp's Symbols pane (which degrades — a bad map is reported and the run proceeds without
564
+ * one) and the cli's `tools.asmlift.symbols` (which is LOUD — an explicit config that failed to
565
+ * load must never read like a row that never had a map).
566
+ *
567
+ * `symbolMapFromJson` alone cannot serve either: it is a total function over `Object.entries`,
568
+ * so `[]` and `{}` and `{"nope": []}` all yield an EMPTY map and no error at all. An empty map
569
+ * is exactly what a map-less run holds, and the two produce different source — so the shapes
570
+ * that silently reduce to it are the ones this checks. Only the load-bearing minimum is
571
+ * validated (hex keys, non-empty arrays, string `name`, `kind` ∈ code|data); the declaration
572
+ * SHAPE fields are typed by this module and fail soft downstream.
573
+ *
574
+ * Emptiness is REPORTED, not judged: a caller that tolerates an empty map reads `map.size`
575
+ * itself. */
576
+ export function parseSymbolMapJson(obj: unknown): { map: SymbolMap } | { error: string } {
577
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
578
+ return {
579
+ error: 'expected an object of hex addresses, e.g. {"0x03001234": [{"name": "gCounter", "kind": "data"}]}',
580
+ };
581
+ }
582
+ for (const [key, infos] of Object.entries(obj)) {
583
+ if (!HEX_KEY.test(key)) {
584
+ return { error: `"${key}" is not a hex address key (e.g. "0x03001234")` };
585
+ }
586
+ if (!Array.isArray(infos) || infos.length === 0) {
587
+ return { error: `"${key}" must map to a non-empty array of symbols` };
588
+ }
589
+ for (const info of infos as unknown[]) {
590
+ const si = info as Partial<SymbolInfo> | null;
591
+ if (
592
+ typeof si !== 'object' ||
593
+ si === null ||
594
+ typeof si.name !== 'string' ||
595
+ (si.kind !== 'code' && si.kind !== 'data')
596
+ ) {
597
+ return { error: `"${key}": every symbol needs a string "name" and "kind": "code" | "data"` };
598
+ }
599
+ }
600
+ }
601
+ return { map: symbolMapFromJson(obj as Record<string, SymbolInfo[]>) };
602
+ }
package/src/target.ts CHANGED
@@ -10,9 +10,27 @@
10
10
  // pre-recovery pass, and idiom gating; a `div` on a target declaring no divider degrades to
11
11
  // a loud opaque (exercised by packages/cli/test/matching/divmul.test.ts). `hwFloat` → idiom
12
12
  // gating only (no float pass yet).
13
- // • capabilities.endianness / flags RESERVED hardware facts, not yet read by any pass
14
- // (byte-addressing will consume endianness; PPC condition regs → flags).
15
- // • compilerBehaviors.*all consumed by the structurer (threaded via StructureOptions).
13
+ // • capabilities.endianness → structureOptionsFor (`littleEndian`), gating LSB-first
14
+ // bitfield-extract recognition in the structurer.
15
+ // • capabilities.flagsRESERVED, not yet read by any pass (PPC condition regs will).
16
+ // • capabilities.readOnlyAddressSinks → the Thumb frame-object audit: a frame address stored to
17
+ // one of these reached a device that only reads through it, so it does not retract `undef`.
18
+ // • capabilities.deviceRegisters → four readers, and they ask ONE question — "would a source
19
+ // have spelled this address `volatile`" — which is a question about SPELLING and may be
20
+ // approximate: the `/vol-store` lever's eligibility (l3/volstore.ts), rank.ts's volatility
21
+ // tie-break between two byte-identical spellings, the first half of `/unreduce`'s
22
+ // disjointness gate (l3/unreduce.ts), and the `/homesplit` pairing's refusal to leave a device
23
+ // READ inline where the spelling it replaces would have qualified it (l3/homesplit.ts).
24
+ // • capabilities.deviceMemoryWriters → the MEMORY-MODEL question, which is a different one and
25
+ // may NOT be approximate: "can a write to this register make the DEVICE write ordinary
26
+ // memory". One reader — `/unreduce`'s second half. Split from `deviceRegisters` because
27
+ // conflating them recorded a false premise (see the field's own comment).
28
+ // • compilerBehaviors.* → mostly consumed by the structurer (threaded via StructureOptions).
29
+ // Four exceptions are read off the target directly, their consumers not being the
30
+ // structurer: `nearBaseSpan` and `foldsConstAddrOffset` (rank.ts, L3 levers),
31
+ // `hoistsSingleSetArm` (raise/pre-recovery.ts, a raising pass) and `arrayShapeFromStride`
32
+ // (raise/globalshape.ts, run on the LIFTED fn). The field names are a
33
+ // SUPERSET of StructureOptions' — see `structureOptionsFor`.
16
34
  //
17
35
  // `capabilities` (HARDWARE facts) vs `compilerBehaviors` (COMPILER canonicalization choices) are
18
36
  // deliberately separate bags: a new compiler must set its behaviors EXPLICITLY instead of
@@ -32,36 +50,225 @@ export interface TargetDescription {
32
50
  compiler: string; // 'agbcc' / 'ido' / 'gcc' / 'mwcc'
33
51
  argRegs: string[];
34
52
  returnReg: string;
53
+ /** Registers this ABI does NOT pass arguments in — half of what makes a def-less live-in read an
54
+ * uninitialised local rather than an argument. The other half is a measurement the FRONTEND
55
+ * owes (did this function save the register), and the rule that combines them is in
56
+ * frontend/ssa.ts (LiveInModel.uninitRegs). ABSENT ⇒ no register partition is claimed, which is
57
+ * what MIPS and PPC take today.
58
+ *
59
+ * It must be DISJOINT from `argRegs`, and the frontend hands both to the builder so that is
60
+ * checked rather than trusted (`checkedLiveInModel`): a spelling that lands in both lists used
61
+ * to delete a parameter and emit `uninit_<reg>` in its place, silently. */
62
+ nonArgRegs?: readonly string[];
63
+ /** Of `nonArgRegs`, the ones this ABI does NOT require a callee to preserve — so the compiler may
64
+ * home a local in one with no prologue save at all, and the save half of the rule above does not
65
+ * apply to it. AAPCS's `ip` is the whole set here, and agbcc really does use it that way.
66
+ *
67
+ * UNDER-stating this list only makes the classification stricter: an unlisted register whose save
68
+ * the frontend cannot find falls back to being a parameter, which is what a target claiming no
69
+ * partition gets. OVER-stating it is the unsound direction — a callee-saved register listed here
70
+ * is classified with no evidence at all, which is the defect the save half exists to close. Every
71
+ * entry must appear in `nonArgRegs`; the frontend refuses a target where one does not. */
72
+ scratchRegs?: readonly string[];
35
73
  // HARDWARE / ISA facts — independent of the compiler.
36
74
  capabilities: {
37
75
  endianness: 'little' | 'big'; // consumed by structureOptionsFor (bitfield extract recognition is LSB-first)
38
76
  hwDivide: boolean; // consumed by patternApplies (idiom gating)
39
77
  hwFloat: boolean; // consumed by patternApplies (idiom gating)
40
78
  flags: boolean; // RESERVED — no pass reads it yet (PPC condition regs will)
79
+ // Addresses a device reads an object THROUGH. A frame address stored to one of these is handed
80
+ // over as a transfer SOURCE, and two facts together are what make that safe to model: the
81
+ // device only ever reads from it, and the register is WRITE-ONLY, so nobody can read the
82
+ // address back out and turn it into a destination. The only code that can name the frame is
83
+ // therefore this function's own, which the Thumb frame-object audit walks.
84
+ //
85
+ // Hardware, so it belongs here — `endianness` above is a board fact rather than an ISA one too
86
+ // (ARMv4T is bi-endian). ABSENT ⇒ every escape is assumed to write, which is the safe
87
+ // direction and what every other target gets.
88
+ readOnlyAddressSinks?: readonly number[];
89
+ // The device-register window, `[start, end)`. A cell in it changes under the program's feet,
90
+ // so a source that touched one all but certainly declared it `volatile`. Its readers all ask
91
+ // the same SPELLING question — "would a source have written `volatile` here" — and the file
92
+ // header's ledger names them and what each does with the answer. None of them decides for the
93
+ // reader: which cells a source qualified is not derivable from the asm, so both spellings are
94
+ // enumerated and the differ referees. ABSENT ⇒ the lever declines everywhere and the tie-break
95
+ // has no preference, which is the neutral direction — outside a declared window the qualifier
96
+ // is a claim about ordinary memory that the target does not support.
97
+ //
98
+ // IT IS NOT A MEMORY-MODEL CLAIM, and reading it as one is how a false premise got recorded
99
+ // in four places (`deviceMemoryWriters` below carries the correction). Approximating the
100
+ // range costs a candidate; approximating the memory model costs a wrong answer.
101
+ deviceRegisters?: readonly [number, number];
102
+ // Byte ranges, `[start, end)`, whose WRITE can make the DEVICE write ordinary memory. The
103
+ // separate, stronger claim: `deviceRegisters` says a cell is not an object a source declares,
104
+ // which is true and says nothing about what the DEVICE then does. A DMA controller reads a
105
+ // control word and writes memory on the program's behalf, so a loop whose every write is a
106
+ // "device register" write can still rewrite any cell — including one a moved read reads.
107
+ //
108
+ // GBA: the four DMA channel CONTROL halfwords (DMAnCNT_H). Bit 15 is the channel enable, and
109
+ // writing it with the bit set starts the transfer immediately; the other three registers of a
110
+ // channel (SAD, DAD, CNT_L) only stage it — which is the same split `readOnlyAddressSinks`
111
+ // above already reasons about from the source side. A store is a trigger when its BYTE RANGE
112
+ // touches one of these, so the 32-bit `DMA3CNT` write every GBA DMA macro ends with
113
+ // (`*(vu32 *)0x040000DC = 0x84000020`) is one, and a halfword write to `DMA3CNT_L` is not.
114
+ //
115
+ // ABSENT ⇒ the target claims nothing, and the one reader treats EVERY device write as a
116
+ // possible memory write — the conservative direction, and what every non-GBA target takes.
117
+ deviceMemoryWriters?: readonly (readonly [number, number])[];
41
118
  };
42
119
  // COMPILER BEHAVIORS — the specific compiler's canonicalization choices, distinct from
43
- // hardware `capabilities`. All consumed by the structurer (threaded through StructureOptions).
120
+ // hardware `capabilities`. Mostly consumed by the structurer (threaded through StructureOptions);
121
+ // the exceptions are listed at the top of this file and each says so at its own field.
44
122
  compilerBehaviors: {
45
123
  // When a loop induction variable's initial value comes from an argument register, some
46
124
  // compilers keep mutating that register across the loop (coalesce → no init copy); others
47
- // copy to a fresh local. IDO -O2 reuses the arg register (true); agbcc/KMC-GCC allocate
48
- // fresh (false).
125
+ // copy to a fresh local. IDO -O2 and KMC GCC -O2 reuse the arg register (true); agbcc
126
+ // allocates fresh (false).
49
127
  coalesceLoopInit?: boolean;
50
128
  // Divergent-if (both arms terminate, no join): reproduce the source branch DIRECTION by
51
129
  // emitting the forward-branch-on-negated-condition (taken arm as `else`). IDO/MIPS preserves
52
130
  // source direction so this must be on to be byte-exact; agbcc/GCC canonicalize either way so
53
131
  // true is a safe default there. A compiler that inverts branch canonicalization sets it
54
- // false. Absent ⇒ true; a compiler opts OUT.
132
+ // false. Absent ⇒ true; a compiler opts OUT. It carries the JOINED case with it:
133
+ // StructureOptions.negateJoinedBranchSense defaults to this value, so the first compiler that
134
+ // preserves divergent sense and inverts joined sense splits them by promoting that option to a
135
+ // field here — never by an `arch ==` branch in the structurer.
55
136
  preserveDivergentBranchSense?: boolean;
56
- // Order the parallel-copy assignments at a CFG edge by the order their values are COMPUTED
57
- // in the predecessor (vs. source/param order), matching a compiler that lays defining ops
58
- // (and the copies reading them) out in computation order. Uniform (true) across all current
59
- // compilers. Absent true; a compiler opts OUT.
60
- orderArgCopiesByComputation?: boolean;
137
+ // Order the parallel-copy assignments at a CFG edge by the order the PREDECESSOR WROTE THEIR
138
+ // DESTINATIONS the frontend's own measurement (ir/core.ts `WriteOrder`), falling back to a
139
+ // def-position proxy on a predecessor no frontend measured. Not "computation order": a
140
+ // destination written with a value defined elsewhere is a plain register copy, and it ranks by
141
+ // where that copy sits, not by where its value was computed. Uniform (true) across all current
142
+ // compilers; absent ⇒ true, and a compiler that opts OUT turns the sort off entirely and emits
143
+ // in source/param order. WHICH order a measured edge takes is not this flag's question and
144
+ // cannot be: the benchmark has rows on both sides inside one compiler (mwcc), so that choice is
145
+ // refereed per row by `/copy-defpos` (rank.ts), never declared per compiler here.
146
+ orderArgCopiesByWriteOrder?: boolean;
61
147
  // Regime-A switch recovery: accept an `x != K` test as a case (the EQUAL side is the case
62
148
  // body). GCC freely emits `!=`; IDO prefers `==`/`<`. Absent ⇒ true (permissive); the
63
149
  // decline path keeps recovery sound either way.
64
150
  switchAllowsNeqCase?: boolean;
151
+ // The compiler collapses `if (…) x = a; else x = b;` into `x = b; if (…) x = a;` when both
152
+ // arms are ONE speculatable SET — gcc 2.x's `jump_optimize` (`gcc/jump.c:443-445`, guard at
153
+ // `:471-502`). The ONE reader is raise/narrowlocal.ts's `edge-extends`, which uses it
154
+ // BACKWARDS: a diamond this compiler would have collapsed and did not is evidence the source
155
+ // DECLARED the local narrow, because `gcc/thumb.h:344` PROMOTE_MODE expands a narrow-declared
156
+ // assignment past one SET. Absent ⇒ false, and the clause never admits. `structureOptionsFor`
157
+ // spreads it onto StructureOptions like every other field here, but NO structurer code reads
158
+ // it: its reader is a pre-recovery pass, threaded from `runPreRecovery`'s own `target`.
159
+ //
160
+ // Set on agbcc, where the 2x2 in raise/narrowlocal.ts's header was compiled and scored. NOT
161
+ // set on MIPS_GCC despite it being the same compiler family: nothing has measured the pair
162
+ // there, the clause reaches 0 of its benchmark rows, and `docs/level-tower.md`'s rule for an
163
+ // unmeasured per-compiler default is to claim nothing.
164
+ hoistsSingleSetArm?: boolean;
165
+ // A subscript over a DECLARED ARRAY OBJECT expands its base ahead of the index, where every
166
+ // pointer or cast base expands it last — so the instruction order in the target's own assembly
167
+ // says which of the two the source wrote, and `raise/globalshape.ts` may derive an array shape
168
+ // for a global no symbol map describes. Absent ⇒ the derivation is empty and every indexed
169
+ // global keeps today's `((T *)&gSym)[i]` cast spelling.
170
+ //
171
+ // "Expands it last" is about the SUBSCRIPT, and a second consumer reads the same flag for a
172
+ // question that is not: `orderLicensedGlobals` asks only where the base was materialized, and a
173
+ // pointer LOCAL materializes it in its own initializer STATEMENT, before the subscript runs —
174
+ // so `u16 *p = (u16 *)&gTbl; p[i]` is base-first in the object while `(p = (u16 *)&gTbl)[i]` is
175
+ // index-first, both through this same fork (compiled; raise/globalshape.ts's header carries the
176
+ // four-way table). This flag is therefore NARROWER than that consumer's mechanism — statement
177
+ // ordering needs no fork, only a compiler that does not schedule — so the home axis is denied
178
+ // to ido/kmc/mwcc for a reason that is not its own. Under-reach, unmeasured, and the fix when a
179
+ // row asks for it is a datum of its own rather than a widening of this one.
180
+ //
181
+ // Set on agbcc, where the fork is `gcc/c-typeck.c build_array_ref`'s
182
+ // `TREE_CODE (TREE_TYPE (array)) == ARRAY_TYPE && TREE_CODE (array) != INDIRECT_REF` and both
183
+ // spellings were compiled against the same target. NOT set anywhere else: whether ido, kmc or
184
+ // mwcc distinguish them at all is unmeasured, and `docs/level-tower.md`'s rule for an
185
+ // unmeasured per-compiler default is to claim nothing. Read off the target by a raising pass
186
+ // (`inferGlobalArrays`), not by the structurer.
187
+ arrayShapeFromStride?: boolean;
188
+ // Which way this compiler hands out FRAME SLOTS against a spilled local's DECLARATION RANK:
189
+ // `ascending` = the earlier-declared spilled local takes the LOWER `[sp,#k]`. Consumed by the
190
+ // structurer (StructureOptions.spillSlotOrder) and applied at emit time by l3/slotorder.ts.
191
+ //
192
+ // `'unknown'` and absent both REFUSE the ordering — there is deliberately no default
193
+ // direction, because the wrong one reorders every declaration list on that target for no
194
+ // reason. That is why three of the four descriptions below ship `'unknown'`: no MIPS or PPC
195
+ // benchmark row lifts with two or more spilled user locals, so no row on those tiers can
196
+ // referee a value, and a value no row can falsify does not earn the level. Two of the three
197
+ // have a direction MEASURED against a committed probe and withheld for that reason; the third
198
+ // (mwcc) has no direction at all, because the probe does not spill there. Each says which it
199
+ // is, and its flip condition, at its own site.
200
+ //
201
+ // KEYED BY DESCRIPTION, WHILE THE FACT IS PER TOOLCHAIN — the first field in this bag with a
202
+ // stated instance of that gap. `MIPS_GCC` serves BOTH `gcc2.7.2kmc` (Snowboard Kids 2's Kyoto
203
+ // build at -O2) and `gcc2.7.2` (Mario Party 3's at -O1); they agree here, and a committed probe
204
+ // says so, but nothing in this bag could express it if they did not. Any behavior that can
205
+ // differ between two toolchains sharing one description is mis-keyed by construction.
206
+ spillSlotOrder?: 'ascending' | 'descending' | 'unknown';
207
+ // Regime-A switch recovery: accept a RELATIONAL test whose BRANCH admits exactly one scrutinee
208
+ // value as that case (`cmp r0, #1 / bcc` is `case 0:` of an unsigned switch) rather than as
209
+ // navigation.
210
+ //
211
+ // A DEFAULT rather than a candidate axis because for agbcc the asm determines the source: at
212
+ // -O2 fold-const rewrites a bounded unsigned comparison into an equality before codegen, so
213
+ // `x < 1u` compiles to `cmp r0, #0 / bne` and `x > 0u` to `cmp r0, #0 / beq` — no source-level
214
+ // comparison chain emits a bound test at all. `emit_case_nodes` runs after folding and does:
215
+ // it jumps straight to `node->left->code_label` on LT once `node_is_bounded (node->left)`, so
216
+ // the remaining value's own test is never emitted. One producer, one reading.
217
+ //
218
+ // Absent ⇒ false, and inheriting it would be wrong rather than merely unmeasured: on the MIPS
219
+ // lanes `sltiu rd, rs, 1` is the ordinary spelling of `!x`, and it lifts to `icmp_ult rs, 1`
220
+ // with no equality fold anywhere — the identical IR shape, from a producer that is not a
221
+ // dispatch. Each compiler opts in on its own dispatch's evidence.
222
+ switchAllowsBoundCase?: boolean;
223
+ // Switch recovery: emit the case arms in the order the ASSEMBLY lays their bodies out, rather
224
+ // than sorted by ascending case value. True claims the compiler emits case bodies as it walks
225
+ // the arms and never MOVES one afterwards — neither reordering basic blocks nor scheduling
226
+ // across them. agbcc declares it from its own sources: `stmt.c` expand_end_case takes
227
+ // `before_case = get_last_insn()` AFTER the bodies are expanded in source order and its closing
228
+ // `reorder_insns` moves only the DISPATCH in front of them, and the Makefile's SRCS compiles
229
+ // neither sched.c nor reorg.c. SCOPE — SRCS does compile jump.c, whose cross-jump merges two
230
+ // identical arm bodies into ONE block, so a merged pair's own order is gone from the asm; that
231
+ // surfaces as two case values sharing a body, which switch-recover.ts ties by ascending value.
232
+ // Absent ⇒ ascending case value, where ido/kmc-gcc/mwcc sit: each has a scheduler and none has
233
+ // been put through that evidence. A compiler opts in on its own, never by inheriting.
234
+ switchArmsFollowLayout?: boolean;
235
+ // Commutative load pairs re-spell in def (evaluation) order (structure.ts lowerDef). Absent
236
+ // ⇒ true — verified byte-exact on agbcc and IDO; a compiler whose scheduler is shown
237
+ // re-ordering independent loads opts OUT here.
238
+ defOrderLoadPairs?: boolean;
239
+ // The single-add-immediate derivation reach for the /nearbase lever (l3/nearbase.ts):
240
+ // neighbor absolute addresses within this many bytes may share one base local. Thumb's
241
+ // `add rd, #imm8` reaches 255. Absent ⇒ the lever stands down for this target.
242
+ nearBaseSpan?: number;
243
+ // Does this compiler CONSTANT-FOLD a constant SUBSCRIPT into the literal address it
244
+ // materializes for an inline constant-address access? agbcc does: `((u8 *)0x3001100)[3]`
245
+ // emits `.word 0x3001103` + `ldrb [r1]` where `u8 *p = (u8 *)0x3001100; p[3]` keeps
246
+ // `.word 0x3001100` + `ldrb [r1, #0x3]`. True is what lets an offset surviving into the memory
247
+ // operand say anything about the source at all; what l3/basecse.ts's `/basefold` admission
248
+ // does with it — and why that is a differ-refereed candidate rather than a default — is that
249
+ // file's header. A compiler opts in on its own compiled pair and never by inheriting: the MIPS
250
+ // and PPC lanes put the addend in the instruction by construction (`lui`/`%lo`, `lis`/`ori`),
251
+ // so a surviving offset carries no information there. Absent ⇒ the row is never offered.
252
+ foldsConstAddrOffset?: boolean;
253
+ // Does this compiler EMIT a memory read in the block the source SPELLED it in? One direction
254
+ // only: the def-block placement rule (StructureOptions.readsStayWhereWritten) re-spells a read
255
+ // at the block the asm performed it in, which reproduces the asm iff nothing sinks a spelled
256
+ // read past a branch and nothing lifts one to a dominator. The CONVERSE — the asm's read block
257
+ // is where the source read — is FALSE even here, and no default may be declared as if it held.
258
+ //
259
+ // agbcc (gcc 2.9-arm, -O2) declares TRUE from its own sources plus a compiled pair: gcc's
260
+ // Makefile SRCS compiles neither sched.c nor reorg.c and toplev.c never mentions
261
+ // flag_schedule_insns, so there is no scheduler; gcse.c calls one_code_hoisting_pass only
262
+ // `if (optimize_size)`, which toplev.c sets only for -Os, so at -O2 the hoister is compiled in
263
+ // and never runs (a -Os project would NOT get this declaration); and `s = *g; if (c) A(s);
264
+ // else B(s);` against `if (c) A(*g); else B(*g);` emits one ldrb + one pool word versus one of
265
+ // each PER ARM, moving neither. The two passes that DO move a read between blocks at -O2 —
266
+ // loop invariant motion, and the PRE that makes the converse false — are refusals the rule
267
+ // owes; structure/analysis.ts carries them.
268
+ //
269
+ // ABSENT ⇒ the rule stands down, where ido/kmc-gcc/mwcc sit: each has a scheduler and none has
270
+ // been put through that pair. A compiler opts in on its own evidence, never by inheriting.
271
+ readsStayWhereWritten?: boolean;
65
272
  };
66
273
  }
67
274
 
@@ -70,8 +277,67 @@ export const ARMV4T_AGBCC: TargetDescription = {
70
277
  compiler: 'agbcc',
71
278
  argRegs: ['r0', 'r1', 'r2', 'r3'],
72
279
  returnReg: 'r0',
73
- capabilities: { endianness: 'little', hwDivide: false, hwFloat: false, flags: true },
74
- compilerBehaviors: { coalesceLoopInit: false, preserveDivergentBranchSense: true, orderArgCopiesByComputation: true },
280
+ // AAPCS passes four in r0-r3, so nothing above them can be an argument. The ATPCS aliases are
281
+ // the spellings this ISA's asm actually uses: censused over the vendored ARM asm, `sb`/`sl`/`ip`/
282
+ // `fp` all occur as operands and no `v<n>`/`a<n>` form does. `sp`, `lr` and `pc` are deliberately
283
+ // absent — sp is the frame, lr is the return address, and neither is a value a source declared.
284
+ nonArgRegs: ['r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10', 'r11', 'r12', 'sb', 'sl', 'fp', 'ip'],
285
+ // AAPCS makes r4-r11 callee-saved and leaves r12 (`ip`, the intra-procedure-call scratch) to the
286
+ // caller, so a local in `ip` needs no save and agbcc puts one there: `dma_fill_uninit` compiles to
287
+ // `mov ip, r1` in two switch arms, no save anywhere, and a `mov r0, ip` past a third arm that
288
+ // writes nothing — an uninitialised local by construction.
289
+ scratchRegs: ['r12', 'ip'],
290
+ // GBA hardware, which this target implies: agbcc is the GBA compiler and this is the only
291
+ // armv4t entry, so `armv4t + agbcc` is the platform. Stated because nothing else states it.
292
+ capabilities: {
293
+ endianness: 'little',
294
+ hwDivide: false,
295
+ hwFloat: false,
296
+ flags: true,
297
+ // The four DMA SOURCE registers (DMA0..3 SAD). Every vendored project spells the transfer the
298
+ // same way — `DmaSet(n, src, dest, control)` takes `vu32 *dmaRegs = REG_ADDR_DMA<n>SAD` and
299
+ // writes `dmaRegs[0] = src`, `dmaRegs[1] = dest`, `dmaRegs[2] = control` — so +0 is the address
300
+ // the engine reads from and the destination is 4 bytes above it. Source Address Control has
301
+ // three legal settings (increment, decrement, fixed) and every one of them is a read; the
302
+ // reload mode that could re-arm a transfer exists only on the DESTINATION side.
303
+ //
304
+ // The idiom this exists for is their `DMA_FILL`: `vu16 tmp = value;
305
+ // DmaSet(n, &tmp, dest, … DMA_SRC_FIXED …)`, where the frame local is the source.
306
+ readOnlyAddressSinks: [0x040000b0, 0x040000bc, 0x040000c8, 0x040000d4],
307
+ // The GBA I/O register file — one page from 0x04000000, the last live register being
308
+ // 0x04000301 (HALTCNT). Everything a source reaches through `REG_*` is in here, and nothing
309
+ // else is: IWRAM, EWRAM, palette, VRAM and OAM are ordinary memory a source does not qualify.
310
+ deviceRegisters: [0x04000000, 0x04000400],
311
+ // DMA0..3 CNT_H — the channel-enable halfwords. Writing one with bit 15 set arms the transfer,
312
+ // and the transfer writes ordinary memory at [DMAnDAD]. Every other I/O register on this board
313
+ // is read or written by the CPU alone.
314
+ deviceMemoryWriters: [
315
+ [0x040000ba, 0x040000bc],
316
+ [0x040000c6, 0x040000c8],
317
+ [0x040000d2, 0x040000d4],
318
+ [0x040000de, 0x040000e0],
319
+ ],
320
+ },
321
+ compilerBehaviors: {
322
+ coalesceLoopInit: false,
323
+ preserveDivergentBranchSense: true,
324
+ orderArgCopiesByWriteOrder: true,
325
+ nearBaseSpan: 255,
326
+ foldsConstAddrOffset: true,
327
+ readsStayWhereWritten: true,
328
+ switchAllowsBoundCase: true,
329
+ switchArmsFollowLayout: true,
330
+ hoistsSingleSetArm: true,
331
+ arrayShapeFromStride: true,
332
+ // agbcc: reload walks pseudos ascending handing each global-alloc loser a fresh slot, a user
333
+ // local's pseudo number is its `expand_decl` position, and the Thumb frame grows UPWARD
334
+ // (FRAME_GROWS_DOWNWARD is commented out in thumb.h). So the earlier-declared spilled local
335
+ // takes the lower offset. The rows that referee it are `synthetic:spillorder` (six `[sp,#k]`
336
+ // operand rows and nothing else, from two locals declared the other way round) and its
337
+ // control `synthetic:spillorder_rev` (the same body in the order asmlift already emits, which
338
+ // must stay a MATCH), plus `synthetic:dma_fill_uninit`, a row this did not author.
339
+ spillSlotOrder: 'ascending',
340
+ },
75
341
  };
76
342
 
77
343
  /** MIPS-II / IDO 7.1 target. IDO is the IRIX C compiler,
@@ -90,8 +356,24 @@ export const MIPS_IDO: TargetDescription = {
90
356
  compilerBehaviors: {
91
357
  coalesceLoopInit: true,
92
358
  preserveDivergentBranchSense: true,
93
- orderArgCopiesByComputation: true,
359
+ orderArgCopiesByWriteOrder: true,
94
360
  switchAllowsNeqCase: false,
361
+ // MEASURED `descending` (the earlier-declared spilled local takes the HIGHER offset) and NOT
362
+ // SHIPPED. The probe is COMMITTED — `packages/core/test/corpus/probe-declrank.c` and its
363
+ // reversed-declaration twin, with this compiler's objects beside them — and a test reads the
364
+ // correspondence off it: 16 of 16 spills, and rank → offset unchanged when the declaration
365
+ // list is reversed, which is what separates declaration rank from the order of the
366
+ // assignments. No ido7.1 benchmark row lifts with two or more spilled user locals — the only
367
+ // spilling shape in the corpus carries a call, and this frontend declines a call — so no row
368
+ // can tell a wrong value from a right one here.
369
+ //
370
+ // FLIP CONDITION, and it has TWO parts because the second is easy to miss. (1) The first
371
+ // ido7.1 row that lifts with two spilled locals. (2) `frontend/mips.ts` must first claim a
372
+ // frame partition (`LiveInModel.declaredLocals`); until it does, the shared stamp refuses every
373
+ // MIPS slot, so this value would order nothing — and if the partition were claimed WRONGLY,
374
+ // O32's caller-owned home area `[0,16)` would be read as this function's first four
375
+ // declaration ranks. Shipping a direction before the partition orders by argument index.
376
+ spillSlotOrder: 'unknown',
95
377
  },
96
378
  };
97
379
 
@@ -103,10 +385,39 @@ export const MIPS_GCC: TargetDescription = {
103
385
  compiler: 'gcc',
104
386
  argRegs: ['a0', 'a1', 'a2', 'a3'],
105
387
  returnReg: 'v0',
106
- // KMC GCC allocates a fresh local for the loop init (coalesceLoopInit false where it differs
107
- // from IDO); the structuring levers take the universal default until a KMC fixture says otherwise.
388
+ // KMC GCC keeps a loop seeded from an argument register IN that register (coalesceLoopInit
389
+ // true, like IDO): test/corpus/gcc-gcd.asm runs its whole loop on a0/a1 with no init copies,
390
+ // and the row it comes from matches only with the parameters as the loop's homes. The other
391
+ // structuring levers take the universal default until a KMC fixture says otherwise.
392
+ //
393
+ // THIS IS A COMPILER-WIDE GUESS STANDING IN FOR A PER-FUNCTION OBSERVATION the assembly states
394
+ // outright: whether the compiler kept a loop's induction variable in its argument register. What
395
+ // would say it is "the header param's register key IS the key the entry value already lives in" —
396
+ // known to the SSA builder (`frontend/ssa.ts` `phiKey`) and to this file (`argRegs`), unexposed.
397
+ // Exposing it would replace two booleans (here, and PPC_MWCC's "false until a CW loop fixture
398
+ // says otherwise") with a measurement.
399
+ // NOT the obvious proxy for it, which was built and measured: adopting the entry value's name
400
+ // when the forward predecessor did not WRITE the param's key moves 36 of the 736 synthetic rows
401
+ // and costs four matches net (continueloop, countpos and loopif on mwcc plus dmafill, dmaptrsrc
402
+ // and dmastride on agbcc lost; maxarr and preupdate_exit_call on agbcc gained) — because a pred
403
+ // that computes the initial value INTO the param's own register wrote the key and still
404
+ // coalesces.
108
405
  capabilities: { endianness: 'big', hwDivide: true, hwFloat: true, flags: false },
109
- compilerBehaviors: { coalesceLoopInit: false, preserveDivergentBranchSense: true, orderArgCopiesByComputation: true },
406
+ compilerBehaviors: {
407
+ coalesceLoopInit: true,
408
+ preserveDivergentBranchSense: true,
409
+ orderArgCopiesByWriteOrder: true,
410
+ // MEASURED `ascending` on both toolchains this description serves — 7 of 7 spills each, and
411
+ // rank → offset unchanged under a reversed declaration list — and NOT SHIPPED, for the same
412
+ // reason as ido7.1: no row on either tier lifts with two or more spilled user locals. Both
413
+ // probes are COMMITTED beside ido7.1's (`corpus/gcc272kmc-declrank*.txt`,
414
+ // `corpus/gcc272-declrank*.txt`) and a test reads the direction off them.
415
+ //
416
+ // The two agreeing is not a formality. The value is per DESCRIPTION and TWO toolchains map
417
+ // here, so a toolchain whose direction differed from its description's would need a
418
+ // per-toolchain override this bag cannot express — see the note at `compilerBehaviors`.
419
+ spillSlotOrder: 'unknown',
420
+ },
110
421
  };
111
422
 
112
423
  /** PowerPC (GameCube/Wii) + Metrowerks CodeWarrior. The real GC/Wii matching target is
@@ -123,18 +434,50 @@ export const PPC_MWCC: TargetDescription = {
123
434
  returnReg: 'r3',
124
435
  capabilities: { endianness: 'big', hwDivide: true, hwFloat: true, flags: true },
125
436
  // CodeWarrior's structuring levers are UNKNOWN until fixtures reveal them — safe universal
126
- // defaults; coalesceLoopInit false until a CW loop fixture says otherwise.
127
- compilerBehaviors: { coalesceLoopInit: false, preserveDivergentBranchSense: true, orderArgCopiesByComputation: true },
437
+ // defaults; coalesceLoopInit false until a CW loop fixture says otherwise — the second of the
438
+ // two compiler-wide guesses standing in for the per-function observation named at MIPS_GCC.
439
+ compilerBehaviors: {
440
+ coalesceLoopInit: false,
441
+ preserveDivergentBranchSense: true,
442
+ orderArgCopiesByWriteOrder: true,
443
+ // NOT MEASURED, and `'unknown'` is therefore the only honest value here rather than a withheld
444
+ // one, as it is at MIPS_IDO and MIPS_GCC. No mwcc row lifts with two or more spilled user
445
+ // locals, and the compiler does not spill the committed declaration-rank probe either: at
446
+ // sixteen locals it homes every one in a register, and at forty it sinks the whole computation
447
+ // past the call so nothing is live across it. And, as at MIPS_IDO, the frame partition comes
448
+ // first: `frontend/ppc.ts` claims no `LiveInModel.declaredLocals`, so the shared stamp records
449
+ // no slot home on this target at all and a direction here would order nothing until it does.
450
+ spillSlotOrder: 'unknown',
451
+ },
128
452
  };
129
453
 
130
454
  /** Build the structurer's options for a target: the function's own `returnsVoid` plus every
131
- * `compilerBehaviors` lever (they map 1:1 onto StructureOptions field names). The ONE place a
132
- * target's compiler behaviors flow into the target-agnostic structurer — a new behavior lever
133
- * is a field in `compilerBehaviors`, consumed automatically. */
455
+ * `compilerBehaviors` lever. The ONE place a target's compiler behaviors flow into the
456
+ * target-agnostic structurer — a new behavior lever is a field in `compilerBehaviors`, consumed
457
+ * automatically.
458
+ *
459
+ * The spread is over the WHOLE bag, so a behavior whose reader is not the structurer rides along
460
+ * and is simply never read: `hoistsSingleSetArm` is one (its reader is a pre-recovery pass), and
461
+ * `nearBaseSpan` / `foldsConstAddrOffset` are read off the target by rank.ts. So the field names
462
+ * are a SUPERSET of StructureOptions', not a bijection, and nothing may derive one from the other
463
+ * by enumerating keys. */
134
464
  export function structureOptionsFor(t: TargetDescription, returnsVoid: boolean): StructureOptions {
135
465
  // `littleEndian` is the one HARDWARE capability the structurer consumes (bitfield extract
136
466
  // recognition is LSB-first); everything else is a compiler behavior.
137
- return { returnsVoid, littleEndian: t.capabilities.endianness === 'little', ...t.compilerBehaviors };
467
+ //
468
+ // ONE FIELD IS NOT A STRAIGHT SPREAD, and this is where the difference belongs. A frame
469
+ // direction has THREE states here — `ascending`, `descending`, and `'unknown'` meaning measured
470
+ // and deliberately not shipped (see `spillSlotOrder` above) — and only TWO downstream: the
471
+ // structurer either has a direction or refuses. `'unknown'` is a fact about what this repo
472
+ // measured, not an instruction to a pass, so it is dropped at the translation rather than
473
+ // carried onto a public option type that would then need a third case nobody branches on.
474
+ const { spillSlotOrder, ...behaviors } = t.compilerBehaviors;
475
+ return {
476
+ returnsVoid,
477
+ littleEndian: t.capabilities.endianness === 'little',
478
+ ...behaviors,
479
+ ...(spillSlotOrder === 'ascending' || spillSlotOrder === 'descending' ? { spillSlotOrder } : {}),
480
+ };
138
481
  }
139
482
 
140
483
  export const C_TYPEDEFS =