@asmlift/core 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +48 -24
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/backend/pascal.ts +2 -2
  5. package/src/codegen-flags.ts +640 -0
  6. package/src/contracts.ts +60 -11
  7. package/src/frontend/disasm.ts +141 -11
  8. package/src/frontend/high-half.ts +149 -0
  9. package/src/frontend/mips.ts +458 -209
  10. package/src/frontend/ppc.ts +332 -67
  11. package/src/frontend/reloc-symbol.ts +109 -0
  12. package/src/frontend/splat.ts +56 -18
  13. package/src/frontend/ssa.ts +127 -30
  14. package/src/frontend/stackargs.ts +420 -0
  15. package/src/frontend/thumb.ts +209 -232
  16. package/src/ir/alias.ts +24 -0
  17. package/src/ir/core.ts +70 -3
  18. package/src/ir/opcodes.ts +52 -7
  19. package/src/ir/parse.ts +7 -1
  20. package/src/ir/simplify.ts +1 -1
  21. package/src/l3/address.ts +2 -2
  22. package/src/l3/advance.ts +373 -0
  23. package/src/l3/argbase.ts +6 -6
  24. package/src/l3/argcopy.ts +269 -0
  25. package/src/l3/ast.ts +110 -22
  26. package/src/l3/basecse.ts +50 -30
  27. package/src/l3/coalesce.ts +118 -61
  28. package/src/l3/gates.ts +75 -1
  29. package/src/l3/hoist.ts +1 -1
  30. package/src/l3/homesplit.ts +13 -13
  31. package/src/l3/initfirst.ts +3 -3
  32. package/src/l3/inlinebase.ts +16 -16
  33. package/src/l3/mentions.ts +68 -5
  34. package/src/l3/mulfirst.ts +3 -3
  35. package/src/l3/nearbase.ts +4 -4
  36. package/src/l3/offmember.ts +5 -5
  37. package/src/l3/parkfirst.ts +6 -6
  38. package/src/l3/pollguard.ts +3 -3
  39. package/src/l3/ptrfield.ts +4 -4
  40. package/src/l3/regspell.ts +8 -8
  41. package/src/l3/reindex.ts +22 -17
  42. package/src/l3/scopebase.ts +32 -29
  43. package/src/l3/sinkinit.ts +7 -7
  44. package/src/l3/slotorder.ts +3 -3
  45. package/src/l3/storage.ts +1 -1
  46. package/src/l3/tailmerge.ts +2 -2
  47. package/src/l3/tailret.ts +70 -0
  48. package/src/l3/typing.ts +3 -3
  49. package/src/l3/unmerge.ts +483 -59
  50. package/src/l3/unreduce.ts +15 -14
  51. package/src/l3/volatileptr.ts +11 -11
  52. package/src/l3/volatileval.ts +11 -11
  53. package/src/l3/volstore.ts +16 -16
  54. package/src/l3/zerosub.ts +6 -6
  55. package/src/mangle.ts +49 -0
  56. package/src/pattern/engine.ts +132 -17
  57. package/src/pipeline.ts +39 -16
  58. package/src/proto.ts +2 -2
  59. package/src/raise/const.ts +203 -3
  60. package/src/raise/divpow2.ts +2 -2
  61. package/src/raise/extscale.ts +345 -0
  62. package/src/raise/globalshape.ts +32 -12
  63. package/src/raise/gvn.ts +2 -2
  64. package/src/raise/magicdiv.ts +2 -2
  65. package/src/raise/memberarrays.ts +4 -4
  66. package/src/raise/narrowlocal.ts +18 -2
  67. package/src/raise/paramwidth.ts +133 -3
  68. package/src/raise/pre-recovery.ts +100 -25
  69. package/src/raise/retsink.ts +389 -19
  70. package/src/raise/shortcircuit.ts +595 -34
  71. package/src/raise/structs.ts +4 -4
  72. package/src/raise/tailsink.ts +141 -0
  73. package/src/rank-declare.ts +21 -13
  74. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  75. package/src/rank.ts +1176 -805
  76. package/src/structure/analysis.ts +87 -90
  77. package/src/structure/bitfields.ts +130 -30
  78. package/src/structure/globalaccess.ts +30 -4
  79. package/src/structure/namecoalesce.ts +32 -13
  80. package/src/structure/retspell.ts +95 -0
  81. package/src/structure/structure.ts +1425 -201
  82. package/src/structure/switch-recover.ts +101 -8
  83. package/src/symbols.ts +127 -6
  84. package/src/target.ts +374 -44
  85. package/src/trace.ts +28 -19
  86. package/src/variation-definitions.ts +1590 -0
  87. package/src/variation-gates.ts +92 -0
  88. package/src/variation-tokens.ts +356 -0
package/src/target.ts CHANGED
@@ -1,4 +1,4 @@
1
- // asmlift — the Target: (isa, compiler) as first-class axes. ABI + capabilities are DATA
1
+ // asmlift — the Target: (isa, compiler) as first-class fields. ABI + capabilities are DATA
2
2
  // consumed generically by shared passes — never a target-name branch inside a shared pass
3
3
  // (m2c's `arch.arch ==` leakage).
4
4
  //
@@ -15,38 +15,46 @@
15
15
  // • capabilities.flags → RESERVED, not yet read by any pass (PPC condition regs will).
16
16
  // • capabilities.readOnlyAddressSinks → the Thumb frame-object audit: a frame address stored to
17
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
18
+ // • capabilities.deviceRegisters → five readers, and they ask ONE question — "would a source
19
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
20
+ // approximate: the `/vol-store` variation's eligibility (l3/volstore.ts), rank.ts's volatility
21
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).
22
+ // disjointness gate (l3/unreduce.ts), the `/homesplit` pairing's refusal to leave a device
23
+ // READ inline where the spelling it replaces would have qualified it (l3/homesplit.ts), and
24
+ // the structurer's refusal to SPELL a dead memory read whose address no qualifier could ever
25
+ // reach (structure.ts `volatileQualifiable`, threaded through StructureOptions).
24
26
  // • capabilities.deviceMemoryWriters → the MEMORY-MODEL question, which is a different one and
