@asmlift/core 0.8.1 → 0.10.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 (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
package/src/target.ts CHANGED
@@ -13,27 +13,50 @@
13
13
  // • capabilities.endianness → structureOptionsFor (`littleEndian`), gating LSB-first
14
14
  // bitfield-extract recognition in the structurer.
15
15
  // • capabilities.flags → RESERVED, not yet read by any pass (PPC condition regs will).
16
- // • capabilities.readOnlyAddressSinks → the Thumb frame-object audit: a frame address stored to
16
+ // • capabilities.readOnlyAddressSinks → the 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.readSourceControl → the same audit: which frame bytes beside the object that
19
+ // device may read, so a slot it cannot reach is not refused.
20
+ // • capabilities.blockTransferCalls → the same audit: a frame address handed to one of these
21
+ // as its SOURCE is read as far as the call's literal control word says, and never written.
18
22
  // • capabilities.deviceRegisters → five readers, and they ask ONE question — "would a source
19
23
  // have spelled this address `volatile`" — which is a question about SPELLING and may be
20
24
  // approximate: the `/vol-store` variation's eligibility (l3/volstore.ts), rank.ts's volatility
21
25
  // tie-break between two byte-identical spellings, the first half of `/unreduce`'s
22
26
  // 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).
27
+ // READ inline where the spelling it replaces would have qualified it (l3/homesplit.ts), and the
28
+ // device pin (frontend/device-pins.ts), which spells every access in the window `volatile`
29
+ // in a function the frame-object audit keeps as one object, and in one it accepts object by
30
+ // object every device read and the device stores a later store in their block overwrites —
31
+ // under either policy save a read the compiler could not have made of a `volatile`
32
+ // (`volatileReadsExtendInRegister`).
33
+ // That last reader makes the answer a correctness one — agbcc deletes or hoists a plain
34
+ // device access the machine made — so it may be approximate in ONE direction only: the
35
+ // window must cover every register a source reaches, and covering more costs a spelling,
36
+ // never an access.
26
37
  // • capabilities.deviceMemoryWriters → the MEMORY-MODEL question, which is a different one and
27
38
  // may NOT be approximate: "can a write to this register make the DEVICE write ordinary
28
39
  // memory". One reader — `/unreduce`'s second half. Split from `deviceRegisters` because
29
40
  // conflating them recorded a false premise (see the field's own comment).
30
41
  // • compilerBehaviors.* → mostly consumed by the structurer (threaded via StructureOptions).
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
36
- // SUPERSET of StructureOptions' — see `structureOptionsFor`.
42
+ // Others are read off the target directly by a consumer that is not the structurer — among
43
+ // them `nearBaseSpan` and `foldsConstAddrOffset` (rank.ts, L3 respell variations),
44
+ // `reloadsLocalReread`, `narrowParamWitness` and `aggregateBoundary` (raise/pre-recovery.ts),
45
+ // `aggregateReturn`, `largestAlignment`, `enumBytes`, `bitfieldPacking` and
46
+ // `byValueAggregateArgWords` (aggregate.ts, for frontend/call-plan.ts — which the Thumb, MIPS
47
+ // and PowerPC lowerings share — frontend/thumb.ts, frontend/mips.ts, frontend/ppc.ts and
48
+ // frontend/frame-objects.ts),
49
+ // `hoistsSingleSetArm` (raise/narrowlocal.ts and raise/retsink.ts), `arrayShapeFromStride`
50
+ // (raise/globalshape.ts, run on the LIFTED fn), `eightByteReturnScratch`
51
+ // (frontend/thumb.ts, which reads the epilogue), `callThunks` (frontend/thumb.ts, which
52
+ // lowers a `bl` to one as a call through its register), `farJumpRelay` (frontend/thumb.ts,
53
+ // which lowers a marked `bl` to the branch it stands for), `roundTripsDoubleLiterals`
54
+ // (raise/floathelpers.ts), `volatileReadsExtendInRegister` (frontend/device-pins.ts, and
55
+ // raise/declared-volatile.ts through liftStamped and the offset-name pass), and
56
+ // `keepsUnusedReads` and `discardsUnusedReadsInto` (frontend/dead-reads.ts, the second
57
+ // gathered by frontend/ppc.ts and frontend/mips.ts as they lower a load). The
58
+ // field names are a SUPERSET of
59
+ // StructureOptions' — see `structureOptionsFor`.
37
60
  //
38
61
  // `capabilities` (HARDWARE facts) vs `compilerBehaviors` (COMPILER canonicalization decisions) are
39
62
  // deliberately separate bags: a new compiler must set its behaviors EXPLICITLY instead of
@@ -42,27 +65,192 @@
42
65
  // This module is browser-pure by contract (no Node APIs, enforced by
43
66
  // test/browser-safe.test.ts): the toolchain paths that COMPILE for these targets
44
67
  // live in @asmlift/toolchains.
45
- import { type CodegenProfile, type FlagFamily, parseFlags } from './codegen-flags';
68
+ import { type CodegenProfile, type FlagFamily, dialectOf, parseFlags } from './codegen-flags';
69
+ import type { Op } from './ir/core';
70
+ import { PRELUDE_TYPEDEFS, type ParamType, type Prototypes, declaredWidth, spellableType } from './proto';
71
+ import {
72
+ AGBCC_RUNTIME_HELPERS,
73
+ IDO_RUNTIME_HELPERS,
74
+ MIPS_GCC_RUNTIME_HELPERS,
75
+ PPC_MWCC_RUNTIME_HELPERS,
76
+ PPC_MWCC_SAVE_ROUTINES,
77
+ type RegisterSaveRoutines,
78
+ type RuntimeHelper,
79
+ } from './runtime-helpers';
80
+ import type { StaticLayout } from './structure/local-statics';
46
81
  import type { StructureOptions } from './structure/structure';
82
+ import type { SwitchBoundCase } from './structure/switch-recover';
47
83
 
48
84
  /** What a compiler's OBJECT shows for a narrow declared parameter — see
49
85
  * `compilerBehaviors.narrowParamWitness` for the compiled pair behind each value. */
50
86
  export type NarrowParamWitness = 'prologue-extension' | 'home-store-and-in-place' | 'none';
51
87
 
88
+ /** What one transfer reads from its SOURCE, as its control bits say: the `unit` it reads in, and
89
+ * which way it `walk`s from the address — `fixed` re-reading one unit, `increment` upward for
90
+ * `bytes` when the control says how many and without end when it does not, `decrement` downward
91
+ * without end. Each transfer source decodes its own bit layout into this (`sourceControlRead`,
92
+ * `blockTransferRead`), and `sourceReach` alone turns it into frame bytes. */
93
+ export interface SourceRead {
94
+ unit: number;
95
+ walk: 'fixed' | 'increment' | 'decrement';
96
+ bytes?: number;
97
+ }
98
+
99
+ /** WHERE THIS ABI CAN LEAVE A GENERAL ARGUMENT REGISTER EMPTY BELOW ONE A CALL FILLS — the two ways
100
+ * argument words stop filling in order, read off the facts that already place them:
101
+ * - `skippable`: the registers an argument aligned to 8 can skip, every odd argument word where
102
+ * `evenWordAlignedArgs` holds (O32 `a1`/`a3`, PowerPC `r4`/`r6`/`r8`/`r10`). Compiled on
103
+ * 2026-10-18: `g(1, b, 3LL)` and `g(1, 3LL)` are one object on gcc2.7.2kmc, gcc2.7.2 and the
104
+ * three mwcc builds, `a1`/`r4` untouched under a set pair. agbcc fills every word in order
105
+ * (`g(1, 3LL)` is r0, r1:r2), so it has none.
106
+ * - `leadingFloatShadow`: a leading floating argument travels in the FPU's registers and leaves
107
+ * the general words it counts against holding nothing (`fpu.slots: 'leading'`, O32): with `gp`
108
+ * unprototyped, `gp(x, 5)` of a `float x` parameter and of an `int x` one compile alike on
109
+ * ido7.1, gcc2.7.2kmc and gcc2.7.2 (`li a1,5`, `a0` untouched). PowerPC's floats take no general
110
+ * word (`'separate'`), so they leave no hole there.
111
+ * Its reader is the guessed-arity gap (frontend/call-plan.ts `decideGap`). */
112
+ export function argRegisterHoles(target: Pick<TargetDescription, 'argRegs' | 'evenWordAlignedArgs' | 'fpu'>): {
113
+ skippable: ReadonlySet<string>;
114
+ leadingFloatShadow: boolean;
115
+ } {
116
+ return {
117
+ skippable: new Set(target.evenWordAlignedArgs ? target.argRegs.filter((_, k) => k % 2 === 1) : []),
118
+ leadingFloatShadow: target.fpu?.slots === 'leading',
119
+ };
120
+ }
121
+
122
+ /** The bytes a source read reaches from an address `off` bytes into the frame, as `[lo, hi)`
123
+ * relative to that address. The machine may force the address down to a unit boundary — the
124
+ * GBA's DMA and BIOS both do — so a 32-bit read of the halfword at [sp,#2] reads from [sp,#0],
125
+ * and the object below shares its unit. The frame base is at least unit-aligned, so the offset
126
+ * says how far down. */
127
+ export function sourceReach(read: SourceRead, off: number): { lo: number; hi: number } {
128
+ return {
129
+ lo: read.walk === 'decrement' ? -Infinity : 0 - (off % read.unit),
130
+ hi: read.walk === 'increment' ? (read.bytes ?? Infinity) : read.unit,
131
+ };
132
+ }
133
+
134
+ /** Could the source have made this read through a `volatile` lvalue, on a compiler that behaves as
135
+ * `behaviors` says? Not a sign-extending narrow load where the compiler extends a qualified narrow
136
+ * read in a register (`volatileReadsExtendInRegister`): the qualified read is another instruction
137
+ * sequence. The device pin (frontend/device-pins.ts) and the `declared` stamp
138
+ * (raise/declared-volatile.ts) both ask it, so the two placements read one load the same way. */
139
+ export function readCouldBeVolatile(
140
+ behaviors: Pick<TargetDescription['compilerBehaviors'], 'volatileReadsExtendInRegister'>,
141
+ op: Op,
142
+ ): boolean {
143
+ const width = (op.opcode === 'aload' ? op.attrs.elemSize : op.attrs.width) as number;
144
+ return !(behaviors.volatileReadsExtendInRegister === true && op.attrs.signed === true && width < 4);
145
+ }
146
+
147
+ /** A device channel's source control, read off the halfword at `sink + offset` of a
148
+ * `readOnlyAddressSinks` register: `modes`, indexed by `(control >> modeShift) & (modes.length -
149
+ * 1)`, is the way the source address steps per unit, null for a setting that bounds nothing; a
150
+ * unit is `units[1]` bytes when `wideBit` is set and `units[0]` when not. See `sourceControlRead`. */
151
+ export interface SourceControl {
152
+ offset: number;
153
+ modeShift: number;
154
+ modes: readonly ('increment' | 'decrement' | 'fixed' | null)[];
155
+ wideBit: number;
156
+ units: readonly [number, number];
157
+ }
158
+
159
+ /** The source read one control halfword arms, or null for a mode that bounds nothing. The count
160
+ * lives in another register, so an incrementing read carries no `bytes`. */
161
+ export function sourceControlRead(control: SourceControl, half: number): SourceRead | null {
162
+ const unit = control.units[(half & control.wideBit) !== 0 ? 1 : 0];
163
+ const walk = control.modes[(half >> control.modeShift) & (control.modes.length - 1)];
164
+ return walk === null || walk === undefined ? null : { unit, walk };
165
+ }
166
+
167
+ /** How far a block-transfer call reads through its `source` argument, decoded from the argument
168
+ * at `control`: `control & countMask` units, of `units[1]` bytes when `wideBit` is set and
169
+ * `units[0]` when not, rounded up to a multiple of `countGranule` units — or ONE unit when
170
+ * `fixedBit` is set, since a fill reads its source once. See `blockTransferRead`. */
171
+ export interface BlockTransferCall {
172
+ source: number;
173
+ control: number;
174
+ countMask: number;
175
+ fixedBit: number;
176
+ wideBit: number;
177
+ units: readonly [number, number];
178
+ countGranule: number;
179
+ }
180
+
181
+ /** The source read a block-transfer call makes for a literal control word. */
182
+ export function blockTransferRead(call: BlockTransferCall, control: number): SourceRead {
183
+ const unit = call.units[(control & call.wideBit) !== 0 ? 1 : 0];
184
+ if ((control & call.fixedBit) !== 0) {
185
+ return { unit, walk: 'fixed' };
186
+ }
187
+ const count = (control & call.countMask) >>> 0;
188
+ return { unit, walk: 'increment', bytes: Math.ceil(count / call.countGranule) * call.countGranule * unit };
189
+ }
190
+
52
191
  export interface TargetDescription {
53
192
  id: string; // the ISA — 'armv4t' / 'mips' / 'ppc'. Selects the frontend (registry.ts).
54
193
  // The COMPILER is a first-class field distinct from the ISA (matching = deoptimize to a specific
55
194
  // compiler): two targets can share an ISA (⇒ one frontend) yet differ here — e.g. MIPS_IDO vs
56
- // MIPS_GCC. Consumed by pattern gating (patternApplies) and the report. (version/flags/language
57
- // are future fields, added when earned.)
195
+ // MIPS_GCC. Consumed by pattern gating (patternApplies) and the report. (version/flags are future
196
+ // fields, added when earned.)
58
197
  compiler: string; // 'agbcc' / 'ido' / 'gcc' / 'mwcc'
198
+ /** `'c++'` when the build compiles its unit as C++ (mwcc `-lang=c++`/`ec++`), which is what
199
+ * `targetFor` reads off the flags; absent is C. A candidate is C-shaped text either way — only
200
+ * what the C++ front end refuses differs, and `structureOptionsFor` is where that is answered. */
201
+ dialect?: 'c++';
59
202
  argRegs: string[];
60
203
  returnReg: string;
204
+ /** The floating-point ABI: where a float argument and a float return travel, on a target whose
205
+ * frontend lifts hardware floating point (`docs/floating-point.md`). ABSENT ⇒ none is claimed, and
206
+ * a frontend refuses every read of its FPU file that no instruction of the function wrote.
207
+ *
208
+ * `argRegs` are the float argument registers in argument order, spelled as the frontend keys
209
+ * them; `returnReg` the float return. `slots` is how they count against `argRegs` above, and the
210
+ * two ABIs here differ in kind rather than in degree — both MEASURED by compiling (below):
211
+ * - `'leading'` (MIPS o32): argument k is in `argRegs[k]` only while arguments 0..k are ALL
212
+ * floating, and it still takes integer slot k — `float f(float a, int b)` reads `$f12` and
213
+ * `a1`, while `float f(int a, float b)` receives `b` in `a1` and moves it over with `mtc1`.
214
+ * - `'separate'` (PowerPC EABI): floating arguments count on their own and take no integer
215
+ * slot — `float g(int *p, float b)` and `float g(float b, int *p)` are one object, `r3` and
216
+ * `f1` either way. */
217
+ fpu?: { argRegs: readonly string[]; returnReg: string; slots: 'leading' | 'separate' };
218
+ /** A `double` crosses a call in GENERAL argument words: the two a 64-bit argument takes wherever
219
+ * this target's call placement puts them, HIGH word first, so the sign and exponent are in the
220
+ * first word. Its readers are `proto.ts` `declaredCallArgs` (the call's layout) and
221
+ * `raise/floathelpers.ts` (a literal's two words). ABSENT ⇒ a declared `double` parameter states
222
+ * no layout and the call takes the arg-register guess.
223
+ *
224
+ * IT IS AN ABI FACT BESIDE `fpu`, NOT INSTEAD OF IT. Where the words go is the frontend's 64-bit
225
+ * placement, which a `long long` shares; this states only that a double takes them, and their
226
+ * order, which a `long long` need not share. agbcc: FLOAT_WORDS_BIG_ENDIAN 1
227
+ * (gcc/config/arm/thumb.h:335) on a little-endian target, so the pair is not a `long long`'s
228
+ * naming of the same bits; FUNCTION_ARG (:632) places by word offset, FUNCTION_ARG_PARTIAL_NREGS
229
+ * (:636) splits a pair across r3 and the stack, FUNCTION_ARG_ADVANCE (:647) rounds to a word and
230
+ * PARM_BOUNDARY is 32 (:354).
231
+ *
232
+ * o32 MIPS has an FPU and a double after an integer argument still takes two general words,
233
+ * high first, at an even word as its `long long` does (`evenWordAlignedArgs`). The MIPS targets
234
+ * do not state it because their frontend lays out no 64-bit argument (`CallLowering.pairs`).
235
+ * PowerPC EABI passes a double in a float register that takes no general word
236
+ * (`fpu.slots: 'separate'`), so it has none to state. */
237
+ doubleArgWords?: 'high-first';
238
+ /** AN ARGUMENT ALIGNED TO 8 STARTS AT AN EVEN ARGUMENT WORD, and the word it skips is left empty:
239
+ * a `long long`, a `double` in general words, and a struct passed by value whose alignment is 8,
240
+ * alike. `proto.ts` `wordsOf` counts the skipped word, and `aggregate.ts` `byValueCallArgs` reads
241
+ * the same count to place a struct. O32, compiled on ido7.1, gcc2.7.2kmc and gcc2.7.2:
242
+ * `takeLL(1, 0x200000003LL)` and `takeD(1, 2.5)` set `a0` and `a2:a3` with `a1` unset, and so does
243
+ * `takeL(s32 k, struct L { long long v; } l)`; at word 0 each is `a0:a1`. mwcc's PowerPC EABI aligns
244
+ * a `long long` the same way (its doubles take no general word, and a struct passed by value is
245
+ * passed by address). ABSENT ⇒ every argument takes the next free word: agbcc (the note at
246
+ * `wordsOf`). */
247
+ evenWordAlignedArgs?: true;
61
248
  /** Registers this ABI does NOT pass arguments in — half of what makes a def-less live-in read an
62
249
  * uninitialised local rather than an argument. The other half is a measurement the FRONTEND
63
250
  * owes (did this function save the register), and the rule that combines them is in
64
- * frontend/ssa.ts (LiveInModel.uninitRegs). ABSENT ⇒ no register partition is claimed, which is
65
- * what MIPS and PPC take today.
251
+ * frontend/ssa.ts (LiveInModel.uninitRegs). Thumb and PPC state it and measure the save half
252
+ * from their prologues (Thumb `savedRegs`, PPC and MIPS `prologueSavedRegs`). ABSENT ⇒ no register
253
+ * partition is claimed.
66
254
  *
67
255
  * It must be DISJOINT from `argRegs`, and the frontend hands both to the builder so that is
68
256
  * checked rather than trusted (`checkedLiveInModel`): a spelling that lands in both lists used
@@ -70,14 +258,82 @@ export interface TargetDescription {
70
258
  nonArgRegs?: readonly string[];
71
259
  /** Of `nonArgRegs`, the ones this ABI does NOT require a callee to preserve — so the compiler may
72
260
  * home a local in one with no prologue save at all, and the save half of the rule above does not
73
- * apply to it. AAPCS's `ip` is the whole set here, and agbcc really does use it that way.
261
+ * apply to it. AAPCS's `ip` is the whole set on Thumb, and agbcc really does use it that way;
262
+ * on PPC it is `r0`, `r11` and `r12`, where mwcc homes a leaf's uninitialised locals (PPC_MWCC).
74
263
  *
75
264
  * UNDER-stating this list only makes the classification stricter: an unlisted register whose save
76
- * the frontend cannot find falls back to being a parameter, which is what a target claiming no
77
- * partition gets. OVER-stating it is the unsound direction — a callee-saved register listed here
78
- * is classified with no evidence at all, which is the defect the save half exists to close. Every
79
- * entry must appear in `nonArgRegs`; the frontend refuses a target where one does not. */
265
+ * the frontend cannot find is read as one the callee must preserve, and a read of it before any
266
+ * write refuses (frontend/ssa.ts `LiveInModel.unsavedCalleeSavedRegs`). OVER-stating it is the
267
+ * unsound direction — a callee-saved register listed here is classified with no evidence at all,
268
+ * which is the defect the save half exists to close. Every entry must appear in `nonArgRegs`;
269
+ * the frontend refuses a target where one does not. */
80
270
  scratchRegs?: readonly string[];
271
+ /** Of `scratchRegs`, the ones something outside the C calling convention hands an input in. On
272
+ * PPC that is mwcc's RUNTIME: its register save routines take their frame address in r11
273
+ * (`addi r11,r1,N; bl _savegpr_NN`, and `_savegpr_31` is `stw r31,-4(r11); blr`), and
274
+ * `__ptmf_scall` takes its member-function pointer in r12 (`lwz r0,0(r12)`). On O32 it is
275
+ * hand-written asm, and every scratch register (`O32_SCRATCH_REGS`). Asm that reads one of these
276
+ * first is being handed a value, not reading an uninitialised local.
277
+ *
278
+ * What separates the two is MEASURED on 2.4.2 and 2.3.3: the uninitialised local the compiler
279
+ * homes in r11 or r12 is assigned on SOME path (`uninit_r12`: `if (s) { x = a*b; ... }`), so its
280
+ * entry value meets that definition at a join. A def-less read of one of these registers whose
281
+ * entry value no join meets with a definition of its own refuses (frontend/ssa.ts
282
+ * `LiveInModel.hiddenInputRegs`). r0 is not
283
+ * listed: the same compilers read a never-assigned local straight out of r0 (`int x; return x +
284
+ * a;` is `add r3,r0,r3`), and the runtime passes nothing there.
285
+ *
286
+ * Every entry must appear in `scratchRegs`; the frontend refuses a target where one does not. */
287
+ hiddenInputRegs?: readonly string[];
288
+ /** FILE-SCOPE GLOBAL REGISTER VARIABLES this compiler accepts (`register T g __asm__("r8");`):
289
+ * each callee-saved register a translation unit may bind a global to, keyed by its disassembly
290
+ * spelling and valued by the name `__asm__` takes. gcc's `globalize_reg` (agbcc regclass.c
291
+ * :587-611) makes such a register fixed and call-used, so no prologue saves it: a callee-saved
292
+ * register a function reads before writing, or writes, WITHOUT saving it is one of these, and
293
+ * the lift reads and writes it as that global (frontend/ssa.ts `LiveInModel.globalRegs`).
294
+ * `contiguous` is the order a compiler that declares them only as a contiguous run requires
295
+ * them in: declaring one declares every register before it. ABSENT ⇒ the compiler has no
296
+ * spelling for one, and such a read or write refuses (`unsavedCalleeSavedRegs`). */
297
+ globalRegisterVariables?: { regs: Readonly<Record<string, string>>; contiguous?: readonly string[] };
298
+ /** Registers a CALL destroys: after one, they hold whatever the callee left there, and no value
299
+ * this function computed. The frontends hand this to the SSA builder so a read of one past a
300
+ * call refuses instead of resolving to the pre-call definition (frontend/ssa.ts).
301
+ *
302
+ * REQUIRED, unlike its two neighbours above, and the asymmetry is the point. An absent
303
+ * `nonArgRegs` claims no partition, which only makes classification stricter; an absent
304
+ * `scratchRegs` under-states a set whose own comment says under-stating is safe. An absent
305
+ * caller-saved list takes the UNSOUND direction — nothing refuses, so a destroyed register keeps
306
+ * resolving to a value the callee overwrote, silently and with exit 0. A channel whose omission
307
+ * is unsound is a required argument, so that forgetting it is a type error.
308
+ *
309
+ * `argRegs` must be a SUBSET of it — a register the caller passes arguments in is by
310
+ * construction one the callee may destroy — and `clobberedByCall` (frontend/ssa.ts) checks that
311
+ * rather than trusting it, the way `checkedLiveInModel` checks the register partition. */
312
+ callerSaved: readonly string[];
313
+ /** Registers the ABI reserves to the kernel, which an exception handler may overwrite between ANY
314
+ * two instructions (O32's `k0`/`k1`), so no value written to one is proved to be there when it is
315
+ * read. A read of one refuses, and the return proof never counts one as holding the return
316
+ * address (frontend/mips.ts). Only hand-written asm names them. Absent on an ABI that reserves
317
+ * none. */
318
+ kernelRegs?: readonly string[];
319
+ /** The COMPILER RUNTIME HELPERS this compiler's codegen calls, and what each computes
320
+ * (runtime-helpers.ts). A compiler fact, which is why it lives here and not in `proto.ts`: that
321
+ * file holds signatures fixed by the C STANDARD, and a helper name is fixed by a runtime
322
+ * library — agbcc calls `__muldi3`, CodeWarrior `__div2i`, IDO `__ll_mul`.
323
+ *
324
+ * ABSENT ⇒ no helper is recognised AND none is refused, so every such call stays an ordinary
325
+ * call with its arguments guessed and the backend spells it. That is the UNMEASURED direction,
326
+ * not the safe one: re-emitting a compiler's own runtime call is the one failure that MATCHES,
327
+ * because a compiler handed `__div2i(a, b)` emits the `bl __div2i` the row was lifted from
328
+ * (`raise/widehelpers.ts` states this at the refusal it exists to make). Absence is still the
329
+ * right default, because a name outside the table cannot be told from a project's own
330
+ * `__`-prefixed function by spelling — but it buys nothing on a target whose runtime has simply
331
+ * not been enumerated. */
332
+ runtimeHelpers?: Readonly<Record<string, RuntimeHelper>>;
333
+ /** The runtime's OUT-OF-LINE REGISTER SAVE ROUTINES (runtime-helpers.ts `RegisterSaveRoutines`).
334
+ * ABSENT ⇒ none is recognised: a `bl` to one is an ordinary call, and the frame address it would
335
+ * have consumed is the stack pointer read as data, which refuses. */
336
+ registerSaveRoutines?: RegisterSaveRoutines;
81
337
  // HARDWARE / ISA facts — independent of the compiler.
82
338
  capabilities: {
83
339
  endianness: 'little' | 'big'; // consumed by structureOptionsFor (bitfield extract recognition is LSB-first)
@@ -88,18 +344,26 @@ export interface TargetDescription {
88
344
  // over as a transfer SOURCE, and two facts together are what make that safe to model: the
89
345
  // device only ever reads from it, and the register is WRITE-ONLY, so nobody can read the
90
346
  // address back out and turn it into a destination. The only code that can name the frame is
91
- // therefore this function's own, which the Thumb frame-object audit walks.
347
+ // therefore this function's own, which the frame-object audit walks.
92
348
  //
93
349
  // Hardware, so it belongs here — `endianness` above is a board fact rather than an ISA one too
94
350
  // (ARMv4T is bi-endian). ABSENT ⇒ every escape is assumed to write, which is the safe
95
351
  // direction and what every other target gets.
96
352
  readOnlyAddressSinks?: readonly number[];
353
+ // HOW FAR a device reads from the address a `readOnlyAddressSinks` register was handed, read
354
+ // off the control halfword at `sink + offset` (`SourceControl`). A fixed source re-reads one
355
+ // unit — the unit-aligned one holding the address, since the device may drop its low bits — so
356
+ // the frame bytes outside that unit are provably not read.
357
+ // ABSENT ⇒ the read is unbounded in both directions, the safe direction.
358
+ readSourceControl?: SourceControl;
97
359
  // The device-register window, `[start, end)`. A cell in it changes under the program's feet,
98
360
  // so a source that touched one all but certainly declared it `volatile`. Its readers all ask
99
361
  // the same SPELLING question — "would a source have written `volatile` here" — and the file
100
- // header's ledger names them and what each does with the answer. None of them decides for the
101
- // reader: which cells a source qualified is not derivable from the asm, so both spellings are
102
- // enumerated and the differ referees. ABSENT ⇒ the variation declines everywhere and the tie-break
362
+ // header's ledger names them and what each does with the answer. All but one leave the
363
+ // decision to the differ: which cells a source qualified is not derivable from the asm, so
364
+ // both spellings are enumerated and the differ referees. The device pin decides, because
365
+ // there the plain spelling recompiles to a different program; that is why the window has to
366
+ // cover every register. ABSENT ⇒ the variation declines everywhere and the tie-break
103
367
  // has no preference, which is the neutral direction — outside a declared window the qualifier
104
368
  // is a claim about ordinary memory that the target does not support.
105
369
  //
@@ -123,6 +387,16 @@ export interface TargetDescription {
123
387
  // ABSENT ⇒ the target claims nothing, and the one reader treats EVERY device write as a
124
388
  // possible memory write — the conservative direction, and what every non-GBA target takes.
125
389
  deviceMemoryWriters?: readonly (readonly [number, number])[];
390
+ // CALLS that only READ through one argument, as far as a control argument says — a platform's
391
+ // block-transfer services, bound to a name by the SDK's own stubs. The frame-object audit is
392
+ // the reader: a frame address handed to one as its `source`, with a literal control word,
393
+ // reaches the bytes `blockTransferRead` decodes and is not an address anything writes through.
394
+ // The destination needs no entry, since a frame address handed over anywhere else keeps
395
+ // escaping as one a callee may write through.
396
+ //
397
+ // ABSENT, or a callee it does not name ⇒ an address handed to a callee may be read and written
398
+ // without bound, the safe direction.
399
+ blockTransferCalls?: Readonly<Record<string, BlockTransferCall>>;
126
400
  };
127
401
  // COMPILER BEHAVIORS — the specific compiler's canonicalization decisions, distinct from
128
402
  // hardware `capabilities`. Mostly consumed by the structurer (threaded through StructureOptions);
@@ -182,6 +456,14 @@ export interface TargetDescription {
182
456
  // compiler behavior is to claim nothing. The evidence a future round needs is one run of
183
457
  // `scripts/regen-select-spelling-probes.ts` retargeted at the compiler in question.
184
458
  hoistsSingleSetArm?: boolean;
459
+ // THE REGISTER AN INTERWORKING EPILOGUE POPS THE RETURN ADDRESS INTO WHEN THE RETURN TYPE IS 5
460
+ // TO 8 BYTES, which is then a fact the epilogue states about the function's width. agbcc's
461
+ // `thumb_exit` (gcc/thumb.c) picks that register from the SIZE of the return mode, not from
462
+ // liveness — r0 for `void`, r1 up to 4 bytes, r2 up to 8 — so `pop {r2}; bx r2` ends a function
463
+ // returning a `long long`, a `double`, or an 8-byte aggregate (which DImode covers even when it is
464
+ // returned through memory). Read by `frontend/thumb.ts`'s `refuseWordReturns` directly, not by
465
+ // the structurer. Absent ⇒ the epilogue states nothing.
466
+ eightByteReturnScratch?: string;
185
467
  // WHAT, IN THIS COMPILER'S OBJECT, WITNESSES A NARROW DECLARED PARAMETER — the fact
186
468
  // raise/paramwidth.ts needs before it may retype `s32 a0` to `s8 a0`. Three answers, because
187
469
  // the compilers measured give three, and the pass refuses wherever the object is silent. Each
@@ -250,41 +532,295 @@ export interface TargetDescription {
250
532
  // says so, but nothing in this bag could express it if they did not. Any behavior that can
251
533
  // differ between two toolchains sharing one description is mis-keyed by construction.
252
534
  spillSlotOrder?: 'ascending' | 'descending' | 'unknown';
535
+ // Which way this compiler places the ADDRESS-TAKEN local arrays of one frame against their
536
+ // DECLARATION order: `ascending` = the earlier-declared array takes the lower address. Read
537
+ // where the frame audit declares the one object as several (frame-objects.ts
538
+ // `keepAsOneObject`, which declares the parts `provenParts` finds in this order), so the
539
+ // recompile puts each part back at its own offset. Absent REFUSES that split: the one object
540
+ // stays one array.
541
+ //
542
+ // MEASURED on `u8 a[64]; u8 b[32]; u8 c[48];`, each filled by a `memcpy`, at canonical flags:
543
+ // agbcc a@0 b@64 c@96; gcc2.7.2 and gcc2.7.2kmc a@16 b@80 c@112; ido7.1 a@104 b@72 c@24 (the
544
+ // reverse); mwcc_242_81 a@88 b@8 c@40, neither order — so absent there.
545
+ addressTakenArrayOrder?: 'ascending' | 'descending';
546
+ // The frame-offset multiple this compiler starts each address-taken `u8` array of a frame at,
547
+ // whatever its size. Read by the same split (frame-objects.ts `provenParts`): a boundary off
548
+ // this multiple is never where two of the source's arrays met, and a part declared there is
549
+ // placed at the next multiple by the recompile, moving it and growing the frame. Absent
550
+ // REFUSES the split, as `addressTakenArrayOrder` does.
551
+ //
552
+ // MEASURED on `u8 a[na]; u8 b[nb];`, each filled by a `memcpy` and both handed to a callee, at
553
+ // canonical flags, for na x nb = 4x12, 12x12, 6x10, 8x8, 20x16, 2x14: agbcc puts b at sp+4,
554
+ // 12, 8, 8, 20, 4 (4); gcc2.7.2 and gcc2.7.2kmc a@16 and b@24, 32, 24, 24, 40, 24 (8:
555
+ // BIGGEST_ALIGNMENT on MIPS, for a BLKmode local); ido7.1 b@24 and a@36, 36, 40, 32, 44, 44 (4).
556
+ addressTakenArrayAlignment?: 4 | 8;
557
+ // Does this compiler's C front end take a DESIGNATED array element in an initializer —
558
+ // `u8 a[16] = { 1, [8] = 2 };`? A fact about the dialect the compiler parses, not about C or
559
+ // C++ as languages: GNU C had it long before C99, and agbcc and gcc 2.7.2 (KMC's build and the
560
+ // plain one) compile the line above, while IDO 7.1's cfe ("Syntax Error") and every mwcc build
561
+ // the benchmark ships, 2.3.3, 2.4.2 and 2.4.7 ("expression syntax error"), refuse it — each
562
+ // compiled. Read by the C-family printer (`SFn.designators`) where a constant image
563
+ // (l3/constinit.ts) leaves elements out: it spells the element after a run of them by index,
564
+ // and without this spells the run as zeros, which a compiler that cannot parse the index takes.
565
+ // Absent ⇒ zeros, the spelling every C compiler parses.
566
+ designatedInitializers?: boolean;
567
+ // Does this compiler's C front end REFUSE the three pointer conversions C makes implicitly — an
568
+ // integer to a pointer, a pointer to an integer, and an object pointer to one of another
569
+ // pointee — as C++ does? Every mwcc build the benchmark ships refuses all three in C, compiled
570
+ // with `-lang=c`: 2.4.2 and 2.4.7 ("illegal implicit conversion" of an `unsigned int` to an
571
+ // `int *`) and 2.3.3 ("cannot convert"). agbcc, IDO 7.1 and both gcc 2.7.2 builds compile the same three
572
+ // calls at their canonical flags. Read by the C-family printer (`SFn.strictConversions`), which
573
+ // then casts an argument to its callee's declared parameter and a pointer of another pointee
574
+ // into a pointer slot exactly as it does for C++ (backend/cfamily.ts `argConversion`,
575
+ // `legalizePointerWrites`), and by the store of an integer into a declared pointer cell
576
+ // (structure/pointer-spelling.ts `intoPtrCell`, which declines that store under C++, where only a
577
+ // cast to the cell's own type converts). Each cast is the conversion the compiler would
578
+ // otherwise have made, so it changes no instruction. Absent ⇒ C's implicit conversions.
579
+ strictPointerConversions?: boolean;
253
580
  // Arguments past the argument registers are staged into an area this function RESERVES at the
254
581
  // BOTTOM of its own frame — `[sp,#0]` upward, one word each — rather than pushed at the call
255
582
  // site. It is GCC's ACCUMULATE_OUTGOING_ARGS target macro, and it is what makes an outgoing
256
583
  // argument INDISTINGUISHABLE by code alone from a dead local: the words sit inside this
257
584
  // frame's reservation and nothing this function does ever reloads one.
258
585
  //
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.
586
+ // It says WHERE the words go and nothing about the rest of the frame. Its one reader is the
587
+ // Thumb frontend's stack-argument licence (`frontend/thumb.ts` `declaredCall`): a declared call
588
+ // consumes the words it stages. Absent ⇒ no outgoing area is claimed, and a `[sp,#k]` store
589
+ // reaching a call unread declines.
590
+ //
591
+ // Set on agbcc, where the layout was read off `gcc/config/arm/thumb.h` and then measured — the
592
+ // corpus's `stkarg` and `stkwide` rows and kleod's `sub_0804C300` all stage their words at
593
+ // [sp,#0] upward inside the prologue's own reservation. NOT set anywhere
594
+ // else: a push-based caller would stage nothing inside the frame, and `docs/level-tower.md`'s
595
+ // rule for an unmeasured compiler behavior is to claim nothing. No other frontend calls the
596
+ // analysis today, so a second armv4t compiler must state this premise rather than inherit it.
271
597
  stagesOutgoingArgsInFrame?: boolean;
272
- // Regime-A switch recovery: accept a RELATIONAL test whose BRANCH admits exactly one scrutinee
273
- // value as that case (`cmp r0, #1 / bcc` is `case 0:` of an unsigned switch) rather than as
274
- // navigation.
275
- //
276
- // A DEFAULT rather than a candidate variation because for agbcc the asm determines the source: at
277
- // -O2 fold-const rewrites a bounded unsigned comparison into an equality before codegen, so
278
- // `x < 1u` compiles to `cmp r0, #0 / bne` and `x > 0u` to `cmp r0, #0 / beq` — no source-level
279
- // comparison chain emits a bound test at all. `emit_case_nodes` runs after folding and does:
280
- // it jumps straight to `node->left->code_label` on LT once `node_is_bounded (node->left)`, so
281
- // the remaining value's own test is never emitted. One producer, one reading.
282
- //
283
- // Absent ⇒ false, and inheriting it would be wrong rather than merely unmeasured: on the MIPS
284
- // lanes `sltiu rd, rs, 1` is the ordinary spelling of `!x`, and it lifts to `icmp_ult rs, 1`
285
- // with no equality fold anywhere — the identical IR shape, from a producer that is not a
286
- // dispatch. Each compiler opts in on its own dispatch's evidence.
287
- switchAllowsBoundCase?: boolean;
598
+ // The outgoing area is ONE region at the frame bottom, sized for the widest call and shared by
599
+ // every call, with every local and spill above it, and the caller never reads a word of it
600
+ // back after a call — the callee may assign to its stack parameters, so the caller re-stages
601
+ // an argument before every call. A compiler can stage in its frame
602
+ // (`stagesOutgoingArgsInFrame`) and still break this. mwcc does not, though it puts a local in
603
+ // the words a call COULD take as stack arguments where no call takes any — which is why a
604
+ // guess that cannot say whether one does refuses (`frontend/ppc.ts`, "A GUESS THAT FILLS EVERY
605
+ // ARGUMENT REGISTER") — and not where one does: the area starts past them.
606
+ //
607
+ // It is a UNIVERSAL — for every source, the compiler emits no such read — and its readers apply
608
+ // the contrapositive (`docs/level-tower.md`, THE CONTRAPOSITIVE OF A UNIVERSAL IS NOT A
609
+ // BACKWARDS READING): a word loaded after a call is not in the area, so it is a local, and so is
610
+ // every word above it (`frontend/frame-objects.ts` `storedNeverRead`). An own-frame word no store
611
+ // reaches on any path is a local too, but only the Thumb frontend acts on that
612
+ // (`frontend/thumb.ts`'s own-frame load arm, which reads it as uninitialised), and it was probed
613
+ // on agbcc alone; a MIPS or PPC reader needs its own probe first. A callee that assigned the
614
+ // word as its stack parameter ran before the load, so the load is after a call, and with no
615
+ // call reaching the load there is no such callee. It is
616
+ // exactly as sound as the universal, so its residue is every producer the universal does not
617
+ // cover: hand-written asm that reads its outgoing argument back after a call loses that argument
618
+ // silently. The premise is this compiler's, not the ISA's.
619
+ // Absent ⇒ every staged word stays a candidate argument.
620
+ //
621
+ // It promises the AFTER-CALL form and no more: a compiler may still load a staged word BEFORE a
622
+ // call — o32 gcc defines REG_PARM_STACK_SPACE, so calls.c's save_fixed_argument_area loads
623
+ // staged reg-parm words around a nested call. The stronger form, no load of the area at all,
624
+ // is its own field (`outgoingAreaNeverLoadedFrom`), stated per compiler and never read off this.
625
+ //
626
+ // Set on agbcc: thumb.h puts the locals at sp + outgoing_args_size (ACCUMULATE_OUTGOING_ARGS)
627
+ // and `calls.c` sizes the area as the maximum over all calls; compiled, `spill10` and
628
+ // `spillarg` keep their spills above the area, and `test/corpus/agbcc-restage.s` holds the
629
+ // re-staging, the callee's write, and no load of the area around calls inside arguments. Set
630
+ // on the three MIPS toolchains off their own probe (`test/corpus/probe-o32-frame.c`), where
631
+ // the frame-object audit is the only reader. Set on
632
+ // mwcc off `test/corpus/probe-mwcc-frame.c` on all three builds (`mwcc-frame-probes.test.ts`):
633
+ // nothing but outgoing words lies below the local area, no word there is loaded, and `twice9`
634
+ // stores its ninth argument again before its second call. The frame-object audit is the only
635
+ // reader there too, and on mwcc it is LOAD-BEARING: it is what refuses an undeclared call whose
636
+ // ninth argument the arity guess cannot see (frontend/ppc.ts `measurePpcFrame`).
637
+ localsAboveOutgoingArea?: boolean;
638
+ // The STRONG form of `localsAboveOutgoingArea`, which it requires: from this offset up, the
639
+ // caller never loads a word of its outgoing area at all — not after a call, and not before one
640
+ // either. The offset counts from the area's start, the frame bottom: `[sp,#0]` on Thumb, `0(sp)`
641
+ // on O32 (whose area opens with the a0..a3 home), `0(r1)` past the push on PowerPC (whose
642
+ // parameter area opens at 8, past the callee's back chain and link-register words). From it up
643
+ // the area holds only what the caller staged for a callee; every argument that contains a call
644
+ // is computed before any word is staged, and nothing is saved there around a nested call. So
645
+ // ANY frame word this function loads at or above the offset is a local, and so is every word
646
+ // above it, since the area is one region at the frame bottom. Its readers apply that
647
+ // contrapositive, at exactly the soundness of the universal. Each frontend measures the lowest
648
+ // such word once (Thumb through `frontend/stackargs.ts` `loadedWordBound`, MIPS in `liftOnce`,
649
+ // PPC `ppcLoadedWordBound`), and every reader takes that number: Thumb's outgoing-argument
650
+ // analysis, `frontend/frame-objects.ts` `storedNeverRead` for its pending stores, the MIPS and
651
+ // PPC undeclared-call guesses (`frontend/mips.ts` `lowerJal`, `frontend/ppc.ts` `bl`), and PPC's
652
+ // unread-slot decline, each weighing only the words beneath it. On MIPS it also caps how far an
653
+ // undeclared call's area can reach (`o32OutgoingArea`'s `widest`), which the local area's
654
+ // partition (`measureO32Frame`) and the owned range of an uninitialised read
655
+ // (`measureOwnedFrame`) both read. A load BELOW the offset proves nothing, and no reader counts
656
+ // it. A word no store reaches on any path needs only the after-call form (see
657
+ // `localsAboveOutgoingArea`).
658
+ // Absent ⇒ only the after-call form is claimed (when `localsAboveOutgoingArea` is set).
659
+ //
660
+ // Its residue is the universal's: hand-written asm that loads a word it staged, then stages it
661
+ // for a call, loses that argument. Each value is MEASURED, and the compiled probes are committed
662
+ // (`packages/core/test/outgoing-area-probes.test.ts` reads every one):
663
+ // • agbcc, 0, off its source: thumb.h defines no REG_PARM_STACK_SPACE, so calls.c:642's
664
+ // save_fixed_argument_area — which loads staged words around a nested call — is not
665
+ // compiled; calls.c:1599-1610 precomputes every argument containing a call; and
666
+ // store_one_arg (calls.c:3420-3470) saves an argument word only when a nested call overlaps
667
+ // it, which the precompute rules out. Compiled, `test/corpus/agbcc-restage.s` loads no word
668
+ // of the area around calls in stack arguments.
669
+ // • ido7.1, gcc2.7.2kmc and gcc2.7.2, 16: `test/corpus/*-outarea.txt` at the rows' levels
670
+ // (ido -O1, -O2 and -O2 -g3; gcc 2.7.2 -O1 and -O2) and at -O0. NOT 0 — ido stages a
671
+ // by-value aggregate's register words in the a1..a3 home and loads them back before the
672
+ // call (`big1`: `lw a1,4(sp)`), and kmc and gcc 2.7.2 do it for a char-array struct (`d3`,
673
+ // `d5`): o32 gcc defines REG_PARM_STACK_SPACE, and GCC 2.9's save_fixed_argument_area
674
+ // (calls.c:648-711) covers only `[0, reg_parm_stack_space)`. No probe loads a word from
675
+ // 16(sp) up the area.
676
+ // • mwcc, 8, on all three builds at the rows' flags and at -O0,p, and on mwcc_242_81 at -O4,s
677
+ // too: no probe loads a word of the parameter area; a struct-return temp or a by-value copy
678
+ // sits at 8(r1) only where no call takes a stack word, and above the area beside one that
679
+ // does.
680
+ // Like `stagesOutgoingArgsInFrame`, a second compiler of the same ISA must state this premise
681
+ // rather than inherit it.
682
+ outgoingAreaNeverLoadedFrom?: number;
683
+ // A frame of EXACTLY ONE reserved word whose base escapes this function holds that object and
684
+ // nothing else. It is the layout premise the Thumb frontend's address-taken-local capability
685
+ // rests on (`frontend/thumb.ts`, `capturedObjectIsTheWholeFrame`): a bare `mov rD, sp` names
686
+ // frame offset 0, and the question this answers is whether the rest of that reservation could
687
+ // be somebody else's — an outgoing argument staged at [sp,#0], a by-value struct argument's
688
+ // block-copy destination, or a struct return's hidden pointer.
689
+ //
690
+ // Absent ⇒ no such claim, and every `[sp,#k]` store reaching a call unread keeps the refusal
691
+ // it had before the capability existed. It is a layout fact about ONE compiler and it is not
692
+ // derivable from the architecture, which is why it is a field and not an `arch ==` branch
693
+ // (`docs/level-tower.md`).
694
+ //
695
+ // Set on agbcc, where it is two compiled producer tables rather than a reading — one per
696
+ // escape, both written out at the predicate: at one word agbcc stages no outgoing argument
697
+ // under a captured local (it moves the local above the area and spells its address `add rD,
698
+ // sp, #4`), and it names a block-copy base with a register only from two words up. The one
699
+ // producer a one-word frame does NOT exclude is a <=4-byte non-integer-like struct return,
700
+ // which the post-lift audit settles per call rather than by size.
701
+ oneWordFrameIsTheCapturedObject?: boolean;
702
+ // A frame address this function hands out — in an argument register at a call, or stored to
703
+ // memory — is the address of a LOCAL, so its receiver may read the frame from there up and no
704
+ // word from there up is a call's outgoing argument. Its one reader is the outgoing-argument
705
+ // analysis (`frontend/stackargs.ts`, the `escape` event): a store never reloaded HERE is read
706
+ // THERE. Absent ⇒ a handed-out frame address explains nothing.
707
+ //
708
+ // Set on agbcc, compiled at 2.9-arm-000512 and its canonical flags, and re-measurable from the
709
+ // committed probe (`scripts/regen-outgoing-area-probes.ts` → `test/corpus/agbcc-escape.s`, read
710
+ // by `frame-escape-probes.test.ts`). An address-taken local sits ABOVE the outgoing area
711
+ // (`f1`: `add r0, sp, #4` beside `h5(1,2,3,4,x)`), and at [sp,#0] only when no call in the
712
+ // function takes a stack word (`f0`).
713
+ //
714
+ // Its RESIDUE is a by-value struct argument's block copy (`mov r0, sp` / `add r0, sp, #4`, then
715
+ // `bl memcpy`), whose base IS the outgoing area. agbcc stages a call's outgoing words in
716
+ // ASCENDING offset order, the copy included (`qh5`, `a2`, `a3`): a word staged before the copy
717
+ // lies below its base, and one staged after it is still pending at the call. That is why the
718
+ // event ends pending words PER PROGRAM POINT and only from its offset up — read
719
+ // flow-insensitively, it would set aside `qh5`'s fifth `h5` word and lift `h5(1, 2, 3, 4)`. The
720
+ // cost is false alarms that decline loud (`if (x) gt(&s); else h(y);`); where the analysis also
721
+ // reads the event whole-function, it only refuses or excuses a word no call takes
722
+ // (stackargs.ts). `oneWordFrameIsTheCapturedObject` is the flow-insensitive licence, sound only
723
+ // where no block copy fits.
724
+ escapedFrameAddressNamesALocal?: boolean;
725
+ // WHERE AN O32 FUNCTION KEEPS ITS LOCALS in the one frame `addiu sp,sp,-N` reserves — the area
726
+ // the MIPS frontend hands the frame-object audit as `ownedLocals`/`declaredLocals`. Below it
727
+ // lies the OUTGOING area, the 16-byte home of a0..a3 every calling function reserves and then
728
+ // the stack words its widest call takes (none in a function that makes no call); the saved
729
+ // registers and the locals share the rest, and which way round is the compiler's:
730
+ // 'above-outgoing-area' — from the outgoing area's end, rounded up to the 8-byte stack
731
+ // alignment, to the lowest saved register;
732
+ // 'above-saved-registers' — from just above the highest saved register (or the outgoing
733
+ // area's end, where that is higher) to the frame top N.
734
+ // Spill temps live in the same area on both, so it is the locals' and the spills' together.
735
+ //
736
+ // MEASURED off `corpus/probe-o32-frame.c` (`scripts/regen-o32-frame-probes.ts`, read by
737
+ // `o32-frame-probes.test.ts`): a leaf's array at sp+0 on all three MIPS toolchains; with a call
738
+ // taking no stack word, gcc 2.7.2 (both builds) at sp+16 under the saves and IDO at the frame top
739
+ // above `ra`; with one stack word, gcc at sp+24 (20 rounded) and IDO still at the top; and IDO's
740
+ // spill of a value live across a call between `ra` and the array.
741
+ //
742
+ // Absent ⇒ no partition is claimed, and a captured frame address keeps its refusal.
743
+ o32LocalArea?: 'above-outgoing-area' | 'above-saved-registers';
744
+ // WHERE A FUNCTION THAT PUSHES ONE FRAME KEEPS ITS LOCALS, on a target whose frame starts with a
745
+ // fixed LINKAGE area — the back chain and the callee's link-register save word, which the ABI
746
+ // fixes and the frontend states (`frontend/ppc.ts` `PARAM_AREA_AT`, where the caller's parameter
747
+ // area starts). Above it the OUTGOING area holds the stack words of the widest call (none in a
748
+ // function whose calls take none), and the locals and spill temps share what is left below the
749
+ // lowest saved register:
750
+ // [linkage + 4 * (stack words of the widest call), lowest saved register)
751
+ // in post-push offsets — O32's `above-outgoing-area` rule (`o32LocalArea`) past a linkage area
752
+ // instead of the a0..a3 home, without the 8-byte rounding (`areaAboveOutgoing`). The PPC
753
+ // frontend hands it to the frame-object audit as `ownedLocals`/`declaredLocals`; a call it
754
+ // lowers takes no stack word, so it refuses one whose declaration may and draws the area from
755
+ // the linkage's end. No object start inside it is fixed: the compiler chooses where in the area
756
+ // each local goes.
757
+ //
758
+ // MEASURED on mwcc 2.3.3, 2.4.2 (at -O4,p and at Mario Party 4's -O0,p) and 2.4.7 off
759
+ // `corpus/probe-mwcc-frame.c` (`scripts/regen-mwcc-frame-probes.ts`, read by
760
+ // `mwcc-frame-probes.test.ts`): every local and every home or spill lies in the area, a leaf
761
+ // with a local still pushes a frame whose area starts at 8, one or two stack words of a call push
762
+ // the locals past them, and the widest call sizes the area for every call. The first local's
763
+ // offset inside it differs per build and per function (8, 12 or 16 on 2.3.3; a homed parameter
764
+ // takes 8 at -O0,p), which is why this is an area and not a start.
765
+ //
766
+ // Absent ⇒ no partition is claimed, and a captured frame address keeps its refusal.
767
+ linkageLocalArea?: 'above-outgoing-area';
768
+ // How this compiler calls through a register: `bl <prefix><reg>` enters a runtime thunk that
769
+ // branches to the address in <reg> with every register as the caller left it, so the call is
770
+ // an indirect call through <reg> and the thunk is no function a source names. `regs` are the
771
+ // thunks whose register can hold an address a source computed; a `<prefix>` callee outside them
772
+ // declines.
773
+ //
774
+ // ABSENT ⇒ a `bl` names a function on this target, whatever it is called.
775
+ callThunks?: { readonly prefix: string; readonly regs: readonly string[] };
776
+ // How this compiler marks a BRANCH it had to spell as a call: a `bl <label>` whose line carries
777
+ // a comment matching `comment` (tested against the text from its `@` to the end of the line) is
778
+ // a relay to that label of the same function, not a call. Two of its facts are this compiler's,
779
+ // and the Thumb frontend's lowering rests on them (`lowerFarJumpRelays`): what the mark means,
780
+ // and that the compiler pushes lr in every function with one, so the lr the relay writes is
781
+ // never the one the function returns through.
782
+ //
783
+ // ABSENT ⇒ no comment makes a `bl` anything but a call: a `bl` to a label of the function keeps
784
+ // its decline, however its line is commented.
785
+ farJumpRelay?: { readonly comment: RegExp };
786
+ // Regime-A switch recovery: accept a RELATIONAL test as a case where the scrutinee values that
787
+ // can reach it — narrowed by the tests above it — leave exactly one on a side this compiler's
788
+ // dispatch lands a case body on: `'taken'` for its BRANCH only, `'either'` for both sides
789
+ // (structure/switch-recover.ts, the note at `Ranges`).
790
+ //
791
+ // One mechanism on both compilers that declare it: a binary-search dispatch stops testing a
792
+ // value once the tests above have narrowed it to one. agbcc's `emit_case_nodes` jumps straight to
793
+ // `node->left->code_label` on LT once `node_is_bounded (node->left)` holds, so
794
+ // `switch (x) { case 1: … case 2: … case 3: … case 4: … case 5: … case 1000: … case 2000: … }`
795
+ // pins `case 3` with `cmp r1, #2; bgt` under `x < 4`, `x != 2` (`corpus/agbcc-swpathbound.s`),
796
+ // and an unsigned switch's `case 0` with `cmp r0, #1; bcc`, which pins it at the end of the
797
+ // domain whatever came first. mwcc's `switch (x) { case 0: … case 1: … default: … }` is
798
+ // `cmpwi r3,1; beq- case1; bge- default; cmpwi r3,0; bge- case0; b default`, where `x >= 0` is
799
+ // `case 0` only because `x != 1` and `x < 1` came first (`corpus/mwcc-swdispatch.asm`).
800
+ //
801
+ // THE SIDES DIFFER, and each is read off its own compiler. Every jump in `emit_case_nodes` that
802
+ // lands on a case body is its test's BRANCH, while the fall-through always continues into more
803
+ // dispatch, and none of 3176 generated agbcc dispatches lands a body on a fall side — so agbcc
804
+ // is `'taken'`. mwcc lands one on either: `synthetic:sw_ret:mwcc_242_81` pins `case 3` on the
805
+ // FALL side (`cmpwi r3,4; bge- default; b case3` under `x > 2`, `x != 2`), so mwcc is
806
+ // `'either'`.
807
+ //
808
+ // A DEFAULT rather than a candidate variation because each declaring compiler's other producer
809
+ // of the same test is told apart by its LAYOUT. An if/else-if ladder written with relational
810
+ // tests pins values by its path just the same (`if (x >= 4) { if (x < 5) … } else if (x >= 3)
811
+ // …` pins 4 and 3) but puts a body between two tests, where the dispatch puts every test above
812
+ // every body — so this reading needs `switchRequiresFrontLoadedTests` beside it, and both
813
+ // declaring compilers declare that too (`corpus/mwcc-sw{dispatch,ladder,relladder,relnest}.asm`
814
+ // from `corpus/probe-mwcc-sw*.c`, regenerated by `scripts/regen-switch-spelling-probes.ts`). On
815
+ // agbcc the unsigned endpoint has no ladder at all: -O2 fold-const rewrites `x < 1u` into
816
+ // `cmp r0, #0 / bne` before codegen. All three builds PPC_MWCC serves emit the dispatch
817
+ // identically at -O4,p and at -O0,p (`packages/cli/test/matching/ppc-compiler-behaviors.test.ts`).
818
+ //
819
+ // Absent ⇒ every relational test navigates, and inheriting it would be wrong rather than merely
820
+ // unmeasured: on the MIPS lanes `sltiu rd, rs, 1` is the ordinary spelling of `!x`, and it lifts
821
+ // to `icmp_ult rs, 1` with no equality fold anywhere — the identical IR shape, from a producer
822
+ // that is not a dispatch. Each compiler opts in on its own dispatch's evidence.
823
+ switchBoundCase?: SwitchBoundCase;
288
824
  // Switch recovery: emit the case arms in the order the ASSEMBLY lays their bodies out, rather
289
825
  // than sorted by ascending case value. True claims the compiler emits case bodies as it walks
290
826
  // the arms and never MOVES one afterwards — neither reordering basic blocks nor scheduling
@@ -294,8 +830,14 @@ export interface TargetDescription {
294
830
  // neither sched.c nor reorg.c. SCOPE — SRCS does compile jump.c, whose cross-jump merges two
295
831
  // identical arm bodies into ONE block, so a merged pair's own order is gone from the asm; that
296
832
  // surfaces as two case values sharing a body, which switch-recover.ts ties by ascending value.
297
- // Absent ⇒ ascending case value, where ido/kmc-gcc/mwcc sit: each has a scheduler and none has
298
- // been put through that evidence. A compiler opts in on its own, never by inheriting.
833
+ // Absent ⇒ ascending case value, where ido/kmc-gcc/mwcc sit. ido and kmc-gcc have a scheduler
834
+ // and have not been put through that evidence. mwcc_242_81 HAS been, on two compiled pairs, and
835
+ // lays the bodies in source order: `synthetic:sw_defmid` with `default:` between two cases, and
836
+ // a three-case switch whose first case is a `beqlr`. What keeps it from declaring the reading is
837
+ // its FRONTEND: frontend/ppc.ts appends every conditional-return block after all the real ones,
838
+ // so such an arm sorts last whatever its address (switch-recover.ts, at `layoutIndex`). Placing
839
+ // those blocks at their branch's address is the prerequisite, and a declaration after it owes
840
+ // its own pairs. A compiler opts in on its own, never by inheriting.
299
841
  switchArmsFollowLayout?: boolean;
300
842
  // Switch recovery: DECLINE a comparison tree whose own layout INTERLEAVES a test block with a
301
843
  // case body, on the reading that the source wrote an if/else-if LADDER there. True claims the
@@ -321,8 +863,12 @@ export interface TargetDescription {
321
863
  // `corpus/probe-agbcc-sw{frontload,ladder}.c`, regenerated by
322
864
  // `scripts/regen-switch-spelling-probes.ts`, asserted in switch-arms.test.ts.
323
865
  //
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.
866
+ // mwcc declares it on its own objects: its `switch` dispatch puts every test above every body,
867
+ // and a ladder written with relational tests puts a body between two of them
868
+ // (`corpus/mwcc-sw{dispatch,relladder,relnest}.asm`, the note at `switchBoundCase`).
869
+ //
870
+ // Absent ⇒ every recoverable tree is still spelled `switch`, which is where ido sits: it has a
871
+ // scheduler that may move a body above a test, and it has not been put through the pair.
326
872
  // A compiler opts in on its own compiled evidence, never by inheriting — and where one
327
873
  // description serves two toolchains (`MIPS_GCC`), each toolchain owes its own pair, because the
328
874
  // field cannot distinguish them (the KEYED BY DESCRIPTION note at `spillSlotOrder`).
@@ -345,6 +891,34 @@ export interface TargetDescription {
345
891
  // and PPC lanes put the addend in the instruction by construction (`lui`/`%lo`, `lis`/`ori`),
346
892
  // so a surviving offset carries no information there. Absent ⇒ the row is never offered.
347
893
  foldsConstAddrOffset?: boolean;
894
+ // Does a STORE to a struct member past the ISA's displacement range compile differently from the
895
+ // same store through a cast of the base (`p->f38 = v` against `*((u8 *)p + 38) = v`)? agbcc's
896
+ // does: expand_assignment (gcc/expr.c) expands only the base of a COMPONENT_REF target (:2535)
897
+ // and evaluates the right-hand side first in store_field (:2624), and it expands a cast target
898
+ // whole (:2689) before the right-hand side (:2747). Measured on agbcc for a loaded, a constant, a
899
+ // computed and a parameter right-hand side: every pair builds two objects, and a hoisted
900
+ // `u8 *d = &p->f38; *d = v;` builds the cast's. True reads a moved store whose right-hand side
901
+ // is computed before its address as struct evidence (raise/structs.ts `memberStoreOrder`).
902
+ // ido, KMC, GCC 2.7.2 and the three mwcc builds put the two spellings in one object, a loaded
903
+ // right-hand side included, so they leave it absent. `/far-cast`, the lift that keeps such a
904
+ // store on its materialised address, is offered on every target and does not read this.
905
+ farMemberStoreOrder?: boolean;
906
+ // Does a member past the displacement range get its OWN materialised base at every access, so
907
+ // that one base read or written at two distinct offsets is a pointer the source held? Stated
908
+ // as the displacement `past` which that holds: a base materialised with a displacement whose
909
+ // magnitude exceeds it was built for one access. agbcc's is 0, every base: `p->f38 + p->f39`
910
+ // compiles to `add r0, #0x26; ldrb [r0]` and `add r1, #0x27; ldrb [r1]`, while
911
+ // `u8 *d = &p->f38; d[0] + d[1]` shares one `add r1, #0x26` between `ldrb [r1]` and
912
+ // `ldrb [r1, #1]`, and only the pointer spelling rebuilds those bytes. ido's is 32767, the
913
+ // `addiu` reach: inside it ido shares one `addiu at, a0, 32767` across adjacent members
914
+ // (`sb a1, 7232(at); sb a2, 7233(at)`), and past it the assembler expands every member access
915
+ // to its own `lui at, 3; addu at, at, a0` while a held `u8 *d = &p->b` (b at 200000) shares one
916
+ // `lui at, 3; addu v0, a0, at` between `sb t6, 3392(v0)` and `sw zero, 3396(v0)`. True refuses
917
+ // raise/structs.ts's displaced-access fold for such a base (`held-pointer-offsets`). The three
918
+ // mwcc builds share one `addis` base across members and a held pointer alike, and KMC and
919
+ // GCC 2.7.2 lift the held pair as members at the same bytes, so they leave it absent. Absent ⇒
920
+ // the fold reads a shared base as members.
921
+ farMemberBasePerAccess?: { past: number };
348
922
  // Does this compiler fold a pointer local's OWN ADVANCE back into the memory operand —
349
923
  // `*p = a; p = p + 1; *p = b;` → `strh [r3, #0]` + `strh [r3, #2]`, no `add` — so the advanced
350
924
  // spelling emits the indexed one's stores wherever the pointee is not volatile? agbcc does, on
@@ -357,6 +931,38 @@ export interface TargetDescription {
357
931
  // the plain variation ships, which is the conservative reading for a compiler whose pair nobody has
358
932
  // compiled — a compiler opts in on its own evidence and never by inheriting.
359
933
  foldsPointerAdvance?: boolean;
934
+ // Does this compiler build a word global read through a pointer conversion, or stored a pointer
935
+ // it converts implicitly, into the object of the bare integer spelling? agbcc does: under
936
+ // `extern u32 g;` the pointer spelling of a cell, `g = (void *)((u8 *)g + 4); … (gLimit ^ (u32)g)`,
937
+ // and the integer one, `g = g + 4; … (gLimit ^ g)`, are one object (the pair
938
+ // `TARGET_BEHAVIOR_READINGS` compiles), the implicit conversion costing a warning. True ⇒ the
939
+ // `int-cell` registry entry's target gate withholds that variation, which can only tie the
940
+ // default's bytes here. It does not tie the warning: under -Werror the pointer spelling does not
941
+ // build against the integer declaration and the integer one does, which no gate sees, since the
942
+ // witness and the bench compile without -Werror. IDO builds the pair into two objects and
943
+ // CodeWarrior rejects the pointer store, so both are offered. KMC gcc builds one object too and
944
+ // does not declare it: the matching suite compiles each witness on one compiler. Absent ⇒
945
+ // offered.
946
+ pointerIntConversionIsFree?: boolean;
947
+ // Does this compiler keep the constant addend of a POINTER sum's offset where the source put it,
948
+ // and move an INTEGER sum's out to the base? agbcc does: `(u8 *)((u32)g + ((a << 4) + 772))`
949
+ // computes `(g + 772) + (a << 4)` (fold-const.c:5033-5058, `A + (V + C)` → `(A + C) + V`), while
950
+ // `(u8 *)g + ((a << 4) + 772)` computes `g + ((a << 4) + 772)` (c-typeck.c:2663-2692 distributes
951
+ // the constant, fold-const.c:4997-5011 folds it back), which is what the project's own
952
+ // `g->arr[a]` compiles to. Compiled at each toolchain's canonical flags, with `g` a struct
953
+ // pointer, a `void *` and a `u32`, as a call argument and under a dereference: agbcc,
954
+ // gcc2.7.2kmc and gcc2.7.2 build the two spellings into two objects wherever the offset has a
955
+ // constant addend and into one where it has none; ido7.1 builds one object every time, and
956
+ // mwcc_242_81 is CodeWarrior's index-first order (structure/pointer-spelling.ts, THE INTEGER
957
+ // SUM). Two rules read it. True ⇒ the arithmetic table's value row (ARITH_ROWS) spells an
958
+ // undeclared pointer global plus an integer with a constant term as the pointer sum where the
959
+ // sum is a value rather than an access's address; and the structurer declares `u8 *` a temp
960
+ // holding a pointer global's value that is the base of such a sum (`declaresBytePointer`),
961
+ // which an integer temp would compile as `(t + K) + x`. Through a temp ido7.1 does build two
962
+ // objects, the byte pointer's index first where the integer temp and the struct source keep the
963
+ // base first, and mwcc_242_81 builds one (the cli matching suite's pointer-spelling.test.ts).
964
+ // Absent ⇒ the integer sum and the integer temp.
965
+ keepsPointerSumAddend?: boolean;
360
966
  // Does this compiler EMIT a memory read in the block the source SPELLED it in? One direction
361
967
  // only: the def-block placement rule (StructureOptions.readsStayWhereWritten) re-spells a read
362
968
  // at the block the asm performed it in, which reproduces the asm iff nothing sinks a spelled
@@ -395,14 +1001,230 @@ export interface TargetDescription {
395
1001
  // three descriptions that measured false (four compilers) set it anyway, so absent means
396
1002
  // UNMEASURED rather than "no".
397
1003
  reloadsLocalReread?: boolean;
1004
+ // Does this compiler convert a constant to a NARROW lvalue's type before it materializes it
1005
+ // for the store? Then the stored register's upper bits say the lvalue's signedness whenever
1006
+ // the narrow sign bit is set: `u16 *p; p[6] = -1;` loads 0xffff and `s16 *p; p[6] = 0xFFFF;`
1007
+ // loads -1. Measured on every bench compiler at its rows' flags by lifting the probe in
1008
+ // packages/cli/test/matching/narrow-store-constant.test.ts and reading the const each store
1009
+ // takes. Read by raise/storesign.ts (through raise/pre-recovery.ts).
1010
+ //
1011
+ // ABSENT ⇒ the seed stands down and a narrow store keeps the unsigned view; a compiler opts IN
1012
+ // on its own probe.
1013
+ narrowStoreConstantTakesLvalueType?: boolean;
1014
+ // A LAYOUT fact rather than a canonicalization one, kept here because it is the COMPILER's:
1015
+ // the size, in bytes, it aligns and rounds EVERY struct and union to, whatever its members.
1016
+ // agbcc: 4 — `sizeof(struct { u16 h; })` is 4, and `struct { u8 a; union { u16 h; u8 b[2]; } u; }`
1017
+ // seats `u` at 4 (gcc 2.9's arm STRUCTURE_SIZE_BOUNDARY). ido7.1, gcc2.7.2kmc and mwcc_242_81
1018
+ // answer 2 and 2: the natural layout, 1. Compiled as `return sizeof …` / `return (int)&((T *)0)->m`
1019
+ // at each row's flags; gcc2.7.2 (-O1), mwcc_233_163n and mwcc_247_107 answer the same as their
1020
+ // descriptions' measured compilers. Read by raise/structs.ts (through raise/pre-recovery.ts),
1021
+ // which sizes a recovered UNION member with it, and by raise/struct-arrays.ts, which declines
1022
+ // an element struct whose stride it would round (agbcc: a stride-6 `u16 [3]` row is 8 bytes).
1023
+ //
1024
+ // ABSENT ⇒ unmeasured: the union recovery declines every union narrower than a word, and the
1025
+ // struct-array recovery declines every element struct. No value is safe to assume, since one
1026
+ // too small mislays the fields after it on agbcc (and mis-strides an element struct) and one
1027
+ // too large drops the pad in front of them everywhere else.
1028
+ aggregateBoundary?: number;
1029
+ // The largest alignment any member of a struct takes, in bytes (BIGGEST_ALIGNMENT): agbcc 4,
1030
+ // thumb.h:358, so a `long long` member sits at a word. With `aggregateBoundary` it is what sizes
1031
+ // a declared aggregate (`aggregate.ts`). ABSENT ⇒ unmeasured, and no aggregate is sized.
1032
+ largestAlignment?: number;
1033
+ // HOW A STRUCT OR UNION RETURNED BY VALUE COMES BACK, which decides whether a call to a function
1034
+ // declared to return one hands it a hidden pointer as argument 0 and moves every declared
1035
+ // argument one register up (`aggregate.ts` `returnsInMemory`, read by the call lowering every
1036
+ // frontend shares, frontend/call-plan.ts).
1037
+ // • 'apcs' — in memory when bigger than a word, when a struct has a second member that is not
1038
+ // a bitfield, or when a union has a member that would be; in the return register otherwise.
1039
+ // agbcc, thumb.c:1423-1493 (compiled: `{u8 a,b,c,d}` through memory; `{u32}`, `{u32 w[1]}`,
1040
+ // `{u32 a:8; u32 b:8;}` and `union {u32; u16;}` in r0).
1041
+ // • 'svr4' — in r3, or r3:r4, when it is 8 bytes or less, whatever its members (a float's
1042
+ // included); in memory otherwise. mwcc_242_81, mwcc_233_163n and mwcc_247_107 alike
1043
+ // (compiled: 1 to 8 bytes, `{float}`, `{double}` and a union come back in r3/r3:r4; a 9- and a
1044
+ // 12-byte struct are handed `addi r3,…` ahead of the call).
1045
+ // • 'memory' — always in memory, whatever its size or members. ido7.1, gcc2.7.2kmc and gcc2.7.2
1046
+ // alike (compiled: `{u8}`, `{u16}`, `{u32}`, `{float}`, `{double}`, 4-, 8- and 12-byte
1047
+ // structs, `union {u32; u16;}` and `{u32 a:8; u32 b:8;}` are each handed `addiu a0,sp,N`
1048
+ // ahead of the `jal`, with the declared arguments from a1 on, and the callee returns that
1049
+ // pointer, `move v0,a0`).
1050
+ // Under every rule the caller's half is lowered (frontend/call-plan.ts), and a function that
1051
+ // itself returns a struct through memory lifts as that struct where its declaration says so and
1052
+ // the lift proves the hidden pointer is only its storage (frontend/own-struct-return.ts); it is
1053
+ // read as the pointer function it compiles to where nothing declares it, or where a context's
1054
+ // declaration does and the proof fails (pipeline.ts `returnOwnStruct`).
1055
+ // ABSENT ⇒ unmeasured, and such a call declines.
1056
+ aggregateReturn?: 'apcs' | 'svr4' | 'memory';
1057
+ // WHETHER A FUNCTION THAT RETURNS A STRUCT THROUGH MEMORY HANDS THE HIDDEN POINTER BACK in the
1058
+ // return register — a COMPILER fact, which no rule above implies. `true` for ido7.1, gcc2.7.2kmc
1059
+ // and gcc2.7.2 (`move v0,a0`, the O32 rule) and for agbcc (`add r0, r6, #0` after a call, r0
1060
+ // left alone in a leaf: compiled `struct S12 f(s32 x)` with and without a call to `fill(t)`).
1061
+ // `false` for mwcc_242_81, mwcc_233_163n and mwcc_247_107, whose callee clobbers r3 before the
1062
+ // `blr`: the same `struct S12 b4(s32 x)` saves the pointer in r30/r31, stores through it, and
1063
+ // returns with r3 = a reload of a frame word. It decides what the proof that a function's own
1064
+ // struct return goes through that pointer asks of each `ret` (frontend/own-struct-return.ts):
1065
+ // where it is `true` every `ret` must hand back the pointer, and where it is `false` what the
1066
+ // return register holds is no part of the function, save a value computed for the return alone
1067
+ // (`returnedAlone`). ABSENT ⇒ unmeasured, and read as `true`, which only refuses more.
1068
+ structReturnHandsBackPointer?: boolean;
1069
+ // WHETHER A CALLER HANDS ITS OWN DESTINATION POINTER STRAIGHT ON as a callee's hidden
1070
+ // struct-return pointer, leaving the first argument register at its incoming value below the
1071
+ // arguments it sets up. `true` for ido7.1: `int b3(struct S *p){ *p = sg(1); return 0; }` is
1072
+ // `jal sg; li a1,1` with a0 never touched, the bytes of `sg(p, 1)` with its result unused.
1073
+ // `false` for gcc2.7.2kmc, gcc2.7.2, agbcc and mwcc_242_81, mwcc_233_163n and mwcc_247_107,
1074
+ // which copy through a frame temporary (a0/r0/r3 set to a frame address ahead of the call).
1075
+ // Compiled at the canonical flags, `b3` and `void b6(struct S *p, int k){ *p = sg(k, 5); }`.
1076
+ // Read by the guessed-arity gap (frontend/call-plan.ts `decideGap`): where it holds, a gap at
1077
+ // the first argument register is decided only where the call's result is read, which no
1078
+ // forwarded struct return's is (IDO reloads `p` from its home after the call in all five shapes
1079
+ // compiled: `return (*p = sg(1)).b`, `return p` and three more). ABSENT ⇒ unmeasured, and read
1080
+ // as `true`, which only refuses more.
1081
+ forwardsStructReturnDestination?: boolean;
1082
+ // A STRUCT PASSED BY VALUE TRAVELS IN CONSECUTIVE ARGUMENT WORDS, the first at the next free
1083
+ // argument word, its size rounded up to words (`aggregate.ts` `byValueCallArgs`). agbcc,
1084
+ // thumb.h:632-650 (FUNCTION_ARG places by word offset, FUNCTION_ARG_PARTIAL_NREGS splits a
1085
+ // BLKmode argument across r3 and the stack, FUNCTION_ARG_ADVANCE rounds its size to a word, and
1086
+ // no FUNCTION_ARG_PASS_BY_REFERENCE is defined); compiled, `struct A {u16 a,b,c,d; s8 e,f;}`
1087
+ // is `ldr r0..r2` off the frame copy ahead of `bl take2`. O32 too, compiled on ido7.1,
1088
+ // gcc2.7.2kmc and gcc2.7.2: `takeQ(s32 k, struct Q {u32 a, b;})` is passed `a1:a2` and a
1089
+ // three-word struct `a0..a2` (an 8-aligned one starts at an even word:
1090
+ // `evenWordAlignedArgs`); IDO also stores each word into its home slot, which the
1091
+ // call owns. ABSENT ⇒ the target passes one some other way or states nothing: mwcc passes the
1092
+ // address of a copy (frontend/ppc.ts refuses it).
1093
+ byValueAggregateArgWords?: boolean;
1094
+ // The bytes an enum takes as a struct member (`aggregate.ts`). agbcc: 4 — flag_short_enums is 0
1095
+ // unless `-fshort-enums` is given (toplev.c:3552-3554 with no DEFAULT_SHORT_ENUMS), so an enum
1096
+ // whose values fit an int is an int (c-decl.c:6123-6135); compiled, `sizeof(enum {K0, K1})`
1097
+ // and `enum {B0 = 300}` are both 4. `targetFor` drops it under `-fshort-enums`, and an enum
1098
+ // declared with an attribute (`packed`), or with a value an int cannot hold, is not sized
1099
+ // (proto-context.ts). ABSENT ⇒ unmeasured,
1100
+ // and an aggregate with an enum member is not sized.
1101
+ enumBytes?: number;
1102
+ // How a struct's bitfields are placed (`aggregate.ts`). 'contiguous' — each at the bit after the
1103
+ // one before, straddling a byte or a word of its declared type, with the next member that is not
1104
+ // a bitfield at the first byte past it that its alignment allows. agbcc: thumb.h defines no
1105
+ // PCC_BITFIELD_TYPE_MATTERS, so stor-layout.c:404-405 lays a bitfield at bit alignment and
1106
+ // skips the no-straddle rule of :462-481 (compiled: `{u32 a:20; u32 b:20; u8 c;}` puts `c` at
1107
+ // 5, `{u32 a:31; u32 b:31; u32 c:2;}` is 8 bytes, `{u8 a:5; u8 b:5; u8 c;}` puts `c` at 2). A
1108
+ // zero-width bitfield, which moves the next member to EMPTY_FIELD_BOUNDARY, is not placed.
1109
+ // ABSENT ⇒ unmeasured, and an aggregate with a bitfield is not sized.
1110
+ bitfieldPacking?: 'contiguous';
1111
+ // Can this compiler CONTRACT a float multiply and the add or subtract that reads it into one
1112
+ // fused instruction, which rounds once? mwcc can: `-fp_contract on` (set on some pikmin,
1113
+ // marioparty4 and ac-decomp units) turns `a * b + c` into `fmadds` and `-(a * b) + c` into
1114
+ // `fnmsubs`, but only within one expression, so the structurer names every such product
1115
+ // (StructureOptions.contractsFloatProducts) and the spelling compiles to the unfused pair under
1116
+ // either setting. The MIPS targets cannot: MIPS II (ido7.1 `-mips2`) and MIPS III (kmc `-mips3`)
1117
+ // have no fused multiply-add, and naming the product there only moves the register allocation
1118
+ // off the object (`fpu-lift.test.ts` pins one on ido7.1).
1119
+ //
1120
+ // ABSENT ⇒ false. A compiler with a fused multiply-add must opt in; the no-FPU targets never
1121
+ // compute on a float at all.
1122
+ contractsFloatProducts?: boolean;
1123
+ // Does this compiler read a double literal's shortest round-trip decimal (`ir/float-bits.ts`
1124
+ // `doubleLiteral`) back as the same double? That is how an `fconst` is printed, so its
1125
+ // producers run only where this is true. agbcc does: c-lex.c:1308 hands the token to
1126
+ // REAL_VALUE_ATOF at DFmode, which is real.c:461 `ereal_atof` → `asctoe53` (:3512) →
1127
+ // `asctoeg(s, y, 53)` (:3533), a conversion in extended precision rounded once to 53 bits.
1128
+ //
1129
+ // ABSENT ⇒ unmeasured, and a literal declines.
1130
+ roundTripsDoubleLiterals?: boolean;
1131
+ // How the compiler lays out a FUNCTION-SCOPE STATIC: the least alignment of an array, the
1132
+ // alignment of a string-literal initializer, and whether a zero scalar keeps its `.data`. The
1133
+ // bytes of a static do not say how the source declared it; these, with the alignment and
1134
+ // section the target shows, say which declaration lands it there (structure/local-statics.ts).
1135
+ // A LAYOUT fact like `aggregateBoundary`, and the compiler's, read by the structurer.
1136
+ //
1137
+ // ABSENT ⇒ unmeasured, and a function that defines a static declines: a wrong floor mislays
1138
+ // every static after the first, which no score sees.
1139
+ staticLayout?: StaticLayout;
1140
+ // Does this compiler load a `volatile` narrow signed read zero-extended and sign-extend it in a
1141
+ // register, never in the load itself? agbcc: yes — `*(volatile s16 *)a` is `ldrh; lsl #16; asr
1142
+ // #16` and `volatile s8` is `ldrb; lsl; asr`, where the plain `s16` read is `ldrsh`: the
1143
+ // sign-extend expander refuses a volatile MEM while expanding (`general_operand`, recog.c:918,
1144
+ // under `init_recog_no_volatile`, function.c:5564) and thumb.md:393-409 then extends in a
1145
+ // register; compiled at the canonical flags. So a lifted sign-extending narrow load is evidence
1146
+ // the source read was plain (`readCouldBeVolatile`): the device pin (frontend/device-pins.ts)
1147
+ // leaves it plain, and the `declared` stamp (raise/declared-volatile.ts) leaves it unplaced.
1148
+ //
1149
+ // ABSENT ⇒ false: a sign-extending load says nothing about the qualifier, and a device read is
1150
+ // pinned, and a read of a declared object placed, whatever its extension.
1151
+ volatileReadsExtendInRegister?: boolean;
1152
+ // Does this compiler, AT THESE FLAGS, keep a plain read whose value nothing uses? A FLAG fact,
1153
+ // never written in a description: `targetFor` sets it from the optimisation level
1154
+ // (`keepsUnusedReadsAt`), so a target at other flags has it absent. Where it holds, a read
1155
+ // the lift finds unused is no evidence of `volatile`, and the dead-read pin
1156
+ // (frontend/dead-reads.ts) stamps only one the machine loaded into the register the compiler
1157
+ // discards a volatile read into (`discardsUnusedReadsInto`).
1158
+ //
1159
+ // ABSENT ⇒ false: the compiler deletes every plain read nobody uses, so one still in the target
1160
+ // was qualified.
1161
+ keepsUnusedReads?: boolean;
1162
+ // WHERE DOES THIS COMPILER PUT A READ NOBODY USES, at a level that keeps a plain one
1163
+ // (`keepsUnusedReads`)? The register a `volatile` read with no consumer is loaded into, and no
1164
+ // plain local is ever homed in. COMPILED at the keeping levels, one statement per line:
1165
+ // `*p;`, `s->b;`, `gV;` and `(*p, 1)` through a volatile lvalue load into `r0` on mwcc 2.4.2,
1166
+ // 2.4.7 and 2.3.3 at `-O0,p`, and into `$zero` on IDO 7.1 at `-O1` and `-O0`; the plain
1167
+ // `x = p[5]; x = 0;` takes a home there instead (`lwz r31`, `lw t6` and a frame store), and so
1168
+ // does a `register` local (`lwz r31`, `lw a2`). Over the 409 complete Mario Party 4 units built
1169
+ // at `-O0,p`, 924 non-frame loads nothing reads land in r17-r31 and f18-f31, and exactly one in
1170
+ // r0: WipeExecAlways's `wipe->type;`, a `volatile u8` member. So a dead load into this
1171
+ // register is a qualified read, and the dead-read pin (frontend/dead-reads.ts) stamps it at a
1172
+ // keeping level too. mwcc keeps one plain read in r0 as well, a read under an arithmetic or a
1173
+ // cast nothing uses (`p[5] + 1;`, `(char)p[5];`), which no real unit holds: that is the pin's
1174
+ // residue. A load this register took and an op then wrote to a home (`lwz r0` + `extsb r31,r0`,
1175
+ // a plain `s8` local) is not discarded (frontend/dead-reads.ts `leftInDiscardRegister`). One
1176
+ // register cannot name a pair: IDO discards a doubleword into zero:at, `lw zero,0(a0)` +
1177
+ // `lw at,4(a0)`, which frontend/mips.ts `emitLoad` reads as the pair this register heads.
1178
+ //
1179
+ // ABSENT ⇒ the compiler has no such register, or it is unmeasured. agbcc, gcc 2.7.2 and KMC
1180
+ // gcc 2.7.2 at `-O0` have none: a volatile `*p;` is an `ldr r0`/`lw v0` nothing stores, the
1181
+ // same instructions as a `register` local assigned and overwritten, so the pin cannot tell the
1182
+ // two apart there and refuses rather than drop the read.
1183
+ discardsUnusedReadsInto?: string;
1184
+ // MAY A DISCARDED READ LAND IN AN ARGUMENT REGISTER, at flags that delete a plain read nobody
1185
+ // uses? Where it may, a value a load left in an argument register is no proof that the next
1186
+ // call takes it. The discard register is the allocator's first free one, not a fixed register,
1187
+ // so a single read on its own says nothing: the probe that decides is one where the usual
1188
+ // discard register is busy. `true` for agbcc, which loads a discarded `volatile` read into the
1189
+ // lowest free argument register (`*(volatile int *)K; g(s)` is `ldr r1,[r1]; bl g`, the bytes of
1190
+ // `g(s, *(int *)K)`). `true` for gcc2.7.2kmc and gcc2.7.2, which discard into v0/v1 until those
1191
+ // are busy and then into a1: `int a = ga * 3; *(volatile int *)&gx; *(volatile int *)&gx;
1192
+ // ga = a + 1; return logv(s) + 1;` is `lw a1,0(v0); lw v0,0(v0); ... jal logv` (`lw a1,0(v1)` on
1193
+ // gcc2.7.2), the bytes of the twin that passes the first read. `true` for mwcc_242_81,
1194
+ // mwcc_233_163n and mwcc_247_107, which discard into r0 until it is busy and then into r4: the
1195
+ // same function's first read is `lwz r4,gx` on all three, and with three reads mwcc_233_163n's
1196
+ // last one is `lwz r4,gx` right before `bl logv`, byte-identical to the twin that passes it.
1197
+ // `false` for ido7.1 alone: `lw zero`, `lbu zero`, `lh zero`, and `lw zero` + `lw at` for a
1198
+ // doubleword, with four discarded reads stacked under the same pressure too. Compiled at the
1199
+ // canonical flags. Read by the guessed-arity gap (frontend/call-plan.ts `decideGap`), which a
1200
+ // load decides only where this is `false`. ABSENT ⇒ unmeasured, and read as `true`, which only
1201
+ // refuses more.
1202
+ discardedReadMayFillArgRegister?: boolean;
398
1203
  };
399
1204
  }
400
1205
 
1206
+ /** THE SPELLINGS ONE REGISTER HAS in an ISA's asm, as `[number, alias]` pairs — the one place they
1207
+ * are stated. A frontend keys registers by spelling, so a function that names one register two
1208
+ * ways is rewritten to one of them first (frontend/ssa.ts `unifyRegisterSpellings`, which both
1209
+ * frontends hand these tables); and every register list a target states names both spellings of a
1210
+ * pair or neither, which `target-register-aliases.test.ts` checks against these tables. A pair
1211
+ * missing here splits one register into two partition keys: saved as `sl` and written as `r10`, its
1212
+ * writes read as a global's the epilogue undoes. */
1213
+ export const ARM_REGISTER_ALIASES: readonly (readonly [string, string])[] = [
1214
+ ['r9', 'sb'],
1215
+ ['r10', 'sl'],
1216
+ ['r11', 'fp'],
1217
+ ['r12', 'ip'],
1218
+ ];
1219
+ /** O32's `$30`, which objdump prints `s8` and hand-written asm also writes `fp` (`ARM_REGISTER_ALIASES`). */
1220
+ export const O32_REGISTER_ALIASES: readonly (readonly [string, string])[] = [['s8', 'fp']];
1221
+
401
1222
  export const ARMV4T_AGBCC: TargetDescription = {
402
1223
  id: 'armv4t',
403
1224
  compiler: 'agbcc',
404
1225
  argRegs: ['r0', 'r1', 'r2', 'r3'],
405
1226
  returnReg: 'r0',
1227
+ doubleArgWords: 'high-first',
406
1228
  // AAPCS passes four in r0-r3, so nothing above them can be an argument. The ATPCS aliases are
407
1229
  // the spellings this ISA's asm actually uses: censused over the vendored ARM asm, `sb`/`sl`/`ip`/
408
1230
  // `fp` all occur as operands and no `v<n>`/`a<n>` form does. `sp`, `lr` and `pc` are deliberately
@@ -413,6 +1235,23 @@ export const ARMV4T_AGBCC: TargetDescription = {
413
1235
  // `mov ip, r1` in two switch arms, no save anywhere, and a `mov r0, ip` past a third arm that
414
1236
  // writes nothing — an uninitialised local by construction.
415
1237
  scratchRegs: ['r12', 'ip'],
1238
+ // MEASURED at the canonical flags, `register int g __asm__("rN")` for each of r4-r11: `f(){ return
1239
+ // g; }` is `add r0, r4, #0` / `mov r0, r8` / `mov r0, sl`, and `s(a){ g = a; }` the reverse, with no
1240
+ // save. agbcc prints r9 as `r9`, r10 as `sl` and r11 as `fp`; hand-written asm also writes `sb`.
1241
+ globalRegisterVariables: {
1242
+ regs: {
1243
+ ...Object.fromEntries(['r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10', 'r11'].map((r) => [r, r])),
1244
+ sb: 'r9',
1245
+ sl: 'r10',
1246
+ fp: 'r11',
1247
+ },
1248
+ },
1249
+ // AAPCS: r0-r3 pass arguments and return, r12 (`ip`) is the intra-procedure-call scratch, and lr
1250
+ // holds the return address the `bl` itself overwrites. agbcc's own machine description says the
1251
+ // same (`thumb.h` CALL_USED_REGISTERS). Both spellings of r12 for the reason `nonArgRegs` carries
1252
+ // both: the ATPCS aliases are what this ISA's asm writes.
1253
+ callerSaved: ['r0', 'r1', 'r2', 'r3', 'r12', 'ip', 'lr'],
1254
+ runtimeHelpers: AGBCC_RUNTIME_HELPERS,
416
1255
  // GBA hardware, which this target implies: agbcc is the GBA compiler and this is the only
417
1256
  // armv4t entry, so `armv4t + agbcc` is the platform. Stated because nothing else states it.
418
1257
  capabilities: {
@@ -430,6 +1269,15 @@ export const ARMV4T_AGBCC: TargetDescription = {
430
1269
  // The idiom this exists for is their `DMA_FILL`: `vu16 tmp = value;
431
1270
  // DmaSet(n, &tmp, dest, … DMA_SRC_FIXED …)`, where the frame local is the source.
432
1271
  readOnlyAddressSinks: [0x040000b0, 0x040000bc, 0x040000c8, 0x040000d4],
1272
+ // DMAnCNT_H, 10 bytes above each SAD: Source Address Control is bits 7-8 (setting 3 is
1273
+ // prohibited) and bit 10 selects 32-bit units. `DMA_FILL`'s `DMA_SRC_FIXED` is setting 2.
1274
+ readSourceControl: {
1275
+ offset: 0xa,
1276
+ modeShift: 7,
1277
+ modes: ['increment', 'decrement', 'fixed', null],
1278
+ wideBit: 0x0400,
1279
+ units: [2, 4],
1280
+ },
433
1281
  // The GBA I/O register file — one page from 0x04000000, the last live register being
434
1282
  // 0x04000301 (HALTCNT). Everything a source reaches through `REG_*` is in here, and nothing
435
1283
  // else is: IWRAM, EWRAM, palette, VRAM and OAM are ordinary memory a source does not qualify.
@@ -443,21 +1291,79 @@ export const ARMV4T_AGBCC: TargetDescription = {
443
1291
  [0x040000d2, 0x040000d4],
444
1292
  [0x040000de, 0x040000e0],
445
1293
  ],
1294
+ // The BIOS block transfers, SWI 0Bh CpuSet and SWI 0Ch CpuFastSet (GBATEK, "BIOS Memory
1295
+ // Copy"): r0 the source, r1 the destination, r2 the control — a count in bits 0-20, bit 24 a
1296
+ // FIXED source (a fill, reading one unit of r0), and for CpuSet bit 26 selecting 32-bit units
1297
+ // over 16-bit ones; CpuFastSet moves words only, its count rounded up to eight. Neither writes
1298
+ // through r0. The names are the SDK's: sa3's and pokeemerald's `libagbsyscall.s` bind
1299
+ // `CpuSet: svc #11` and `CpuFastSet: svc #12`, kleod's `CpuSet: svc #0xb`, and each declares
1300
+ // `void CpuSet(const void *src, void *dest, u32 control)`.
1301
+ blockTransferCalls: {
1302
+ CpuSet: {
1303
+ source: 0,
1304
+ control: 2,
1305
+ countMask: 0x1fffff,
1306
+ fixedBit: 0x01000000,
1307
+ wideBit: 0x04000000,
1308
+ units: [2, 4],
1309
+ countGranule: 1,
1310
+ },
1311
+ CpuFastSet: {
1312
+ source: 0,
1313
+ control: 2,
1314
+ countMask: 0x1fffff,
1315
+ fixedBit: 0x01000000,
1316
+ wideBit: 0,
1317
+ units: [4, 4],
1318
+ countGranule: 8,
1319
+ },
1320
+ },
446
1321
  },
447
1322
  compilerBehaviors: {
448
1323
  coalesceLoopInit: false,
449
1324
  preserveDivergentBranchSense: true,
450
1325
  orderArgCopiesByWriteOrder: true,
451
1326
  nearBaseSpan: 255,
1327
+ volatileReadsExtendInRegister: true,
452
1328
  foldsConstAddrOffset: true,
1329
+ farMemberStoreOrder: true,
1330
+ farMemberBasePerAccess: { past: 0 },
453
1331
  foldsPointerAdvance: true,
1332
+ pointerIntConversionIsFree: true,
1333
+ keepsPointerSumAddend: true,
454
1334
  readsStayWhereWritten: true,
455
- switchAllowsBoundCase: true,
1335
+ switchBoundCase: 'taken',
456
1336
  switchArmsFollowLayout: true,
457
1337
  switchRequiresFrontLoadedTests: true,
458
1338
  hoistsSingleSetArm: true,
1339
+ eightByteReturnScratch: 'r2',
459
1340
  arrayShapeFromStride: true,
460
1341
  reloadsLocalReread: true,
1342
+ // MEASURED: the probe at the field. A signed constant often lifts as `mov #1; neg` or
1343
+ // `mov #0x80; lsl #8`, a `neg`/`shl` the seed does not read. agbcc also materializes one
1344
+ // constant ONCE for several narrow stores through the same base, whatever their signedness:
1345
+ // `struct { s32 a; s16 b; u16 c; } *p; p->b = -300; p->c = -300;` is one `ldr r1,=0xfed4` for
1346
+ // both `strh`. A stamp here then names one store's type, not necessarily each's (the `s16`
1347
+ // store is stamped unsigned, which is also the convention).
1348
+ narrowStoreConstantTakesLvalueType: true,
1349
+ aggregateBoundary: 4,
1350
+ largestAlignment: 4,
1351
+ aggregateReturn: 'apcs',
1352
+ structReturnHandsBackPointer: true,
1353
+ // MEASURED: the probes at the fields.
1354
+ forwardsStructReturnDestination: false,
1355
+ discardedReadMayFillArgRegister: true,
1356
+ byValueAggregateArgWords: true,
1357
+ enumBytes: 4,
1358
+ bitfieldPacking: 'contiguous',
1359
+ roundTripsDoubleLiterals: true,
1360
+ // agbcc 2.9 (gcc/varasm.c `assemble_variable`, gcc/thumb.h): an array takes its element's
1361
+ // alignment (no DATA_ALIGNMENT); a declaration initialized by a STRING_CST is word-aligned —
1362
+ // CONSTANT_ALIGNMENT (thumb.h:361) over DECL_INITIAL (varasm.c:1214-1216), so
1363
+ // `static const char s[] = "hi"` is at 4 while `= {'h', 'i', 0}` and an array of strings
1364
+ // (whose DECL_INITIAL is a CONSTRUCTOR) are at 1; and `= 0` keeps its `.data`, where only a
1365
+ // declaration with no initializer is `.lcomm` (compiled).
1366
+ staticLayout: { aggregateAlign: 1, stringAlign: 4, zeroScalar: 'data' },
461
1367
  narrowParamWitness: 'prologue-extension',
462
1368
  // agbcc: reload walks pseudos ascending handing each global-alloc loser a fresh slot, a user
463
1369
  // local's pseudo number is its `expand_decl` position, and the Thumb frame grows UPWARD
@@ -466,13 +1372,97 @@ export const ARMV4T_AGBCC: TargetDescription = {
466
1372
  // operand rows and nothing else, from two locals declared the other way round) and its
467
1373
  // control `synthetic:spillorder_rev` (the same body in the order asmlift already emits, which
468
1374
  // must stay a MATCH), plus `synthetic:dma_fill_uninit`, a row this did not author.
1375
+ designatedInitializers: true, // agbcc compiles `{ 1, [8] = 2 }` (see the field)
469
1376
  spillSlotOrder: 'ascending',
1377
+ addressTakenArrayOrder: 'ascending', // MEASURED, the probe at the field
1378
+ addressTakenArrayAlignment: 4, // MEASURED, the probe at the field
470
1379
  // agbcc reserves the outgoing area with the rest of the frame (`add sp, sp, #-N` covers both)
471
1380
  // and stages arguments 5+ into it at [sp,#0] upward — thumb.h's ACCUMULATE_OUTGOING_ARGS.
472
1381
  stagesOutgoingArgsInFrame: true,
1382
+ // …one area for every call, below the locals (thumb.h:573/600-622/628, calls.c:1675), which
1383
+ // agbcc re-stages before each call and never reads back after one (agbcc-restage.s).
1384
+ localsAboveOutgoingArea: true,
1385
+ // …nor loads at all, before a call or after: no REG_PARM_STACK_SPACE (see the field).
1386
+ outgoingAreaNeverLoadedFrom: 0,
1387
+ // the two producer tables behind this are compiled, at agbcc 2.9-arm-000512 and the rows' own
1388
+ // flags, and they are written out where the predicate reads it (`frontend/thumb.ts`)
1389
+ oneWordFrameIsTheCapturedObject: true,
1390
+ // compiled: the committed probe `test/corpus/agbcc-escape.s` (`scripts/regen-outgoing-area-probes.ts`)
1391
+ escapedFrameAddressNamesALocal: true,
1392
+ // agbcc's `*call_indirect` and `*call_value_indirect` emit `bl _call_via_%0` (gcc/thumb.md:997-1021),
1393
+ // and libgcc defines `_call_via_<reg>` as `bx <reg>` for r0-r9, sl, fp, ip, sp and lr
1394
+ // (libgcc/lib1thumb.asm:595-633). Neither `sp` (the frame) nor `lr` (which the `bl` itself
1395
+ // overwrites) holds a function's address.
1396
+ callThunks: {
1397
+ prefix: '_call_via_',
1398
+ regs: ['r0', 'r1', 'r2', 'r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'sl', 'fp', 'ip'],
1399
+ },
1400
+ // agbcc spells a branch past the ±2 KB reach of Thumb `b` as `bl <label>` with this comment,
1401
+ // from three patterns: `jump` (`bl %l0\t@ far jump`, gcc/thumb.md:837), and the relays of
1402
+ // `*cond_branch` (`.LCB`, :930) and `*cond_branch_reversed` (`.LCBR`, :957), both
1403
+ // `bl %l0\t@far jump`. thumb.c `far_jump_used_p` (:715, read at :785 and :1065) makes it push lr
1404
+ // in any function that has one. Only the WHOLE comment is the mark: a longer comment that
1405
+ // mentions a far jump is not the compiler's.
1406
+ farJumpRelay: { comment: /^@\s*far jump\s*$/ },
473
1407
  },
474
1408
  };
475
1409
 
1410
+ /** MIPS o32's floating-point homes, shared by both compilers that target it (see `fpu`). The odd
1411
+ * halves (`$f13`, `$f15`) carry the other word of a DOUBLE argument, which no frontend lifts. */
1412
+ const O32_FPU: TargetDescription['fpu'] = { argRegs: ['$f12', '$f14'], returnReg: '$f0', slots: 'leading' };
1413
+
1414
+ /** O32's register partition, shared by every compiler that targets it (`nonArgRegs` and
1415
+ * `scratchRegs`). Every general register but the four argument registers and the ones that are
1416
+ * never a local's home (`zero`, `k0`/`k1`, `gp`, `sp`, `ra`). `s8` and `fp` are the two spellings
1417
+ * of $30, listed both as `ARMV4T_AGBCC` lists `r12` and `ip`: objdump prints `s8`, and a spelling
1418
+ * nobody listed keeps its existing treatment. */
1419
+ const O32_NON_ARG_REGS = [
1420
+ 'at',
1421
+ 'v0',
1422
+ 'v1',
1423
+ ...Array.from({ length: 10 }, (_, i) => `t${i}`),
1424
+ ...Array.from({ length: 8 }, (_, i) => `s${i}`),
1425
+ 's8',
1426
+ 'fp',
1427
+ ] as const;
1428
+ /** Of those, the ones O32 lets a callee use with no save — `at`, `v0`/`v1` and `t0`..`t9`, the
1429
+ * caller-saved registers that pass no argument. MEASURED on GCC_KMC_TOOLCHAIN and the stock gcc
1430
+ * 2.7.2 toolchain at their synthetic flags: a leaf homes an uninitialised local in v0
1431
+ * (`uninit_join`: `bgtzl a0,…; sll v0,a0,1; jr ra; ori v0,v0,1`) and in v1 (`uninit_sw`).
1432
+ *
1433
+ * Every one of them is also a `hiddenInputRegs` entry, because O32's hand-written asm hands values
1434
+ * over in them: af's famicom_emu routines are handed a value in t0 or v1 (`sw t0,6348(s8)` is
1435
+ * its first instruction), and a label inside a function starts with its registers live.
1436
+ * MEASURED over every function and label symbol of the vendored snowboardkids2 (KMC),
1437
+ * marioparty3 (gcc 2.7.2) and af (IDO) objects, 29,399 in all, the refusal below fires on 17 —
1438
+ * twelve labels, three hand-written routines (two of famicom_emu's and libkmc's `div64_64_1`) and
1439
+ * two data symbols disassembled as code — and on no compiled function. A
1440
+ * compiled local is assigned on some path, so its entry value meets that assignment at a join;
1441
+ * `uninitfirst` also reads one before the switch that assigns it, which a join allows. The join
1442
+ * must take a definition the entry value does not feed, because an input carried round a loop
1443
+ * (`addiu t0,t0,-1; bnez t0,top`) meets only its own next value at the loop header. famicom_emu's
1444
+ * hand-written Yaz0 reader `func_8082E4E0_jp` is NOT that shape:
1445
+ * its t0 is a bit buffer loaded whenever the count runs out (and the count starts at 0), then
1446
+ * shifted each trip, and it lifts with t0 an uninitialised local no path reads before the load.
1447
+ * The cost is a local NO path assigns
1448
+ * (`int r; return r + a;` is `jr ra; addu v0,v0,a0` on gcc, and `int x, y; if (c) x = y;` joins
1449
+ * v0 with v1), which refuses: none of the 29,399 is one.
1450
+ *
1451
+ * THE RULE'S BLIND SIDE is a count a hand-written routine is handed and RELOADS on an outer arm
1452
+ * (`… addiu t0,t0,-1; bnez t0,inner; …; bnezl a1,inner; li t0,8`): the outer arm's constant is a
1453
+ * definition of its own at the inner loop's header, so the entry value counts as met and the first
1454
+ * pass reads an undeclared local — the input is dropped. Nothing tells it from compiled
1455
+ * `int t; do { do { … } while (--t); t = 8; } while (--n);`, and none of the 29,399 is either.
1456
+ * The same side holds an input RELOADED FROM MEMORY on one arm and then transformed: hand
1457
+ * func_8082E4E0_jp its count in a2 as well (drop its `move a2,zero`), and the arm that runs when
1458
+ * the count is not 0 reads the caller's bit buffer in t0, but the loop header's join meets the
1459
+ * shift `sll t0,t0,1`, which reaches t0's entry value only through the join the load `lw t0,0(a0)`
1460
+ * also feeds. So the shift is a definition of its own, the routine lifts with `int code;` read on
1461
+ * that first pass, and the caller's t0 is dropped. Nothing tells it from compiled `int code;`
1462
+ * with the bit count a parameter, and none of the 29,399 is that shape: the real routine zeroes
1463
+ * a2 before the loop, so its load runs first. */
1464
+ const O32_SCRATCH_REGS = ['at', 'v0', 'v1', ...Array.from({ length: 10 }, (_, i) => `t${i}`)] as const;
1465
+
476
1466
  /** MIPS-II / IDO 7.1 target. IDO is the IRIX C compiler,
477
1467
  * statically recompiled to run natively (ido-static-recomp). Unlike agbcc
478
1468
  * it emits no textual asm, so asmlift's input is the DISASSEMBLED object (`mips-linux-gnu-
@@ -483,16 +1473,63 @@ export const MIPS_IDO: TargetDescription = {
483
1473
  compiler: 'ido',
484
1474
  argRegs: ['a0', 'a1', 'a2', 'a3'],
485
1475
  returnReg: 'v0',
1476
+ // MEASURED on ido7.1, gcc2.7.2kmc and gcc2.7.2: the probes at the field.
1477
+ evenWordAlignedArgs: true,
1478
+ // O32: at, v0-v1, a0-a3, t0-t9 and ra are all caller-saved. `frontend/mips.ts` reads the rest, and
1479
+ // `ra`, as the registers a frame store SAVES.
1480
+ callerSaved: [
1481
+ 'at',
1482
+ 'v0',
1483
+ 'v1',
1484
+ 'a0',
1485
+ 'a1',
1486
+ 'a2',
1487
+ 'a3',
1488
+ 't0',
1489
+ 't1',
1490
+ 't2',
1491
+ 't3',
1492
+ 't4',
1493
+ 't5',
1494
+ 't6',
1495
+ 't7',
1496
+ 't8',
1497
+ 't9',
1498
+ 'ra',
1499
+ ],
1500
+ // O32's partition (`O32_NON_ARG_REGS`). The frontend narrows it to the scratch half and the
1501
+ // callee-saved registers its prologue saves (frontend/mips.ts `prologueSavedRegs`).
1502
+ nonArgRegs: O32_NON_ARG_REGS,
1503
+ scratchRegs: O32_SCRATCH_REGS,
1504
+ // Every scratch register, for the measurement at `O32_SCRATCH_REGS`.
1505
+ hiddenInputRegs: O32_SCRATCH_REGS,
1506
+ kernelRegs: ['k0', 'k1'],
1507
+ runtimeHelpers: IDO_RUNTIME_HELPERS,
1508
+ // MEASURED with this toolchain's own flags: `float f(float a, float b){ return a + b; }` is
1509
+ // `jr ra; add.s $f0,$f12,$f14`, and the `'leading'` rule's two halves are the pair in `fpu`'s note.
1510
+ fpu: O32_FPU,
486
1511
  capabilities: { endianness: 'big', hwDivide: true, hwFloat: true, flags: false },
487
1512
  // `switchAllowsNeqCase: false` — IDO's switch dispatch uses `==`/`<`, never `!=` cases;
488
1513
  // leaving it permissive mis-recognises `!=`-rooted if-else chains as switches.
489
1514
  compilerBehaviors: {
1515
+ contractsFloatProducts: false,
490
1516
  coalesceLoopInit: true,
491
1517
  preserveDivergentBranchSense: true,
492
1518
  orderArgCopiesByWriteOrder: true,
493
1519
  switchAllowsNeqCase: false,
494
1520
  // MEASURED — the pair at the field compiles to one load of `p[1]` for every local spelling.
495
1521
  reloadsLocalReread: false,
1522
+ // MEASURED: the probe at the field.
1523
+ narrowStoreConstantTakesLvalueType: true,
1524
+ aggregateBoundary: 1,
1525
+ // MEASURED on all three O32 toolchains (ido7.1, gcc2.7.2kmc, gcc2.7.2): `struct {char c; double d;}`
1526
+ // and its `long long` twin put the member at 8 and size 16 (`sizeof` and the member's offset
1527
+ // compiled into `.data`).
1528
+ largestAlignment: 8,
1529
+ // MEASURED on all three O32 toolchains: the probes at the field.
1530
+ aggregateReturn: 'memory',
1531
+ structReturnHandsBackPointer: true,
1532
+ byValueAggregateArgWords: true,
496
1533
  // MEASURED at `-mips2 -O2 -32 -non_shared -G 0`: the `sll` leads the function for BOTH
497
1534
  // spellings, so the prologue position cannot decide; a narrow DECLARED parameter is the one that
498
1535
  // is both homed dead AND widened in its own argument register. raise/paramwidth.ts's header has
@@ -503,8 +1540,7 @@ export const MIPS_IDO: TargetDescription = {
503
1540
  // reversed-declaration twin, with this compiler's objects beside them — and a test reads the
504
1541
  // correspondence off it: 16 of 16 spills, and rank → offset unchanged when the declaration
505
1542
  // list is reversed, which is what separates declaration rank from the order of the
506
- // assignments. No ido7.1 benchmark row lifts with two or more spilled user locals — the only
507
- // spilling shape in the corpus carries a call, and this frontend declines a call — so no row
1543
+ // assignments. No ido7.1 benchmark row lifts with two or more spilled user locals, so no row
508
1544
  // can tell a wrong value from a right one here.
509
1545
  //
510
1546
  // FLIP CONDITION, and it has TWO parts because the second is easy to miss. (1) The first
@@ -514,6 +1550,26 @@ export const MIPS_IDO: TargetDescription = {
514
1550
  // O32's caller-owned home area `[0,16)` would be read as this function's first four
515
1551
  // declaration ranks. Shipping a direction before the partition orders by argument index.
516
1552
  spillSlotOrder: 'unknown',
1553
+ addressTakenArrayOrder: 'descending', // MEASURED, the probe at the field
1554
+ addressTakenArrayAlignment: 4, // MEASURED, the probe at the field
1555
+ // MEASURED, the probe at the field: the local ends at the frame top, above `ra` and the spills.
1556
+ o32LocalArea: 'above-saved-registers',
1557
+ // MEASURED off the same probe (`twice`, `owns`): O32's outgoing area is the frame bottom, an
1558
+ // unchanged argument is staged again before each call, and a callee assigns to its stack
1559
+ // parameter in place. Read by the frame-object audit alone here (`storedNeverRead`).
1560
+ localsAboveOutgoingArea: true,
1561
+ // MEASURED at -O1, -O2, -O2 -g3 and -O0 (`test/corpus/ido71*-outarea.txt`): no load from 16(sp)
1562
+ // up, but the a1..a3 home words a by-value aggregate is staged in are loaded back (see the field).
1563
+ outgoingAreaNeverLoadedFrom: 16,
1564
+ // MEASURED at -O1 and -O0: the note at the field — `lw zero` for a volatile read, a `t6` load
1565
+ // and a frame store for a plain dead local, `lw a2` for a `register` one.
1566
+ discardsUnusedReadsInto: 'zero',
1567
+ // MEASURED: the probes at the fields.
1568
+ forwardsStructReturnDestination: true,
1569
+ discardedReadMayFillArgRegister: false,
1570
+ // MEASURED, the probes at the field: past the `addiu` reach every member access gets its own
1571
+ // `lui`/`addu`, and a held pointer shares one.
1572
+ farMemberBasePerAccess: { past: 32767 },
517
1573
  },
518
1574
  };
519
1575
 
@@ -525,6 +1581,46 @@ export const MIPS_GCC: TargetDescription = {
525
1581
  compiler: 'gcc',
526
1582
  argRegs: ['a0', 'a1', 'a2', 'a3'],
527
1583
  returnReg: 'v0',
1584
+ // MEASURED on ido7.1, gcc2.7.2kmc and gcc2.7.2: the probes at the field.
1585
+ evenWordAlignedArgs: true,
1586
+ // The same O32 convention MIPS_IDO carries, read the same way.
1587
+ callerSaved: [
1588
+ 'at',
1589
+ 'v0',
1590
+ 'v1',
1591
+ 'a0',
1592
+ 'a1',
1593
+ 'a2',
1594
+ 'a3',
1595
+ 't0',
1596
+ 't1',
1597
+ 't2',
1598
+ 't3',
1599
+ 't4',
1600
+ 't5',
1601
+ 't6',
1602
+ 't7',
1603
+ 't8',
1604
+ 't9',
1605
+ 'ra',
1606
+ ],
1607
+ // O32's partition, as at MIPS_IDO.
1608
+ nonArgRegs: O32_NON_ARG_REGS,
1609
+ scratchRegs: O32_SCRATCH_REGS,
1610
+ hiddenInputRegs: O32_SCRATCH_REGS,
1611
+ // MEASURED on gcc2.7.2kmc and gcc2.7.2 at their canonical flags: `register int g __asm__("$16");
1612
+ // int f(void){ return g; }` is `jr ra; move v0,s0`, and `$23`/`$30` give s7/s8. KMC's `-fno-asm`
1613
+ // rejects the bare `asm(` spelling, which is why every target spells `__asm__`. IDO 7.1 has no
1614
+ // such declaration (its cfe rejects both spellings), so MIPS_IDO states none.
1615
+ globalRegisterVariables: {
1616
+ regs: {
1617
+ ...Object.fromEntries(Array.from({ length: 8 }, (_, i) => [`s${i}`, `$${16 + i}`])),
1618
+ s8: '$30',
1619
+ fp: '$30',
1620
+ },
1621
+ },
1622
+ kernelRegs: ['k0', 'k1'],
1623
+ runtimeHelpers: MIPS_GCC_RUNTIME_HELPERS,
528
1624
  // KMC GCC keeps a loop seeded from an argument register IN that register (coalesceLoopInit
529
1625
  // true, like IDO): test/corpus/gcc-gcd.asm runs its whole loop on a0/a1 with no init copies,
530
1626
  // and the row it comes from matches only with the parameters as the loop's homes. The other
@@ -542,8 +1638,12 @@ export const MIPS_GCC: TargetDescription = {
542
1638
  // and dmastride on agbcc lost; maxarr and preupdate_exit_call on agbcc gained) — because a pred
543
1639
  // that computes the initial value INTO the param's own register wrote the key and still
544
1640
  // coalesces.
1641
+ // The same o32 convention, measured on GCC_KMC_TOOLCHAIN at -O2: the fadd pair compiles to the
1642
+ // same two instructions IDO's does, and `float f(int a, float b)` moves `b` over with `mtc1 a1`.
1643
+ fpu: O32_FPU,
545
1644
  capabilities: { endianness: 'big', hwDivide: true, hwFloat: true, flags: false },
546
1645
  compilerBehaviors: {
1646
+ contractsFloatProducts: false,
547
1647
  coalesceLoopInit: true,
548
1648
  preserveDivergentBranchSense: true,
549
1649
  orderArgCopiesByWriteOrder: true,
@@ -585,6 +1685,17 @@ export const MIPS_GCC: TargetDescription = {
585
1685
  // MEASURED on BOTH toolchains this description serves (the note above): one load of `p[1]` for
586
1686
  // every local spelling of the pair at the field, gcc2.7.2kmc at -O2 and gcc2.7.2 at -O1 alike.
587
1687
  reloadsLocalReread: false,
1688
+ // MEASURED on BOTH toolchains this description serves, the probe at the field.
1689
+ narrowStoreConstantTakesLvalueType: true,
1690
+ // MEASURED on BOTH toolchains this description serves, the pairs at the field.
1691
+ keepsPointerSumAddend: true,
1692
+ aggregateBoundary: 1,
1693
+ // MEASURED on BOTH toolchains this description serves, and on ido7.1: the probe at MIPS_IDO's.
1694
+ largestAlignment: 8,
1695
+ // MEASURED on BOTH toolchains this description serves: the probes at the field.
1696
+ aggregateReturn: 'memory',
1697
+ structReturnHandsBackPointer: true,
1698
+ byValueAggregateArgWords: true,
588
1699
  // MEASURED on BOTH toolchains this description serves: `int f(s8 x){return x;}` and
589
1700
  // `int f(s32 x){return (s8)x;}` compile to BYTE-IDENTICAL objects, gcc2.7.2kmc at -O2 and
590
1701
  // gcc2.7.2 at -O1 alike — so the object carries no witness at all and the pass refuses.
@@ -598,7 +1709,21 @@ export const MIPS_GCC: TargetDescription = {
598
1709
  // The two agreeing is not a formality. The value is per DESCRIPTION and TWO toolchains map
599
1710
  // here, so a toolchain whose direction differed from its description's would need a
600
1711
  // per-toolchain override this bag cannot express — see the note at `compilerBehaviors`.
1712
+ designatedInitializers: true, // both gcc 2.7.2 builds compile `{ 1, [8] = 2 }` (see the field)
601
1713
  spillSlotOrder: 'unknown',
1714
+ addressTakenArrayOrder: 'ascending', // MEASURED on both toolchains, the probe at the field
1715
+ addressTakenArrayAlignment: 8, // MEASURED on both toolchains, the probe at the field
1716
+ // MEASURED on BOTH toolchains this description serves, the probe at the field: identical
1717
+ // layouts, the local right above the 8-aligned outgoing area and under the saves.
1718
+ o32LocalArea: 'above-outgoing-area',
1719
+ // MEASURED on BOTH toolchains this description serves, the probe at `o32LocalArea` (`twice`).
1720
+ localsAboveOutgoingArea: true,
1721
+ // MEASURED on BOTH toolchains at their rows' levels and at -O0 (`test/corpus/gcc272*-outarea.txt`):
1722
+ // no load from 16(sp) up the area; a char-array struct's home words are loaded (see the field).
1723
+ outgoingAreaNeverLoadedFrom: 16,
1724
+ // MEASURED on BOTH toolchains this description serves: the probes at the fields.
1725
+ forwardsStructReturnDestination: false,
1726
+ discardedReadMayFillArgRegister: true,
602
1727
  },
603
1728
  };
604
1729
 
@@ -614,11 +1739,49 @@ export const PPC_MWCC: TargetDescription = {
614
1739
  // PPC EABI: r3–r10 pass integer/pointer arguments; r3 also returns.
615
1740
  argRegs: ['r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10'],
616
1741
  returnReg: 'r3',
1742
+ // MEASURED on 2.4.2 at the synthetic tier's flags and on 2.3.3 and 2.4.7 at -O4,p:
1743
+ // `takeLL(1, gL)` sets r3 and r5:r6 with r4 unset.
1744
+ evenWordAlignedArgs: true,
1745
+ // PPC EABI: r0 and r3-r12 are volatile, and lr carries the return address `bl` overwrites. r11
1746
+ // and r12 are the linker's stub scratch, r13 is the small-data base and r14 upward are
1747
+ // callee-saved.
1748
+ callerSaved: ['r0', 'r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10', 'r11', 'r12', 'lr'],
1749
+ // Every GPR but r1 (the stack pointer, never an SSA register here) and the eight argument
1750
+ // registers. The frontend narrows it to `scratchRegs` and the ones the prologue saved
1751
+ // (`prologueSavedRegs`). r2 and r13, the small-data bases, never reach that rule: a read of either
1752
+ // as a value refuses first (`SMALL_DATA_BASES` in frontend/ppc.ts).
1753
+ nonArgRegs: ['r0', 'r2', ...Array.from({ length: 21 }, (_, i) => `r${i + 11}`)],
1754
+ // The volatile registers no argument travels in, which a callee may use with no save. MEASURED on
1755
+ // 2.4.2 at the synthetic tier's flags and on 2.3.3 at Pikmin's -O4,p (in C): a leaf homes an
1756
+ // uninitialised local in each of them, unsaved — `int x; if (s) x = a*3; return x + a;` is
1757
+ // `mulli r0,r4,3; add r3,r0,r4`, and with all eight argument registers live two such locals land
1758
+ // in r11 and r12. 2.4.7 at MP4's flags puts them in saved registers instead.
1759
+ scratchRegs: ['r0', 'r11', 'r12'],
1760
+ // r11 is the save routines' frame address and r12 `__ptmf_scall`'s member-function pointer: a
1761
+ // read of either that no path's definition joins refuses instead of minting `uninit_r11`.
1762
+ hiddenInputRegs: ['r11', 'r12'],
1763
+ // MEASURED on 2.4.2 and 2.4.7 at -O4,p: `register int g __asm__("r14"); int f(void){ return g; }`
1764
+ // is `mr r3,r14`. They accept r14..r28 only ("supports 15 registers beginning with r14") and no
1765
+ // gap among them ("nothing at 'r19' not permitted"), in any declaration order. 2.3.3 refuses the
1766
+ // storage class, so `mwcc_233_163n` drops this field.
1767
+ globalRegisterVariables: {
1768
+ regs: Object.fromEntries(Array.from({ length: 15 }, (_, i) => [`r${14 + i}`, `r${14 + i}`])),
1769
+ contiguous: Array.from({ length: 15 }, (_, i) => `r${14 + i}`),
1770
+ },
1771
+ runtimeHelpers: PPC_MWCC_RUNTIME_HELPERS,
1772
+ registerSaveRoutines: PPC_MWCC_SAVE_ROUTINES,
1773
+ // PPC EABI with `-fp hard`, measured at the synthetic tier's flags: `fadds f1,f1,f2; blr`, a third
1774
+ // float argument in f3, and the GPR/FPR order of mixed parameters absent from the object.
1775
+ fpu: { argRegs: ['f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8'], returnReg: 'f1', slots: 'separate' },
617
1776
  capabilities: { endianness: 'big', hwDivide: true, hwFloat: true, flags: true },
618
1777
  // CodeWarrior's structuring compiler behaviors are UNKNOWN until fixtures reveal them — safe universal
619
1778
  // defaults; coalesceLoopInit false until a CW loop fixture says otherwise — the second of the
620
1779
  // two compiler-wide guesses standing in for the per-function observation named at MIPS_GCC.
621
1780
  compilerBehaviors: {
1781
+ contractsFloatProducts: true,
1782
+ // MEASURED on all three builds in C: an integer, a pointer and a `u16 *` passed to an `s32 *`,
1783
+ // `u32` and `s32 *` parameter each fail to compile, and each compiles once cast (see the field).
1784
+ strictPointerConversions: true,
622
1785
  coalesceLoopInit: false,
623
1786
  preserveDivergentBranchSense: true,
624
1787
  orderArgCopiesByWriteOrder: true,
@@ -627,10 +1790,51 @@ export const PPC_MWCC: TargetDescription = {
627
1790
  // `u8 v = p[3]; if ((v & 0x7f) == 0x7f) { fnA(); p[4] = v; return; }` under an `if (a)`
628
1791
  // matches only once `read-behind-effect` stops refusing it (3/24 → MATCH 0/22).
629
1792
  reloadsLocalReread: false,
1793
+ // MEASURED on all three builds at -O4,p, and on mwcc_247_107 at -O0,p: the probe at the field.
1794
+ narrowStoreConstantTakesLvalueType: true,
1795
+ aggregateBoundary: 1,
1796
+ // MEASURED on all three builds: `struct {u8 a; double d;}` and its `long long` twin put the
1797
+ // member at 8 and size 16
1798
+ largestAlignment: 8,
1799
+ aggregateReturn: 'svr4',
1800
+ // MEASURED on all three builds: r3 holds a reload of a frame word at the `blr` of a struct
1801
+ // returner that calls something
1802
+ structReturnHandsBackPointer: false,
1803
+ // MEASURED on mwcc_242_81 through the `.comment` alignment record: an array or a struct is at
1804
+ // 4 at least (`u8[1]`, a struct of two bytes), a scalar at its width; a string-literal
1805
+ // initializer gets no more than the array does (`char s[] = "hello!"` and a `u8` list both 4);
1806
+ // and a scalar `= 0` moves to bss, where `int q[1] = {0}` stays in `.data`. In both bss
1807
+ // sections the moved scalars come first in declaration order. The statics with no initializer
1808
+ // follow reversed in `.sbss`; in `.bss` (an object over the `-sdata` threshold: an array over 8
1809
+ // bytes, every scalar at `-sdata 0`) they follow in the order the compiler first uses them
1810
+ // (`a[i]=1; b[i]=2;` puts a first, the swap b), which neither the source's text
1811
+ // (`x = b[i]; a[i] = 1;` puts a first) nor the emitted code (`a[i] = b[i]` reads b first, puts a
1812
+ // first) fixes. The other mwcc builds this description serves are not measured.
1813
+ staticLayout: {
1814
+ aggregateAlign: 4,
1815
+ stringAlign: 4,
1816
+ zeroScalar: 'bss',
1817
+ uninitOrder: { '.sbss': 'reversed', '.bss': 'first-use' },
1818
+ },
1819
+ // MEASURED on all three builds at -O4,p and -O0,p: the note at the field. The two are one
1820
+ // declaration: without the layout gate a relational if-ladder reads as a `switch`. The gate's
1821
+ // reach is the committed probes alone — withdrawn, it moves 0 of the 180 PPC benchmark rows
1822
+ // that lift — so switch-arms.test.ts holds the pairing over every target instead of a row.
1823
+ switchBoundCase: 'either',
1824
+ switchRequiresFrontLoadedTests: true,
630
1825
  // The PowerPC prologue widens a declared narrow parameter with `extsb`/`extsh`, which the
631
1826
  // frontend lifts to the same `sext` op agbcc's shift pair folds to — the position shape, on
632
1827
  // another ISA. `synthetic:{sextb,tos8}:mwcc_242_81` are its rows, MATCH through that pass.
633
1828
  narrowParamWitness: 'prologue-extension',
1829
+ // MEASURED on all three builds: the note at the field.
1830
+ linkageLocalArea: 'above-outgoing-area',
1831
+ // MEASURED off the same probes, on all three builds and at -O0,p (`twice9`, and every outgoing
1832
+ // word below the area unread): Mario Party 4's -O0,p homes a parameter in the area and reloads
1833
+ // it after a call, which proves the word a local's (`ee`, `synthetic:homecall:mwcc_242_81`).
1834
+ localsAboveOutgoingArea: true,
1835
+ // MEASURED on all three builds at the rows' flags and -O0,p, and on mwcc_242_81 at -O4,s
1836
+ // (`test/corpus/mwcc*-outarea.txt`): no load of the parameter area, which starts at 8(r1).
1837
+ outgoingAreaNeverLoadedFrom: 8,
634
1838
  // NOT MEASURED, and `'unknown'` is therefore the only honest value here rather than a withheld
635
1839
  // one, as it is at MIPS_IDO and MIPS_GCC. No mwcc row lifts with two or more spilled user
636
1840
  // locals, and the compiler does not spill the committed declaration-rank probe either: at
@@ -639,9 +1843,19 @@ export const PPC_MWCC: TargetDescription = {
639
1843
  // first: `frontend/ppc.ts` claims no `LiveInModel.declaredLocals`, so the shared stamp records
640
1844
  // no slot home on this target at all and a direction here would order nothing until it does.
641
1845
  spillSlotOrder: 'unknown',
1846
+ // MEASURED on all three builds at -O0,p: the note at the field — `lwz r0` for a volatile read,
1847
+ // an `r31` home for a plain or `register` dead local.
1848
+ discardsUnusedReadsInto: 'r0',
1849
+ // MEASURED on all three builds: the probes at the fields.
1850
+ forwardsStructReturnDestination: false,
1851
+ discardedReadMayFillArgRegister: true,
642
1852
  },
643
1853
  };
644
1854
 
1855
+ /** CodeWarrior 2.3.3b163n (Pikmin): `PPC_MWCC` but for the one declaration a probe refuted there —
1856
+ * it rejects a global register variable ("illegal storage class"), so it has no spelling for one. */
1857
+ export const PPC_MWCC_233: TargetDescription = { ...PPC_MWCC, globalRegisterVariables: undefined };
1858
+
645
1859
  /** A toolchain: one compiler binary, named as decomp.me names it. Several toolchains may share a
646
1860
  * description (`MIPS_GCC` serves two), and each keeps its own evidence. */
647
1861
  export interface ToolchainTarget {
@@ -715,7 +1929,8 @@ export const TOOLCHAIN_TARGETS = {
715
1929
  description: PPC_MWCC,
716
1930
  },
717
1931
  // 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
1932
+ // game tree) and 2.4.7b107 (Mario Party 4's DOL). Both SHARE `PPC_MWCC` (2.3.3 less the global
1933
+ // register variables it refuses, `PPC_MWCC_233`), and on their own evidence
719
1934
  // rather than because they are the same compiler family: `test/matching/ppc-compiler-behaviors.test.ts`
720
1935
  // re-runs both of its probes on each binary at the flags that build's rows compile at, and each
721
1936
  // reads as the description says. What moves those readings is the optimisation level — at `-O0,p`
@@ -727,7 +1942,7 @@ export const TOOLCHAIN_TARGETS = {
727
1942
  // are two different sets, neither of which is "the" one. See `canonicalFlags` above.
728
1943
  mwcc_233_163n: {
729
1944
  family: 'mwcc',
730
- description: PPC_MWCC,
1945
+ description: PPC_MWCC_233,
731
1946
  },
732
1947
  mwcc_247_107: {
733
1948
  family: 'mwcc',
@@ -766,17 +1981,64 @@ export interface ResolvedTarget {
766
1981
  toolchain: ToolchainId;
767
1982
  /** the flags the function's target and every candidate compile with */
768
1983
  cflags: readonly string[];
769
- /** the description asmlift decompiles against: the toolchain's, at every flag set */
1984
+ /** the description asmlift decompiles against: the toolchain's, less a fact a flag changes */
770
1985
  target: TargetDescription;
771
1986
  /** what the flags make the compiler do */
772
1987
  profile: CodegenProfile;
773
1988
  }
774
1989
 
1990
+ /** Does a compiler of `family`, at optimisation level `level` (the profile's `O` slot), keep a plain
1991
+ * read whose value nothing uses (`compilerBehaviors.keepsUnusedReads`)? COMPILED, `int x; x = p[5];
1992
+ * x = 0; *q = x;` and `int x = p[1];`: mwcc 2.3.3 and 2.4.2 at `-O0` and 2.4.7 at `-O0,p` keep
1993
+ * `lwz r31, 20(r3)` in a register nothing reads; IDO 7.1 at `-O0` and at `-O1` (its default) keeps
1994
+ * `lw t6, 20(a0)`; agbcc and KMC gcc 2.7.2 at `-O0` keep the read and store it to the frame. None
1995
+ * of them does at agbcc/KMC `-O1`, `-O2` or `-O3`, IDO `-O2` or `-O3`, or mwcc 2.4.2 `-O1` through
1996
+ * `-O4,p` and `-O4,s`. A plain expression statement's read (`*p;`) is gone at every level on every
1997
+ * one of them; a volatile one stays, at a keeping level in the register `discardsUnusedReadsInto`
1998
+ * names where the compiler has one.
1999
+ *
2000
+ * A LEVEL NO FLAG NAMES (`undefined`) KEEPS. Every family's unflagged compile was measured keeping:
2001
+ * agbcc's and IDO's are their `-O0` and `-O1` (codegen-flags.ts `EFFECTIVE` names them, so they
2002
+ * never reach here unnamed), KMC gcc 2.7.2 with no `-O` emits `-O0`'s object byte for byte, and
2003
+ * mwcc 2.4.2 with no level, with `-opt off` or with `-opt level=0` keeps `x = p[5]; x = 0;`'s
2004
+ * `lwz r31, 20(r3)` as at `-O0`. And the cost runs one way: answering "keeps" where the compiler
2005
+ * deletes withholds a `volatile` outside the discard register (`discardsUnusedReadsInto`): the
2006
+ * read is dropped as plain on mwcc and IDO, and the lift refuses on a compiler with none. Answering
2007
+ * "deletes" where it keeps publishes one the source never wrote, a value this stamp alone decides,
2008
+ * with no candidate to referee it (docs/level-tower.md, A PROFILE OF A COMPILER INHERITS). */
2009
+ export function keepsUnusedReadsAt(family: FlagFamily, level: string | undefined): boolean {
2010
+ if (level === undefined) {
2011
+ return true;
2012
+ }
2013
+ const n = /^\d/.exec(level)?.[0];
2014
+ return n === '0' || (family === 'ido' && n === '1');
2015
+ }
2016
+
2017
+ /** The level `keepsUnusedReadsAt` judges a profile at: its `O` slot, or none where a word the table
2018
+ * does not read may set it too — mwcc's `-opt off` and `-opt level=0` compile at `-O0`, and the
2019
+ * table reads neither, so a profile that spells one has a level nothing here knows. */
2020
+ function unusedReadLevel(profile: CodegenProfile): string | undefined {
2021
+ const optWord = profile.family === 'mwcc' && profile.unclassified.some((w) => w.startsWith('-opt'));
2022
+ return optWord ? undefined : profile.slots.O;
2023
+ }
2024
+
775
2025
  /** The target a function compiled by `toolchain` at `cflags` decompiles against, and the profile
776
2026
  * those flags describe. Throws on a level word the toolchain's family cannot read. */
777
2027
  export function targetFor(toolchain: ToolchainId, cflags: readonly string[]): ResolvedTarget {
778
2028
  const t: ToolchainTarget = TOOLCHAIN_TARGETS[toolchain];
779
- return { toolchain, cflags, target: t.description, profile: parseFlags(t.family, cflags) };
2029
+ const profile = parseFlags(t.family, cflags);
2030
+ const cpp = dialectOf(profile.slots.lang) === 'c++';
2031
+ let target: TargetDescription = cpp ? { ...t.description, dialect: 'c++' } : t.description;
2032
+ if (keepsUnusedReadsAt(t.family, unusedReadLevel(profile))) {
2033
+ target = { ...target, compilerBehaviors: { ...target.compilerBehaviors, keepsUnusedReads: true } };
2034
+ }
2035
+ if (profile.slots['-fshort-enums'] === 'on') {
2036
+ // every enum is the smallest integer its values fit (c-decl.c:6064-6065, :6123; compiled,
2037
+ // `sizeof(struct {enum {K0, K1} k[2];})` is 4 under the flag and 8 without), so none is enumBytes
2038
+ const { enumBytes: _flagged, ...behaviors } = target.compilerBehaviors;
2039
+ target = { ...target, compilerBehaviors: behaviors };
2040
+ }
2041
+ return { toolchain, cflags, target, profile };
780
2042
  }
781
2043
 
782
2044
  /** Build the structurer's options for a target: the function's own `returnsVoid` plus every
@@ -789,10 +2051,13 @@ export function targetFor(toolchain: ToolchainId, cflags: readonly string[]): Re
789
2051
  * `nearBaseSpan` / `foldsConstAddrOffset` are read off the target by rank.ts. So the field names
790
2052
  * are a SUPERSET of StructureOptions', not a bijection, and nothing may derive one from the other
791
2053
  * by enumerating keys. */
792
- export function structureOptionsFor(t: TargetDescription, returnsVoid: boolean): StructureOptions {
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.
2054
+ export function structureOptionsFor(
2055
+ t: TargetDescription,
2056
+ returnsVoid: boolean,
2057
+ prototypes: Prototypes = {},
2058
+ ): StructureOptions {
2059
+ // `littleEndian` is the HARDWARE capability the structurer consumes (bitfield extract recognition
2060
+ // is LSB-first); everything else is a compiler behavior.
796
2061
  //
797
2062
  // ONE FIELD IS NOT A STRAIGHT SPREAD, and this is where the difference belongs. A frame
798
2063
  // direction has THREE states here — `ascending`, `descending`, and `'unknown'` meaning measured
@@ -803,13 +2068,64 @@ export function structureOptionsFor(t: TargetDescription, returnsVoid: boolean):
803
2068
  const { spillSlotOrder, ...behaviors } = t.compilerBehaviors;
804
2069
  return {
805
2070
  returnsVoid,
2071
+ declaredArgs: declaredArgTypes(prototypes, t.dialect !== 'c++'),
2072
+ declaredReturns: declaredReturnTypes(prototypes),
2073
+ ...(t.dialect === 'c++' ? { dialect: 'c++' as const } : {}),
806
2074
  littleEndian: t.capabilities.endianness === 'little',
807
- ...(t.capabilities.deviceRegisters ? { deviceRegisters: t.capabilities.deviceRegisters } : {}),
808
2075
  ...behaviors,
809
2076
  ...(spillSlotOrder === 'ascending' || spillSlotOrder === 'descending' ? { spillSlotOrder } : {}),
810
2077
  };
811
2078
  }
812
2079
 
813
- export const C_TYPEDEFS =
814
- 'typedef unsigned char u8;typedef unsigned short u16;typedef unsigned int u32;' +
815
- 'typedef signed char s8;typedef short s16;typedef int s32;\n';
2080
+ /** The global a register `globalRegisterVariables` names is lifted as (`asmName` its `__asm__`
2081
+ * name: `greg_r8`, and `greg_r16` for MIPS `$16`). */
2082
+ export const globalRegisterSymbol = (asmName: string): string => `greg_${asmName.replace(/^\$/, 'r')}`;
2083
+
2084
+ /** The candidate prelude: a `typedef` for every name in the scalar vocabulary. PRINTED from
2085
+ * `proto.ts`'s `PRELUDE_TYPEDEFS` rather than spelled out, because the other reader of that
2086
+ * vocabulary decides which type texts asmlift may PRINT into a candidate, and a second copy of
2087
+ * the list is a copy that can disagree — in the direction no test over two lists catches.
2088
+ *
2089
+ * The decomp checkouts all define the same names, and the harness keeps typedefs per NAME against
2090
+ * the vendored ctx, so a unit that already has them gets no redefinition. */
2091
+ export const C_TYPEDEFS = `${[...PRELUDE_TYPEDEFS].map(([name, base]) => `typedef ${base} ${name};`).join('')}\n`;
2092
+
2093
+ /** Each declared callee's parameter types, where one can be PRINTED as a cast — the argument
2094
+ * conversions C++ refuses to make implicitly (backend/cfamily.ts `argConversion`), and a pointer
2095
+ * cell's integer arguments (l3/ptrcell.ts). In C (`opaquePointers`), an object pointer to a type
2096
+ * this cannot spell — `struct S *` — is kept too: the printer casts its argument through `void *`,
2097
+ * which C converts to any object pointer implicitly, where a strict front end (mwcc) refuses
2098
+ * `use2(s)` for a `u32 s[2]` the static's bytes were typed as. Any other entry this cannot spell is
2099
+ * `undefined`, and that argument is printed uncast. */
2100
+ function declaredArgTypes(
2101
+ prototypes: Prototypes,
2102
+ opaquePointers: boolean,
2103
+ ): Record<string, readonly (ParamType | undefined)[]> {
2104
+ const out: Record<string, readonly (ParamType | undefined)[]> = {};
2105
+ for (const [name, p] of Object.entries(prototypes)) {
2106
+ if (Array.isArray(p.params)) {
2107
+ out[name] = p.params.map((t) =>
2108
+ (spellableType(t) && declaredWidth(t) !== undefined) || (opaquePointers && objectPointerType(t))
2109
+ ? t
2110
+ : undefined,
2111
+ );
2112
+ }
2113
+ }
2114
+ return out;
2115
+ }
2116
+
2117
+ /** A pointer to an object, a pointer to a function excluded: `void *` converts to it implicitly in
2118
+ * C, which it does not to a function pointer. */
2119
+ const objectPointerType = (t: ParamType): boolean => /\*\s*(?:const|volatile|\s)*$/.test(t) && !t.includes('(');
2120
+
2121
+ /** Each declared callee's return type as the project spells it, which is never printed: whether a
2122
+ * pointer cell compared with the call meets a pointer (l3/ptrcell.ts). */
2123
+ function declaredReturnTypes(prototypes: Prototypes): Record<string, ParamType> {
2124
+ const out: Record<string, ParamType> = {};
2125
+ for (const [name, p] of Object.entries(prototypes)) {
2126
+ if (p.returns !== undefined) {
2127
+ out[name] = p.returns;
2128
+ }
2129
+ }
2130
+ return out;
2131
+ }