25
27
  // may NOT be approximate: "can a write to this register make the DEVICE write ordinary
26
28
  // memory". One reader — `/unreduce`'s second half. Split from `deviceRegisters` because
27
29
  // conflating them recorded a false premise (see the field's own comment).
28
30
  // • 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
31
+ // Five exceptions are read off the target directly, their consumers not being the
32
+ // structurer: `nearBaseSpan` and `foldsConstAddrOffset` (rank.ts, L3 respell variations),
33
+ // `reloadsLocalReread` (raise/pre-recovery.ts), `hoistsSingleSetArm` (two raising passes
34
+ // raise/narrowlocal.ts and raise/retsink.ts) and
35
+ // `arrayShapeFromStride` (raise/globalshape.ts, run on the LIFTED fn). The field names are a
33
36
  // SUPERSET of StructureOptions' — see `structureOptionsFor`.
34
37
  //
35
- // `capabilities` (HARDWARE facts) vs `compilerBehaviors` (COMPILER canonicalization choices) are
38
+ // `capabilities` (HARDWARE facts) vs `compilerBehaviors` (COMPILER canonicalization decisions) are
36
39
  // deliberately separate bags: a new compiler must set its behaviors EXPLICITLY instead of
37
40
  // silently inheriting a universal that is really per-compiler. `coalesceLoopInit` already
38
41
  // differs across targets (IDO true, agbcc/GCC false).
39
42
  // This module is browser-pure by contract (no Node APIs, enforced by
40
43
  // test/browser-safe.test.ts): the toolchain paths that COMPILE for these targets
41
44
  // live in @asmlift/toolchains.
45
+ import { type CodegenProfile, type FlagFamily, parseFlags } from './codegen-flags';
42
46
  import type { StructureOptions } from './structure/structure';
43
47
 
48
+ /** What a compiler's OBJECT shows for a narrow declared parameter — see
49
+ * `compilerBehaviors.narrowParamWitness` for the compiled pair behind each value. */
50
+ export type NarrowParamWitness = 'prologue-extension' | 'home-store-and-in-place' | 'none';
51
+
44
52
  export interface TargetDescription {
45
53
  id: string; // the ISA — 'armv4t' / 'mips' / 'ppc'. Selects the frontend (registry.ts).
46
- // The COMPILER is a first-class axis distinct from the ISA (matching = deoptimize to a specific
54
+ // The COMPILER is a first-class field distinct from the ISA (matching = deoptimize to a specific
47
55
  // compiler): two targets can share an ISA (⇒ one frontend) yet differ here — e.g. MIPS_IDO vs
48
56
  // MIPS_GCC. Consumed by pattern gating (patternApplies) and the report. (version/flags/language
49
- // are future axes, added when earned.)
57
+ // are future fields, added when earned.)
50
58
  compiler: string; // 'agbcc' / 'ido' / 'gcc' / 'mwcc'
51
59
  argRegs: string[];
52
60
  returnReg: string;
@@ -91,7 +99,7 @@ export interface TargetDescription {
91
99
  // the same SPELLING question — "would a source have written `volatile` here" — and the file
92
100
  // header's ledger names them and what each does with the answer. None of them decides for the
93
101
  // 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
102
+ // enumerated and the differ referees. ABSENT ⇒ the variation declines everywhere and the tie-break
95
103
  // has no preference, which is the neutral direction — outside a declared window the qualifier
96
104
  // is a claim about ordinary memory that the target does not support.
97
105
  //
@@ -116,7 +124,7 @@ export interface TargetDescription {
116
124
  // possible memory write — the conservative direction, and what every non-GBA target takes.
117
125
  deviceMemoryWriters?: readonly (readonly [number, number])[];
118
126
  };
119
- // COMPILER BEHAVIORS — the specific compiler's canonicalization choices, distinct from
127
+ // COMPILER BEHAVIORS — the specific compiler's canonicalization decisions, distinct from
120
128
  // hardware `capabilities`. Mostly consumed by the structurer (threaded through StructureOptions);
121
129
  // the exceptions are listed at the top of this file and each says so at its own field.
122
130
  compilerBehaviors: {
@@ -141,7 +149,7 @@ export interface TargetDescription {
141
149
  // where that copy sits, not by where its value was computed. Uniform (true) across all current
142
150
  // compilers; absent ⇒ true, and a compiler that opts OUT turns the sort off entirely and emits
143
151
  // 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
152
+ // cannot be: the benchmark has rows on both sides inside one compiler (mwcc), so that decision is
145
153
  // refereed per row by `/copy-defpos` (rank.ts), never declared per compiler here.
146
154
  orderArgCopiesByWriteOrder?: boolean;
147
155
  // Regime-A switch recovery: accept an `x != K` test as a case (the EQUAL side is the case
@@ -150,18 +158,56 @@ export interface TargetDescription {
150
158
  switchAllowsNeqCase?: boolean;
151
159
  // The compiler collapses `if (…) x = a; else x = b;` into `x = b; if (…) x = a;` when both
152
160
  // 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`
161
+ // `:471-502`). Absent false, and every clause below never admits. `structureOptionsFor`
157
162
  // 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`.
163
+ // it: both readers are raising passes, threaded from their driver's own `target`.
164
+ //
165
+ // TWO READERS, ONE FACT, BOTH READING IT BACKWARDS. One field rather than one per reader,
166
+ // because a second boolean for the same guard lets a round that measures another compiler's
167
+ // `jump_optimize` set one and leave the other false, with both comments reading as
168
+ // authoritative.
169
+ //
170
+ // • raise/narrowlocal.ts's `edge-extends`: a diamond this compiler would have collapsed and
171
+ // did NOT is evidence the source DECLARED the local narrow, because `gcc/thumb.h:344`
172
+ // PROMOTE_MODE expands a narrow-declared assignment past one SET.
173
+ // • raise/retsink.ts's `compiler-hoists-single-set-arm`: a merge-variable select whose arms
174
+ // this guard would have collapsed never comes back as a diamond, so a TARGET holding one
175
+ // was written with early returns and its returns should be sunk.
159
176
  //
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.
177
+ // Set on agbcc, where the 2x2 in raise/narrowlocal.ts's header was compiled and scored and
178
+ // where retsink's seven-function spelling pair was compiled and committed
179
+ // (`packages/core/test/corpus/agbcc-select-{merge,early}.s`). NOT set on MIPS_GCC despite it
180
+ // being the same compiler family: nothing has measured the pair there, the clause reaches 0 of
181
+ // its benchmark rows on either reader, and `docs/level-tower.md`'s rule for an unmeasured
182
+ // compiler behavior is to claim nothing. The evidence a future round needs is one run of
183
+ // `scripts/regen-select-spelling-probes.ts` retargeted at the compiler in question.
164
184
  hoistsSingleSetArm?: boolean;
185
+ // WHAT, IN THIS COMPILER'S OBJECT, WITNESSES A NARROW DECLARED PARAMETER — the fact
186
+ // raise/paramwidth.ts needs before it may retype `s32 a0` to `s8 a0`. Three answers, because
187
+ // the compilers measured give three, and the pass refuses wherever the object is silent. Each
188
+ // target below carries its own measurement, and raise/paramwidth.ts's header carries the
189
+ // compiled pairs all three readings rest on.
190
+ //
191
+ // • `'prologue-extension'` — the extension's POSITION decides it: a narrow-declared
192
+ // parameter widens at the very top of the function, a body cast widens at its use.
193
+ // • `'home-store-and-in-place'` — the position decides NOTHING, both spellings leading the
194
+ // function, and TWO other facts decide it together: the parameter is stored to an argument
195
+ // home nothing reads back AND widened in its own argument register. Each half alone has a
196
+ // compiled counterexample, so only the PAIR separates a declaration from a body cast.
197
+ //
198
+ // THE HOME STORE IS AN `-O2` OBSERVABLE, AND `-g` IS NOT WHAT REMOVES IT. Measured on
199
+ // `int f(s8 x){ return x; }`: present at `-O2` and at `-O2 -g3`, absent at `-O1`, at `-O0`
200
+ // and at `-g` (which implies `-O0`). So the 42 real af rows that build at
201
+ // `-G 0 -non_shared -Wab,-r4300_mul -mips2 -EB -O2 -g3` DO carry it — checked at exactly
202
+ // those flags — and a target built at `-O1`/`-O0` would have to claim `'none'`. The
203
+ // in-place widening survives every one of those levels, but on its own it decides nothing,
204
+ // so the pass refuses there rather than reading half a pair.
205
+ // • `'none'` — the object does not distinguish the two at all, so no reading of it licenses
206
+ // the narrowing. It is a MEASUREMENT rather than a withholding wherever a target claims it.
207
+ //
208
+ // A compiler that sets nothing here also refuses, which is the right default for one nobody has
209
+ // compiled the pair with (docs/level-tower.md: claim nothing about an unmeasured behavior).
210
+ narrowParamWitness?: NarrowParamWitness;
165
211
  // A subscript over a DECLARED ARRAY OBJECT expands its base ahead of the index, where every
166
212
  // pointer or cast base expands it last — so the instruction order in the target's own assembly
167
213
  // says which of the two the source wrote, and `raise/globalshape.ts` may derive an array shape
@@ -174,7 +220,7 @@ export interface TargetDescription {
174
220
  // so `u16 *p = (u16 *)&gTbl; p[i]` is base-first in the object while `(p = (u16 *)&gTbl)[i]` is
175
221
  // index-first, both through this same fork (compiled; raise/globalshape.ts's header carries the
176
222
  // 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
223
+ // ordering needs no fork, only a compiler that does not schedule — so the home variation is denied
178
224
  // to ido/kmc/mwcc for a reason that is not its own. Under-reach, unmeasured, and the fix when a
179
225
  // row asks for it is a datum of its own rather than a widening of this one.
180
226
  //
@@ -182,7 +228,7 @@ export interface TargetDescription {
182
228
  // `TREE_CODE (TREE_TYPE (array)) == ARRAY_TYPE && TREE_CODE (array) != INDIRECT_REF` and both
183
229
  // spellings were compiled against the same target. NOT set anywhere else: whether ido, kmc or
184
230
  // 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
231
+ // unmeasured compiler behavior is to claim nothing. Read off the target by a raising pass
186
232
  // (`inferGlobalArrays`), not by the structurer.
187
233
  arrayShapeFromStride?: boolean;
188
234
  // Which way this compiler hands out FRAME SLOTS against a spilled local's DECLARATION RANK:
@@ -204,11 +250,30 @@ export interface TargetDescription {
204
250
  // says so, but nothing in this bag could express it if they did not. Any behavior that can
205
251
  // differ between two toolchains sharing one description is mis-keyed by construction.
206
252
  spillSlotOrder?: 'ascending' | 'descending' | 'unknown';
253
+ // Arguments past the argument registers are staged into an area this function RESERVES at the
254
+ // BOTTOM of its own frame — `[sp,#0]` upward, one word each — rather than pushed at the call
255
+ // site. It is GCC's ACCUMULATE_OUTGOING_ARGS target macro, and it is what makes an outgoing
256
+ // argument INDISTINGUISHABLE by code alone from a dead local: the words sit inside this
257
+ // frame's reservation and nothing this function does ever reloads one.
258
+ //
259
+ // Absent ⇒ no outgoing area is claimed and the Thumb frontend's stack-argument licence never
260
+ // fires, so a `[sp,#k]` store reaching a call unread declines exactly as it did before that
261
+ // licence existed. Read off the target by the frontend (`frontend/thumb.ts` `declaredCall`),
262
+ // not by the structurer.
263
+ //
264
+ // Set on agbcc, where the layout was read off `gcc/config/arm/thumb.h` and then measured —
265
+ // the corpus's `stkarg` (accepting) and `stkwide` (refusing) rows and kleod's
266
+ // `sub_0804C300` all stage their words at [sp,#0] upward inside the prologue's own
267
+ // reservation. NOT set anywhere else: a push-based caller would stage nothing inside the
268
+ // frame, and `docs/level-tower.md`'s rule for an unmeasured compiler behavior is to claim
269
+ // nothing. No other frontend calls the analysis today, so the field claims a premise rather
270
+ // than changing a verdict — which is the point: a second armv4t compiler must state it.
271
+ stagesOutgoingArgsInFrame?: boolean;
207
272
  // Regime-A switch recovery: accept a RELATIONAL test whose BRANCH admits exactly one scrutinee
208
273
  // value as that case (`cmp r0, #1 / bcc` is `case 0:` of an unsigned switch) rather than as
209
274
  // navigation.
210
275
  //
211
- // A DEFAULT rather than a candidate axis because for agbcc the asm determines the source: at
276
+ // A DEFAULT rather than a candidate variation because for agbcc the asm determines the source: at
212
277
  // -O2 fold-const rewrites a bounded unsigned comparison into an equality before codegen, so
213
278
  // `x < 1u` compiles to `cmp r0, #0 / bne` and `x > 0u` to `cmp r0, #0 / beq` — no source-level
214
279
  // comparison chain emits a bound test at all. `emit_case_nodes` runs after folding and does:
@@ -232,13 +297,43 @@ export interface TargetDescription {
232
297
  // Absent ⇒ ascending case value, where ido/kmc-gcc/mwcc sit: each has a scheduler and none has
233
298
  // been put through that evidence. A compiler opts in on its own, never by inheriting.
234
299
  switchArmsFollowLayout?: boolean;
300
+ // Switch recovery: DECLINE a comparison tree whose own layout INTERLEAVES a test block with a
301
+ // case body, on the reading that the source wrote an if/else-if LADDER there. True claims the
302
+ // compiler emits a source `switch`'s whole dispatch AHEAD of every arm body — the same
303
+ // `expand_end_case` closing `reorder_insns` `switchArmsFollowLayout` is read off, used for the
304
+ // other half of what it does — while a ladder's tests stay above their own bodies.
305
+ //
306
+ // IT NEEDS HALF OF `switchArmsFollowLayout`'s PREMISE, AND THE IMPLICATION RUNS ONE WAY ONLY.
307
+ // That flag PLACES the arms and so needs the whole no-reordering claim (nothing moved a block
308
+ // at all); this one only asks whether any BODY sits above a test, so a compiler that moves
309
+ // instructions, fills delay slots, or reorders within a block can still declare it. A compiler
310
+ // that declares the PLACING one has therefore already said what this one needs — never the
311
+ // converse. `MIPS_GCC` is the standing counterexample to the converse: it declares this flag on
312
+ // its own pairs (below) and deliberately does NOT declare `switchArmsFollowLayout`, because it
313
+ // has a scheduler. Declaring this one is not evidence for that one.
314
+ //
315
+ // agbcc declares it, and its own pair of objects says the reading is not vacuous: at agbcc's
316
+ // canonical flags the same two-case body is 20 bytes (0x14, ten Thumb instructions)
317
+ // written either way and is a DIFFERENT object — the `switch` emits `cmp #0x1e; beq` then
318
+ // `cmp #0x64; bne` before either body, sorted ascending and so in the reverse of the written
319
+ // order; the ladder emits `cmp #0x64; bne` directly above its own body and reaches `cmp #0x1e`
320
+ // only after it. The pair is committed: `corpus/agbcc-sw{frontload,ladder}.s` from
321
+ // `corpus/probe-agbcc-sw{frontload,ladder}.c`, regenerated by
322
+ // `scripts/regen-switch-spelling-probes.ts`, asserted in switch-arms.test.ts.
323
+ //
324
+ // Absent ⇒ every recoverable tree is still spelled `switch`, which is where ido/mwcc sit: each
325
+ // has a scheduler that may move a body above a test, and neither has been put through the pair.
326
+ // A compiler opts in on its own compiled evidence, never by inheriting — and where one
327
+ // description serves two toolchains (`MIPS_GCC`), each toolchain owes its own pair, because the
328
+ // field cannot distinguish them (the KEYED BY DESCRIPTION note at `spillSlotOrder`).
329
+ switchRequiresFrontLoadedTests?: boolean;
235
330
  // Commutative load pairs re-spell in def (evaluation) order (structure.ts lowerDef). Absent
236
331
  // ⇒ true — verified byte-exact on agbcc and IDO; a compiler whose scheduler is shown
237
332
  // re-ordering independent loads opts OUT here.
238
333
  defOrderLoadPairs?: boolean;
239
- // The single-add-immediate derivation reach for the /nearbase lever (l3/nearbase.ts):
334
+ // The single-add-immediate derivation reach for the /nearbase variation (l3/nearbase.ts):
240
335
  // 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.
336
+ // `add rd, #imm8` reaches 255. Absent ⇒ the variation stands down for this target.
242
337
  nearBaseSpan?: number;
243
338
  // Does this compiler CONSTANT-FOLD a constant SUBSCRIPT into the literal address it
244
339
  // materializes for an inline constant-address access? agbcc does: `((u8 *)0x3001100)[3]`
@@ -250,25 +345,56 @@ export interface TargetDescription {
250
345
  // and PPC lanes put the addend in the instruction by construction (`lui`/`%lo`, `lis`/`ori`),
251
346
  // so a surviving offset carries no information there. Absent ⇒ the row is never offered.
252
347
  foldsConstAddrOffset?: boolean;
348
+ // Does this compiler fold a pointer local's OWN ADVANCE back into the memory operand —
349
+ // `*p = a; p = p + 1; *p = b;` → `strh [r3, #0]` + `strh [r3, #2]`, no `add` — so the advanced
350
+ // spelling emits the indexed one's stores wherever the pointee is not volatile? agbcc does, on
351
+ // its own compiled evidence: the four corners in test/advance.test.ts's header, each built
352
+ // through the benchmark's agbcc against `kleod:StreamCmd_SetWindowRegs`'s object, and the pair
353
+ // `TARGET_BEHAVIOR_READINGS` compiles to one object in the matching suite. True ⇒
354
+ // the `advance` registry entry's target gate withholds the UN-QUALIFIED variation, whose spelling this compiler cannot
355
+ // distinguish from the indexed one it already offers; `/advance/volatile` still rides, because
356
+ // `volatile` is what bars the fold and that product is the match on this row. Absent ⇒ falsy ⇒
357
+ // the plain variation ships, which is the conservative reading for a compiler whose pair nobody has
358
+ // compiled — a compiler opts in on its own evidence and never by inheriting.
359
+ foldsPointerAdvance?: boolean;
253
360
  // Does this compiler EMIT a memory read in the block the source SPELLED it in? One direction
254
361
  // only: the def-block placement rule (StructureOptions.readsStayWhereWritten) re-spells a read
255
362
  // at the block the asm performed it in, which reproduces the asm iff nothing sinks a spelled
256
363
  // read past a branch and nothing lifts one to a dominator. The CONVERSE — the asm's read block
257
364
  // is where the source read — is FALSE even here, and no default may be declared as if it held.
258
365
  //
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.
366
+ // agbcc (gcc 2.9-arm) declares TRUE from its own sources plus compiled pairs: gcc's Makefile
367
+ // SRCS compiles neither sched.c nor reorg.c and toplev.c never mentions flag_schedule_insns, so
368
+ // there is no scheduler; and `s = *g; if (c) A(s); else B(s);` against `if (c) A(*g); else
369
+ // B(*g);` emits one ldrb + one pool word versus one of each PER ARM, moving neither. gcse.c calls
370
+ // one_code_hoisting_pass only `if (optimize_size)`, which toplev.c sets only for -Os, so at -O2
371
+ // the hoister never runs. At -Os it runs, and on `if (c) A(*gp); else B(*gp);` it moves only the
372
+ // pool ADDRESS load above the branch: each arm keeps its own dereference, so the read still
373
+ // stays in the block that spelled it. That pair is committed: `corpus/agbcc-hoist-{O2,Os}.s` from
374
+ // `corpus/probe-agbcc-hoist.c`, regenerated by `scripts/regen-flag-pair-probes.ts`, asserted in
375
+ // hoist-level-probes.test.ts. The two passes that DO move a read between blocks at -O2 — loop
376
+ // invariant motion, and the PRE that makes the converse false — are refusals the rule owes;
377
+ // structure/analysis.ts carries them.
268
378
  //
269
379
  // ABSENT ⇒ the rule stands down, where ido/kmc-gcc/mwcc sit: each has a scheduler and none has
270
380
  // been put through that pair. A compiler opts in on its own evidence, never by inheriting.
271
381
  readsStayWhereWritten?: boolean;
382
+ // Does a LOCAL initialised with a memory read its dominating TEST already performed cost this
383
+ // compiler a SECOND load? The pair is the arm of `if (a && (p[1] & 0x7f) == 0x7f) { … }` spelled
384
+ // `u8 v = p[1]; p[2] = v;` against `p[2] = p[1];`, and again with a store and with a call
385
+ // between the local and its use. agbcc loads `p[1]` TWICE for every local spelling and once
386
+ // for the inline one; ido7.1, gcc2.7.2kmc, gcc2.7.2 and mwcc_242_81 load it ONCE for every
387
+ // local spelling, holding the register across the store and across the call. agbcc is the
388
+ // odd one out of five, so the one agbcc-shaped claim that rested on it — raise/shortcircuit.ts's
389
+ // `read-behind-effect`, "a copy analysis.ts spells as a local costs a load" — reads it here
390
+ // rather than running on every target — on mwcc it costs the probe named at PPC_MWCC's value
391
+ // its byte-match.
392
+ // Read off the target by a raising pass (raise/pre-recovery.ts), not by the structurer.
393
+ //
394
+ // ABSENT ⇒ false: the refusal stands down, and a compiler opts IN on its own compiled pair. The
395
+ // three descriptions that measured false (four compilers) set it anyway, so absent means
396
+ // UNMEASURED rather than "no".
397
+ reloadsLocalReread?: boolean;
272
398
  };
273
399
  }
274
400
 
@@ -324,11 +450,15 @@ export const ARMV4T_AGBCC: TargetDescription = {
324
450
  orderArgCopiesByWriteOrder: true,
325
451
  nearBaseSpan: 255,
326
452
  foldsConstAddrOffset: true,
453
+ foldsPointerAdvance: true,
327
454
  readsStayWhereWritten: true,
328
455
  switchAllowsBoundCase: true,
329
456
  switchArmsFollowLayout: true,
457
+ switchRequiresFrontLoadedTests: true,
330
458
  hoistsSingleSetArm: true,
331
459
  arrayShapeFromStride: true,
460
+ reloadsLocalReread: true,
461
+ narrowParamWitness: 'prologue-extension',
332
462
  // agbcc: reload walks pseudos ascending handing each global-alloc loser a fresh slot, a user
333
463
  // local's pseudo number is its `expand_decl` position, and the Thumb frame grows UPWARD
334
464
  // (FRAME_GROWS_DOWNWARD is commented out in thumb.h). So the earlier-declared spilled local
@@ -337,6 +467,9 @@ export const ARMV4T_AGBCC: TargetDescription = {
337
467
  // control `synthetic:spillorder_rev` (the same body in the order asmlift already emits, which
338
468
  // must stay a MATCH), plus `synthetic:dma_fill_uninit`, a row this did not author.
339
469
  spillSlotOrder: 'ascending',
470
+ // agbcc reserves the outgoing area with the rest of the frame (`add sp, sp, #-N` covers both)
471
+ // and stages arguments 5+ into it at [sp,#0] upward — thumb.h's ACCUMULATE_OUTGOING_ARGS.
472
+ stagesOutgoingArgsInFrame: true,
340
473
  },
341
474
  };
342
475
 
@@ -358,6 +491,13 @@ export const MIPS_IDO: TargetDescription = {
358
491
  preserveDivergentBranchSense: true,
359
492
  orderArgCopiesByWriteOrder: true,
360
493
  switchAllowsNeqCase: false,
494
+ // MEASURED — the pair at the field compiles to one load of `p[1]` for every local spelling.
495
+ reloadsLocalReread: false,
496
+ // MEASURED at `-mips2 -O2 -32 -non_shared -G 0`: the `sll` leads the function for BOTH
497
+ // spellings, so the prologue position cannot decide; a narrow DECLARED parameter is the one that
498
+ // is both homed dead AND widened in its own argument register. raise/paramwidth.ts's header has
499
+ // the disassemblies, including the counterexample for each half alone.
500
+ narrowParamWitness: 'home-store-and-in-place',
361
501
  // MEASURED `descending` (the earlier-declared spilled local takes the HIGHER offset) and NOT
362
502
  // SHIPPED. The probe is COMMITTED — `packages/core/test/corpus/probe-declrank.c` and its
363
503
  // reversed-declaration twin, with this compiler's objects beside them — and a test reads the
@@ -388,7 +528,7 @@ export const MIPS_GCC: TargetDescription = {
388
528
  // KMC GCC keeps a loop seeded from an argument register IN that register (coalesceLoopInit
389
529
  // true, like IDO): test/corpus/gcc-gcd.asm runs its whole loop on a0/a1 with no init copies,
390
530
  // 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.
531
+ // structuring compiler behaviors take the universal default until a KMC fixture says otherwise.
392
532
  //
393
533
  // THIS IS A COMPILER-WIDE GUESS STANDING IN FOR A PER-FUNCTION OBSERVATION the assembly states
394
534
  // outright: whether the compiler kept a loop's induction variable in its argument register. What
@@ -407,6 +547,48 @@ export const MIPS_GCC: TargetDescription = {
407
547
  coalesceLoopInit: true,
408
548
  preserveDivergentBranchSense: true,
409
549
  orderArgCopiesByWriteOrder: true,
550
+ // DECLARED ON A PAIR FROM EACH TOOLCHAIN THIS DESCRIPTION SERVES, never inherited from agbcc's
551
+ // and never from one sibling to the other. This description is keyed per DESCRIPTION while the
552
+ // fact is per TOOLCHAIN (the note at `spillSlotOrder`), and `MIPS_GCC` serves two, so both owe
553
+ // a pair: `corpus/gcc272kmc-sw{frontload,ladder}.asm` from GCC_KMC_TOOLCHAIN at -O2 and
554
+ // `corpus/gcc272-sw{frontload,ladder}.asm` from the Mario Party 3 toolchain at -O1 — one
555
+ // two-case body written each way, regenerated from their committed C bodies by
556
+ // `scripts/regen-switch-spelling-probes.ts`, each carrying a provenance header, and each pair
557
+ // two different objects: the `switch` emits both `beq`s before the first `sw`, while the ladder
558
+ // puts an arm's `sw` above the second test. A test asserts that split off every fixture. The
559
+ // two toolchains come out byte-identical on this body — measured, not assumed, and their
560
+ // declaration-rank probe objects differ, so a sibling pair is not a formality.
561
+ //
562
+ // BOTH TOOLCHAINS EMIT BRANCH-LIKELY on some bodies, and the reading survives it — a property
563
+ // of the pair, not a way the two diverge. `s32 m1(s32 x, s32 *p){ switch (x) { case 6: *p = 1;
564
+ // break; case 7: *p = 2; break; } return 0; }` cross-jumps the two stores into one and compiles
565
+ // instruction for instruction identically at -O1 and at -O2, to `beq` / `beql` with the shared
566
+ // `sw` after both; the same body as an if/else-if ladder puts the first arm's `li v0,1` BETWEEN
567
+ // the two tests. Both spellings get all the way through on both toolchains — the `switch` one
568
+ // recovers a `switch`, the ladder an `if` nest — so what keeps that body out of the fixture set
569
+ // is not a decline. It is that the cross-jumped arms leave no STORE to read the split off: both
570
+ // put their one `sw` after both tests, and the interleaved instruction is the ladder's
571
+ // `li v0,1`. The committed pair keeps its distinct-store arms for that reason. (A THREE-case
572
+ // body says the
573
+ // same more loudly, the balanced tree's `slti` bound test landing ahead of the bodies with the
574
+ // rest, but at three cases neither spelling reaches Regime A on this compiler, so that pair
575
+ // could not also serve as the recovery test.)
576
+ //
577
+ // WHAT IS WEAKER HERE THAN AT agbcc: this compiler HAS a scheduler and fills delay slots, and
578
+ // both fixtures show it — the ladder's `bne` carries the NEXT test's `li` in its slot. What the
579
+ // pair shows is that it moves no BODY above a test, which is the only claim the gate rests on,
580
+ // and the gate's failure direction (switch-recover.ts PRE5) is a lost `switch` spelling, never
581
+ // a wrong answer. A lost spelling is not always a clean ladder: recovery re-runs on the
582
+ // sub-trees a decline leaves, so a NESTED dispatch comes back as an `if` nest around a `switch`
583
+ // over some of its arms.
584
+ switchRequiresFrontLoadedTests: true,
585
+ // MEASURED on BOTH toolchains this description serves (the note above): one load of `p[1]` for
586
+ // every local spelling of the pair at the field, gcc2.7.2kmc at -O2 and gcc2.7.2 at -O1 alike.
587
+ reloadsLocalReread: false,
588
+ // MEASURED on BOTH toolchains this description serves: `int f(s8 x){return x;}` and
589
+ // `int f(s32 x){return (s8)x;}` compile to BYTE-IDENTICAL objects, gcc2.7.2kmc at -O2 and
590
+ // gcc2.7.2 at -O1 alike — so the object carries no witness at all and the pass refuses.
591
+ narrowParamWitness: 'none',
410
592
  // MEASURED `ascending` on both toolchains this description serves — 7 of 7 spills each, and
411
593
  // rank → offset unchanged under a reversed declaration list — and NOT SHIPPED, for the same
412
594
  // reason as ido7.1: no row on either tier lifts with two or more spilled user locals. Both
@@ -433,13 +615,22 @@ export const PPC_MWCC: TargetDescription = {
433
615
  argRegs: ['r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10'],
434
616
  returnReg: 'r3',
435
617
  capabilities: { endianness: 'big', hwDivide: true, hwFloat: true, flags: true },
436
- // CodeWarrior's structuring levers are UNKNOWN until fixtures reveal them — safe universal
618
+ // CodeWarrior's structuring compiler behaviors are UNKNOWN until fixtures reveal them — safe universal
437
619
  // defaults; coalesceLoopInit false until a CW loop fixture says otherwise — the second of the
438
620
  // two compiler-wide guesses standing in for the per-function observation named at MIPS_GCC.
439
621
  compilerBehaviors: {
440
622
  coalesceLoopInit: false,
441
623
  preserveDivergentBranchSense: true,
442
624
  orderArgCopiesByWriteOrder: true,
625
+ // MEASURED — one load of `p[1]` for every local spelling of the pair at the field, the value
626
+ // held in a callee-saved register across the call. And the rule it turns off pays here:
627
+ // `u8 v = p[3]; if ((v & 0x7f) == 0x7f) { fnA(); p[4] = v; return; }` under an `if (a)`
628
+ // matches only once `read-behind-effect` stops refusing it (3/24 → MATCH 0/22).
629
+ reloadsLocalReread: false,
630
+ // The PowerPC prologue widens a declared narrow parameter with `extsb`/`extsh`, which the
631
+ // frontend lifts to the same `sext` op agbcc's shift pair folds to — the position shape, on
632
+ // another ISA. `synthetic:{sextb,tos8}:mwcc_242_81` are its rows, MATCH through that pass.
633
+ narrowParamWitness: 'prologue-extension',
443
634
  // NOT MEASURED, and `'unknown'` is therefore the only honest value here rather than a withheld
444
635
  // one, as it is at MIPS_IDO and MIPS_GCC. No mwcc row lifts with two or more spilled user
445
636
  // locals, and the compiler does not spill the committed declaration-rank probe either: at
@@ -451,9 +642,146 @@ export const PPC_MWCC: TargetDescription = {
451
642
  },
452
643
  };
453
644
 
645
+ /** A toolchain: one compiler binary, named as decomp.me names it. Several toolchains may share a
646
+ * description (`MIPS_GCC` serves two), and each keeps its own evidence. */
647
+ export interface ToolchainTarget {
648
+ family: FlagFamily;
649
+ /** The codegen flags every committed probe of this toolchain was compiled with: the flags a
650
+ * synthetic row compiles at, and the flags a decompile with none given assumes. They are in
651
+ * normal form (`storedFlags` keeps every word), so the words only the harness needs (`-c`, the
652
+ * diagnostics) live beside the binary's paths in @asmlift/toolchains.
653
+ *
654
+ * A toolchain with no SYNTHETIC tier has NONE, and inventing one would be the fiction the field
655
+ * exists to avoid: nothing about the compiler picks a set, every row names the flags its own
656
+ * build compiles that unit with, and the probes behind its description are run at those. A
657
+ * decompile that reaches such a toolchain with no flags from anywhere is refused rather than
658
+ * resolved against a set nobody chose (`resolveFlags`). */
659
+ canonicalFlags?: readonly string[];
660
+ /** What this compiler does. Every flag set of the toolchain decompiles against it: a profile of a
661
+ * compiler inherits its declarations until a probe refutes one there. */
662
+ description: TargetDescription;
663
+ }
664
+
665
+ export const TOOLCHAIN_TARGETS = {
666
+ agbcc: {
667
+ family: 'agbcc',
668
+ canonicalFlags: ['-mthumb-interwork', '-O2', '-fhex-asm', '-fprologue-bugfix'],
669
+ description: ARMV4T_AGBCC,
670
+ },
671
+ 'ido7.1': {
672
+ family: 'ido',
673
+ canonicalFlags: ['-mips2', '-O2', '-32', '-non_shared', '-G', '0'],
674
+ description: MIPS_IDO,
675
+ },
676
+ 'gcc2.7.2kmc': {
677
+ family: 'gcc',
678
+ canonicalFlags: [
679
+ '-mabi=32',
680
+ '-mgp32',
681
+ '-mfp32',
682
+ '-mno-abicalls',
683
+ '-fno-PIC',
684
+ '-G',
685
+ '0',
686
+ '-funsigned-char',
687
+ '-mips3',
688
+ '-EB',
689
+ '-O2',
690
+ '-fno-builtin',
691
+ '-fno-asm',
692
+ ],
693
+ description: MIPS_GCC,
694
+ },
695
+ 'gcc2.7.2': {
696
+ family: 'gcc',
697
+ canonicalFlags: ['-G0', '-mips3', '-mgp32', '-mfp32', '-O1', '-Wa,--vr4300mul-off'],
698
+ description: MIPS_GCC,
699
+ },
700
+ mwcc_242_81: {
701
+ family: 'mwcc',
702
+ canonicalFlags: [
703
+ '-proc',
704
+ 'gekko',
705
+ '-O4,p',
706
+ '-enum',
707
+ 'int',
708
+ '-inline',
709
+ 'auto',
710
+ '-fp',
711
+ 'hard',
712
+ '-Cpp_exceptions',
713
+ 'off',
714
+ ],
715
+ description: PPC_MWCC,
716
+ },
717
+ // The other two CodeWarrior builds the GameCube projects compile with: 2.3.3b163n (Pikmin's whole
718
+ // game tree) and 2.4.7b107 (Mario Party 4's DOL). Both SHARE `PPC_MWCC`, and on their own evidence
719
+ // rather than because they are the same compiler family: `test/matching/ppc-compiler-behaviors.test.ts`
720
+ // re-runs both of its probes on each binary at the flags that build's rows compile at, and each
721
+ // reads as the description says. What moves those readings is the optimisation level — at `-O0,p`
722
+ // the shipped `mwcc_242_81` re-reads the local too — so the difference is one no per-compiler
723
+ // field could carry anyway.
724
+ //
725
+ // NEITHER HAS CANONICAL FLAGS. They have no synthetic tier: every row of theirs is a real one that
726
+ // names its unit's own flags, and Pikmin's `-O4,p -lang=c++` and Mario Party 4's `-O0,p -lang=c`
727
+ // are two different sets, neither of which is "the" one. See `canonicalFlags` above.
728
+ mwcc_233_163n: {
729
+ family: 'mwcc',
730
+ description: PPC_MWCC,
731
+ },
732
+ mwcc_247_107: {
733
+ family: 'mwcc',
734
+ description: PPC_MWCC,
735
+ },
736
+ } as const satisfies Readonly<Record<string, ToolchainTarget>>;
737
+
738
+ export type ToolchainId = keyof typeof TOOLCHAIN_TARGETS;
739
+
740
+ /** A toolchain that HAS canonical flags — every toolchain with a synthetic tier. Computed from the
741
+ * registry, so the set cannot drift from it: a caller that needs a fallback flag set (the
742
+ * playground's target picker, the benchmark's synthetic rows) takes this instead of `ToolchainId`
743
+ * and a real-only toolchain is refused where it is named, not where it is used. */
744
+ export type CanonicalToolchainId = {
745
+ [K in ToolchainId]: (typeof TOOLCHAIN_TARGETS)[K] extends { canonicalFlags: readonly string[] } ? K : never;
746
+ }[ToolchainId];
747
+
748
+ /** A toolchain's canonical flags, or `undefined` where it has none. The ONE place the optional
749
+ * field is read: the registry's literal type says which entries carry it, and every caller that
750
+ * holds a plain `ToolchainId` has to answer for the ones that do not. */
751
+ export function canonicalFlagsOf(id: ToolchainId): readonly string[] | undefined {
752
+ const t: ToolchainTarget = TOOLCHAIN_TARGETS[id];
753
+ return t.canonicalFlags;
754
+ }
755
+
756
+ export function isCanonicalToolchainId(id: ToolchainId): id is CanonicalToolchainId {
757
+ return canonicalFlagsOf(id) !== undefined;
758
+ }
759
+
760
+ export function isToolchainId(id: string): id is ToolchainId {
761
+ return Object.hasOwn(TOOLCHAIN_TARGETS, id);
762
+ }
763
+
764
+ /** A toolchain at one flag set. */
765
+ export interface ResolvedTarget {
766
+ toolchain: ToolchainId;
767
+ /** the flags the function's target and every candidate compile with */
768
+ cflags: readonly string[];
769
+ /** the description asmlift decompiles against: the toolchain's, at every flag set */
770
+ target: TargetDescription;
771
+ /** what the flags make the compiler do */
772
+ profile: CodegenProfile;
773
+ }
774
+
775
+ /** The target a function compiled by `toolchain` at `cflags` decompiles against, and the profile
776
+ * those flags describe. Throws on a level word the toolchain's family cannot read. */
777
+ export function targetFor(toolchain: ToolchainId, cflags: readonly string[]): ResolvedTarget {
778
+ const t: ToolchainTarget = TOOLCHAIN_TARGETS[toolchain];
779
+ return { toolchain, cflags, target: t.description, profile: parseFlags(t.family, cflags) };
780
+ }
781
+
454
782
  /** Build the structurer's options for a target: the function's own `returnsVoid` plus every
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
783
+ * `compilerBehaviors` field. The ONE place a target's compiler behaviors flow into the
784
+ * target-agnostic structurer — a new compiler behavior is a field in `compilerBehaviors`, consumed
457
785
  * automatically.
458
786
  *
459
787
  * The spread is over the WHOLE bag, so a behavior whose reader is not the structurer rides along
@@ -462,8 +790,9 @@ export const PPC_MWCC: TargetDescription = {
462
790
  * are a SUPERSET of StructureOptions', not a bijection, and nothing may derive one from the other
463
791
  * by enumerating keys. */
464
792
  export function structureOptionsFor(t: TargetDescription, returnsVoid: boolean): StructureOptions {
465
- // `littleEndian` is the one HARDWARE capability the structurer consumes (bitfield extract
466
- // recognition is LSB-first); everything else is a compiler behavior.
793
+ // `littleEndian` and `deviceRegisters` are the HARDWARE capabilities the structurer consumes
794
+ // (bitfield extract recognition is LSB-first; the dead-read spelling refuses outside the device
795
+ // window); everything else is a compiler behavior.
467
796
  //
468
797
  // ONE FIELD IS NOT A STRAIGHT SPREAD, and this is where the difference belongs. A frame
469
798
  // direction has THREE states here — `ascending`, `descending`, and `'unknown'` meaning measured
@@ -475,6 +804,7 @@ export function structureOptionsFor(t: TargetDescription, returnsVoid: boolean):
475
804
  return {
476
805
  returnsVoid,
477
806
  littleEndian: t.capabilities.endianness === 'little',
807
+ ...(t.capabilities.deviceRegisters ? { deviceRegisters: t.capabilities.deviceRegisters } : {}),
478
808
  ...behaviors,
479
809
  ...(spillSlotOrder === 'ascending' || spillSlotOrder === 'descending' ? { spillSlotOrder } : {}),
480
810
  };