@asmlift/core 0.9.0 → 0.11.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 (117) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +2 -0
  5. package/src/backend/cfamily.ts +197 -61
  6. package/src/backend/cpp.ts +3 -1
  7. package/src/backend/lines.ts +10 -0
  8. package/src/backend/pascal.ts +47 -16
  9. package/src/cdecl/parse.ts +21 -0
  10. package/src/contracts.ts +246 -36
  11. package/src/declare.ts +69 -20
  12. package/src/frontend/arm-body.ts +155 -0
  13. package/src/frontend/asmdata.ts +9 -2
  14. package/src/frontend/call-plan.ts +1117 -61
  15. package/src/frontend/callee-body.ts +370 -0
  16. package/src/frontend/dead-reads.ts +628 -0
  17. package/src/frontend/device-pins.ts +33 -4
  18. package/src/frontend/emit.ts +4 -3
  19. package/src/frontend/fpu.ts +224 -17
  20. package/src/frontend/frame-objects.ts +1689 -206
  21. package/src/frontend/frontend.ts +84 -3
  22. package/src/frontend/high-half.ts +53 -4
  23. package/src/frontend/jump-table.ts +545 -0
  24. package/src/frontend/local-object.ts +158 -32
  25. package/src/frontend/mips.ts +2296 -217
  26. package/src/frontend/own-struct-return.ts +500 -0
  27. package/src/frontend/ppc.ts +2748 -245
  28. package/src/frontend/register-struct-call.ts +399 -0
  29. package/src/frontend/reloc-symbol.ts +22 -7
  30. package/src/frontend/ssa.ts +1721 -43
  31. package/src/frontend/stackargs.ts +199 -72
  32. package/src/frontend/thumb-entry.ts +340 -0
  33. package/src/frontend/thumb.ts +2400 -325
  34. package/src/ir/alias.ts +65 -2
  35. package/src/ir/bits.ts +1699 -4
  36. package/src/ir/core.ts +254 -9
  37. package/src/ir/discipline.ts +86 -21
  38. package/src/ir/entry.ts +157 -0
  39. package/src/{structure → ir}/loops.ts +6 -2
  40. package/src/ir/opcodes.ts +137 -9
  41. package/src/ir/parse.ts +1 -0
  42. package/src/ir/simplify.ts +61 -10
  43. package/src/ir/types.ts +86 -2
  44. package/src/ir/verify.ts +96 -11
  45. package/src/l3/argbase.ts +2 -0
  46. package/src/l3/ast.ts +582 -18
  47. package/src/l3/basecse.ts +68 -15
  48. package/src/l3/caseendgoto.ts +223 -0
  49. package/src/l3/coalesce.ts +42 -14
  50. package/src/l3/constinit.ts +410 -0
  51. package/src/l3/dce.ts +53 -9
  52. package/src/l3/initfirst.ts +42 -11
  53. package/src/l3/intcell.ts +2 -1
  54. package/src/l3/narrowdecl.ts +77 -36
  55. package/src/l3/pollguard.ts +8 -2
  56. package/src/l3/ptrcell.ts +33 -34
  57. package/src/l3/ptrfield.ts +123 -8
  58. package/src/l3/regspell.ts +11 -4
  59. package/src/l3/reindex.ts +295 -99
  60. package/src/l3/scopebase.ts +2 -0
  61. package/src/l3/storage.ts +98 -1
  62. package/src/l3/symbol-refs.ts +84 -6
  63. package/src/l3/tailmerge.ts +6 -2
  64. package/src/l3/typing.ts +108 -15
  65. package/src/l3/unreduce.ts +6 -5
  66. package/src/l3/volatileptr.ts +17 -54
  67. package/src/mangle.ts +30 -0
  68. package/src/pattern/engine.ts +361 -73
  69. package/src/pipeline.ts +1039 -38
  70. package/src/proto-context.ts +1039 -45
  71. package/src/proto.ts +555 -40
  72. package/src/raise/aggmembers.ts +290 -0
  73. package/src/raise/arrays.ts +5 -1
  74. package/src/raise/const.ts +25 -0
  75. package/src/raise/declared-volatile.ts +207 -31
  76. package/src/raise/divpow2.ts +192 -0
  77. package/src/raise/floathelpers.ts +254 -33
  78. package/src/raise/gvn.ts +3 -3
  79. package/src/raise/latch.ts +162 -6
  80. package/src/raise/memberarrays.ts +10 -4
  81. package/src/raise/offsetnames.ts +422 -36
  82. package/src/raise/pairparams.ts +33 -0
  83. package/src/raise/paramwidth.ts +196 -6
  84. package/src/raise/pre-recovery.ts +107 -21
  85. package/src/raise/pre-structure.ts +73 -0
  86. package/src/raise/recover.ts +12 -5
  87. package/src/raise/reroll.ts +674 -0
  88. package/src/raise/shortcircuit.ts +217 -34
  89. package/src/raise/softdiv.ts +1 -1
  90. package/src/raise/storesign.ts +153 -0
  91. package/src/raise/struct-arrays.ts +30 -11
  92. package/src/raise/structs.ts +396 -33
  93. package/src/raise/truncload.ts +4 -3
  94. package/src/raise/unpre.ts +594 -0
  95. package/src/raise/unthread.ts +400 -0
  96. package/src/rank-declare.ts +118 -12
  97. package/src/rank-variations.ts +186 -35
  98. package/src/rank.ts +536 -68
  99. package/src/runtime-helpers.ts +140 -22
  100. package/src/structure/analysis.ts +217 -8
  101. package/src/structure/arith-ops.ts +23 -0
  102. package/src/structure/cross-jumped-tail.ts +216 -0
  103. package/src/structure/globalaccess.ts +157 -10
  104. package/src/structure/hazards.ts +16 -64
  105. package/src/structure/local-statics.ts +145 -9
  106. package/src/structure/namecoalesce.ts +1 -1
  107. package/src/structure/pointer-spelling.ts +1106 -0
  108. package/src/structure/redundant-test.ts +1 -1
  109. package/src/structure/structure.ts +6192 -1213
  110. package/src/structure/switch-recover.ts +583 -120
  111. package/src/structure/unfolded-branch-constant.ts +90 -0
  112. package/src/symbols.ts +58 -15
  113. package/src/target.ts +1028 -54
  114. package/src/trace.ts +49 -17
  115. package/src/variation-definitions.ts +333 -20
  116. package/src/variation-gates.ts +4 -0
  117. package/src/variation-tokens.ts +68 -3
package/src/target.ts CHANGED
@@ -5,7 +5,9 @@
5
5
  // What each datum drives:
6
6
  // • id → frontend dispatch (registry.ts)
7
7
  // • compiler → idiom gating (patternApplies) + the report
8
- // • argRegs / returnReg → entry-param ordering and return-value read in the frontends
8
+ // • argRegs / returnReg → entry-param ordering and return-value read in the frontends; argRegs
9
+ // also says whether an ARM body's argument scan counts this target's words
10
+ // (frontend/arm-body.ts `scansArgumentsOf`, read by frontend/callee-body.ts)
9
11
  // • capabilities.hwDivide → gates the MIPS hardware-divide decode (mips.ts), the soft-division
10
12
  // pre-recovery pass, and idiom gating; a `div` on a target declaring no divider degrades to
11
13
  // a loud opaque (exercised by packages/cli/test/matching/divmul.test.ts). `hwFloat` → idiom
@@ -19,14 +21,12 @@
19
21
  // device may read, so a slot it cannot reach is not refused.
20
22
  // • capabilities.blockTransferCalls → the same audit: a frame address handed to one of these
21
23
  // as its SOURCE is read as far as the call's literal control word says, and never written.
22
- // • capabilities.deviceRegisters → six readers, and they ask ONE question — "would a source
24
+ // • capabilities.deviceRegisters → five readers, and they ask ONE question — "would a source
23
25
  // have spelled this address `volatile`" — which is a question about SPELLING and may be
24
26
  // approximate: the `/vol-store` variation's eligibility (l3/volstore.ts), rank.ts's volatility
25
27
  // tie-break between two byte-identical spellings, the first half of `/unreduce`'s
26
28
  // disjointness gate (l3/unreduce.ts), the `/homesplit` pairing's refusal to leave a device
27
- // READ inline where the spelling it replaces would have qualified it (l3/homesplit.ts), the
28
- // structurer's refusal to SPELL a dead memory read whose address no qualifier could ever
29
- // reach (structure.ts `volatileQualifiable`, threaded through StructureOptions), and the
29
+ // READ inline where the spelling it replaces would have qualified it (l3/homesplit.ts), and the
30
30
  // device pin (frontend/device-pins.ts), which spells every access in the window `volatile`
31
31
  // in a function the frame-object audit keeps as one object, and in one it accepts object by
32
32
  // object every device read and the device stores a later store in their block overwrites —
@@ -44,14 +44,19 @@
44
44
  // Others are read off the target directly by a consumer that is not the structurer — among
45
45
  // them `nearBaseSpan` and `foldsConstAddrOffset` (rank.ts, L3 respell variations),
46
46
  // `reloadsLocalReread`, `narrowParamWitness` and `aggregateBoundary` (raise/pre-recovery.ts),
47
- // `aggregateReturn`, `largestAlignment`, `enumBytes` and `bitfieldPacking` (aggregate.ts, for
48
- // frontend/thumb.ts, frontend/ppc.ts and frontend/frame-objects.ts),
47
+ // `aggregateReturn`, `largestAlignment`, `enumBytes`, `bitfieldPacking` and
48
+ // `byValueAggregateArgWords` (aggregate.ts, for frontend/call-plan.ts — which the Thumb, MIPS
49
+ // and PowerPC lowerings share — frontend/thumb.ts, frontend/mips.ts, frontend/ppc.ts and
50
+ // frontend/frame-objects.ts; `enumBytes` is the structurer's too, through StructureOptions),
49
51
  // `hoistsSingleSetArm` (raise/narrowlocal.ts and raise/retsink.ts), `arrayShapeFromStride`
50
52
  // (raise/globalshape.ts, run on the LIFTED fn), `eightByteReturnScratch`
51
53
  // (frontend/thumb.ts, which reads the epilogue), `callThunks` (frontend/thumb.ts, which
52
- // lowers a `bl` to one as a call through its register), `roundTripsDoubleLiterals`
53
- // (raise/floathelpers.ts) and `volatileReadsExtendInRegister` (frontend/device-pins.ts, and
54
- // raise/declared-volatile.ts through liftStamped and the offset-name pass). The
54
+ // lowers a `bl` to one as a call through its register), `farJumpRelay` (frontend/thumb.ts,
55
+ // which lowers a marked `bl` to the branch it stands for), `roundTripsDoubleLiterals`
56
+ // (raise/floathelpers.ts), `volatileReadsExtendInRegister` (frontend/device-pins.ts, and
57
+ // raise/declared-volatile.ts through liftStamped and the offset-name pass), and
58
+ // `keepsUnusedReads` and `discardsUnusedReadsInto` (frontend/dead-reads.ts, the second
59
+ // gathered by frontend/ppc.ts and frontend/mips.ts as they lower a load). The
55
60
  // field names are a SUPERSET of
56
61
  // StructureOptions' — see `structureOptionsFor`.
57
62
  //
@@ -70,6 +75,8 @@ import {
70
75
  IDO_RUNTIME_HELPERS,
71
76
  MIPS_GCC_RUNTIME_HELPERS,
72
77
  PPC_MWCC_RUNTIME_HELPERS,
78
+ PPC_MWCC_SAVE_ROUTINES,
79
+ type RegisterSaveRoutines,
73
80
  type RuntimeHelper,
74
81
  } from './runtime-helpers';
75
82
  import type { StaticLayout } from './structure/local-statics';
@@ -91,6 +98,29 @@ export interface SourceRead {
91
98
  bytes?: number;
92
99
  }
93
100
 
101
+ /** WHERE THIS ABI CAN LEAVE A GENERAL ARGUMENT REGISTER EMPTY BELOW ONE A CALL FILLS — the two ways
102
+ * argument words stop filling in order, read off the facts that already place them:
103
+ * - `skippable`: the registers an argument aligned to 8 can skip, every odd argument word where
104
+ * `evenWordAlignedArgs` holds (O32 `a1`/`a3`, PowerPC `r4`/`r6`/`r8`/`r10`). Compiled on
105
+ * 2026-10-18: `g(1, b, 3LL)` and `g(1, 3LL)` are one object on gcc2.7.2kmc, gcc2.7.2 and the
106
+ * three mwcc builds, `a1`/`r4` untouched under a set pair. agbcc fills every word in order
107
+ * (`g(1, 3LL)` is r0, r1:r2), so it has none.
108
+ * - `leadingFloatShadow`: a leading floating argument travels in the FPU's registers and leaves
109
+ * the general words it counts against holding nothing (`fpu.slots: 'leading'`, O32): with `gp`
110
+ * unprototyped, `gp(x, 5)` of a `float x` parameter and of an `int x` one compile alike on
111
+ * ido7.1, gcc2.7.2kmc and gcc2.7.2 (`li a1,5`, `a0` untouched). PowerPC's floats take no general
112
+ * word (`'separate'`), so they leave no hole there.
113
+ * Its reader is the guessed-arity gap (frontend/call-plan.ts `decideGap`). */
114
+ export function argRegisterHoles(target: Pick<TargetDescription, 'argRegs' | 'evenWordAlignedArgs' | 'fpu'>): {
115
+ skippable: ReadonlySet<string>;
116
+ leadingFloatShadow: boolean;
117
+ } {
118
+ return {
119
+ skippable: new Set(target.evenWordAlignedArgs ? target.argRegs.filter((_, k) => k % 2 === 1) : []),
120
+ leadingFloatShadow: target.fpu?.slots === 'leading',
121
+ };
122
+ }
123
+
94
124
  /** The bytes a source read reaches from an address `off` bytes into the frame, as `[lo, hi)`
95
125
  * relative to that address. The machine may force the address down to a unit boundary — the
96
126
  * GBA's DMA and BIOS both do — so a 32-bit read of the halfword at [sp,#2] reads from [sp,#0],
@@ -202,16 +232,27 @@ export interface TargetDescription {
202
232
  * PARM_BOUNDARY is 32 (:354).
203
233
  *
204
234
  * o32 MIPS has an FPU and a double after an integer argument still takes two general words,
205
- * high first, at an even word as its `long long` does (IDO 7.1). The MIPS targets do not state it
206
- * because their frontend lays out no 64-bit argument (`CallLowering.pairs`). PowerPC EABI passes
207
- * a double in a float register that takes no general word (`fpu.slots: 'separate'`), so it has
208
- * none to state. */
235
+ * high first, at an even word as its `long long` does (`evenWordAlignedArgs`). The MIPS targets
236
+ * do not state it because their frontend lays out no 64-bit argument (`CallLowering.pairs`).
237
+ * PowerPC EABI passes a double in a float register that takes no general word
238
+ * (`fpu.slots: 'separate'`), so it has none to state. */
209
239
  doubleArgWords?: 'high-first';
240
+ /** AN ARGUMENT ALIGNED TO 8 STARTS AT AN EVEN ARGUMENT WORD, and the word it skips is left empty:
241
+ * a `long long`, a `double` in general words, and a struct passed by value whose alignment is 8,
242
+ * alike. `proto.ts` `wordsOf` counts the skipped word, and `aggregate.ts` `byValueCallArgs` reads
243
+ * the same count to place a struct. O32, compiled on ido7.1, gcc2.7.2kmc and gcc2.7.2:
244
+ * `takeLL(1, 0x200000003LL)` and `takeD(1, 2.5)` set `a0` and `a2:a3` with `a1` unset, and so does
245
+ * `takeL(s32 k, struct L { long long v; } l)`; at word 0 each is `a0:a1`. mwcc's PowerPC EABI aligns
246
+ * a `long long` the same way (its doubles take no general word, and a struct passed by value is
247
+ * passed by address). ABSENT ⇒ every argument takes the next free word: agbcc (the note at
248
+ * `wordsOf`). */
249
+ evenWordAlignedArgs?: true;
210
250
  /** Registers this ABI does NOT pass arguments in — half of what makes a def-less live-in read an
211
251
  * uninitialised local rather than an argument. The other half is a measurement the FRONTEND
212
252
  * owes (did this function save the register), and the rule that combines them is in
213
- * frontend/ssa.ts (LiveInModel.uninitRegs). ABSENT ⇒ no register partition is claimed, which is
214
- * what MIPS and PPC take today.
253
+ * frontend/ssa.ts (LiveInModel.uninitRegs). Thumb and PPC state it and measure the save half
254
+ * from their prologues (Thumb `savedRegs`, PPC and MIPS `prologueSavedRegs`). ABSENT ⇒ no register
255
+ * partition is claimed.
215
256
  *
216
257
  * It must be DISJOINT from `argRegs`, and the frontend hands both to the builder so that is
217
258
  * checked rather than trusted (`checkedLiveInModel`): a spelling that lands in both lists used
@@ -219,14 +260,43 @@ export interface TargetDescription {
219
260
  nonArgRegs?: readonly string[];
220
261
  /** Of `nonArgRegs`, the ones this ABI does NOT require a callee to preserve — so the compiler may
221
262
  * home a local in one with no prologue save at all, and the save half of the rule above does not
222
- * apply to it. AAPCS's `ip` is the whole set here, and agbcc really does use it that way.
263
+ * apply to it. AAPCS's `ip` is the whole set on Thumb, and agbcc really does use it that way;
264
+ * on PPC it is `r0`, `r11` and `r12`, where mwcc homes a leaf's uninitialised locals (PPC_MWCC).
223
265
  *
224
266
  * UNDER-stating this list only makes the classification stricter: an unlisted register whose save
225
- * the frontend cannot find falls back to being a parameter, which is what a target claiming no
226
- * partition gets. OVER-stating it is the unsound direction — a callee-saved register listed here
227
- * is classified with no evidence at all, which is the defect the save half exists to close. Every
228
- * entry must appear in `nonArgRegs`; the frontend refuses a target where one does not. */
267
+ * the frontend cannot find is read as one the callee must preserve, and a read of it before any
268
+ * write refuses (frontend/ssa.ts `LiveInModel.unsavedCalleeSavedRegs`). OVER-stating it is the
269
+ * unsound direction — a callee-saved register listed here is classified with no evidence at all,
270
+ * which is the defect the save half exists to close. Every entry must appear in `nonArgRegs`;
271
+ * the frontend refuses a target where one does not. */
229
272
  scratchRegs?: readonly string[];
273
+ /** Of `scratchRegs`, the ones something outside the C calling convention hands an input in. On
274
+ * PPC that is mwcc's RUNTIME: its register save routines take their frame address in r11
275
+ * (`addi r11,r1,N; bl _savegpr_NN`, and `_savegpr_31` is `stw r31,-4(r11); blr`), and
276
+ * `__ptmf_scall` takes its member-function pointer in r12 (`lwz r0,0(r12)`). On O32 it is
277
+ * hand-written asm, and every scratch register (`O32_SCRATCH_REGS`). Asm that reads one of these
278
+ * first is being handed a value, not reading an uninitialised local.
279
+ *
280
+ * What separates the two is MEASURED on 2.4.2 and 2.3.3: the uninitialised local the compiler
281
+ * homes in r11 or r12 is assigned on SOME path (`uninit_r12`: `if (s) { x = a*b; ... }`), so its
282
+ * entry value meets that definition at a join. A def-less read of one of these registers whose
283
+ * entry value no join meets with a definition of its own refuses (frontend/ssa.ts
284
+ * `LiveInModel.hiddenInputRegs`). r0 is not
285
+ * listed: the same compilers read a never-assigned local straight out of r0 (`int x; return x +
286
+ * a;` is `add r3,r0,r3`), and the runtime passes nothing there.
287
+ *
288
+ * Every entry must appear in `scratchRegs`; the frontend refuses a target where one does not. */
289
+ hiddenInputRegs?: readonly string[];
290
+ /** FILE-SCOPE GLOBAL REGISTER VARIABLES this compiler accepts (`register T g __asm__("r8");`):
291
+ * each callee-saved register a translation unit may bind a global to, keyed by its disassembly
292
+ * spelling and valued by the name `__asm__` takes. gcc's `globalize_reg` (agbcc regclass.c
293
+ * :587-611) makes such a register fixed and call-used, so no prologue saves it: a callee-saved
294
+ * register a function reads before writing, or writes, WITHOUT saving it is one of these, and
295
+ * the lift reads and writes it as that global (frontend/ssa.ts `LiveInModel.globalRegs`).
296
+ * `contiguous` is the order a compiler that declares them only as a contiguous run requires
297
+ * them in: declaring one declares every register before it. ABSENT ⇒ the compiler has no
298
+ * spelling for one, and such a read or write refuses (`unsavedCalleeSavedRegs`). */
299
+ globalRegisterVariables?: { regs: Readonly<Record<string, string>>; contiguous?: readonly string[] };
230
300
  /** Registers a CALL destroys: after one, they hold whatever the callee left there, and no value
231
301
  * this function computed. The frontends hand this to the SSA builder so a read of one past a
232
302
  * call refuses instead of resolving to the pre-call definition (frontend/ssa.ts).
@@ -242,6 +312,12 @@ export interface TargetDescription {
242
312
  * construction one the callee may destroy — and `clobberedByCall` (frontend/ssa.ts) checks that
243
313
  * rather than trusting it, the way `checkedLiveInModel` checks the register partition. */
244
314
  callerSaved: readonly string[];
315
+ /** Registers the ABI reserves to the kernel, which an exception handler may overwrite between ANY
316
+ * two instructions (O32's `k0`/`k1`), so no value written to one is proved to be there when it is
317
+ * read. A read of one refuses, and the return proof never counts one as holding the return
318
+ * address (frontend/mips.ts). Only hand-written asm names them. Absent on an ABI that reserves
319
+ * none. */
320
+ kernelRegs?: readonly string[];
245
321
  /** The COMPILER RUNTIME HELPERS this compiler's codegen calls, and what each computes
246
322
  * (runtime-helpers.ts). A compiler fact, which is why it lives here and not in `proto.ts`: that
247
323
  * file holds signatures fixed by the C STANDARD, and a helper name is fixed by a runtime
@@ -256,6 +332,10 @@ export interface TargetDescription {
256
332
  * `__`-prefixed function by spelling — but it buys nothing on a target whose runtime has simply
257
333
  * not been enumerated. */
258
334
  runtimeHelpers?: Readonly<Record<string, RuntimeHelper>>;
335
+ /** The runtime's OUT-OF-LINE REGISTER SAVE ROUTINES (runtime-helpers.ts `RegisterSaveRoutines`).
336
+ * ABSENT ⇒ none is recognised: a `bl` to one is an ordinary call, and the frame address it would
337
+ * have consumed is the stack pointer read as data, which refuses. */
338
+ registerSaveRoutines?: RegisterSaveRoutines;
259
339
  // HARDWARE / ISA facts — independent of the compiler.
260
340
  capabilities: {
261
341
  endianness: 'little' | 'big'; // consumed by structureOptionsFor (bitfield extract recognition is LSB-first)
@@ -454,6 +534,51 @@ export interface TargetDescription {
454
534
  // says so, but nothing in this bag could express it if they did not. Any behavior that can
455
535
  // differ between two toolchains sharing one description is mis-keyed by construction.
456
536
  spillSlotOrder?: 'ascending' | 'descending' | 'unknown';
537
+ // Which way this compiler places the ADDRESS-TAKEN local arrays of one frame against their
538
+ // DECLARATION order: `ascending` = the earlier-declared array takes the lower address. Read
539
+ // where the frame audit declares the one object as several (frame-objects.ts
540
+ // `keepAsOneObject`, which declares the parts `provenParts` finds in this order), so the
541
+ // recompile puts each part back at its own offset. Absent REFUSES that split: the one object
542
+ // stays one array.
543
+ //
544
+ // MEASURED on `u8 a[64]; u8 b[32]; u8 c[48];`, each filled by a `memcpy`, at canonical flags:
545
+ // 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
546
+ // reverse); mwcc_242_81 a@88 b@8 c@40, neither order — so absent there.
547
+ addressTakenArrayOrder?: 'ascending' | 'descending';
548
+ // The frame-offset multiple this compiler starts each address-taken `u8` array of a frame at,
549
+ // whatever its size. Read by the same split (frame-objects.ts `provenParts`): a boundary off
550
+ // this multiple is never where two of the source's arrays met, and a part declared there is
551
+ // placed at the next multiple by the recompile, moving it and growing the frame. Absent
552
+ // REFUSES the split, as `addressTakenArrayOrder` does.
553
+ //
554
+ // MEASURED on `u8 a[na]; u8 b[nb];`, each filled by a `memcpy` and both handed to a callee, at
555
+ // canonical flags, for na x nb = 4x12, 12x12, 6x10, 8x8, 20x16, 2x14: agbcc puts b at sp+4,
556
+ // 12, 8, 8, 20, 4 (4); gcc2.7.2 and gcc2.7.2kmc a@16 and b@24, 32, 24, 24, 40, 24 (8:
557
+ // BIGGEST_ALIGNMENT on MIPS, for a BLKmode local); ido7.1 b@24 and a@36, 36, 40, 32, 44, 44 (4).
558
+ addressTakenArrayAlignment?: 4 | 8;
559
+ // Does this compiler's C front end take a DESIGNATED array element in an initializer —
560
+ // `u8 a[16] = { 1, [8] = 2 };`? A fact about the dialect the compiler parses, not about C or
561
+ // C++ as languages: GNU C had it long before C99, and agbcc and gcc 2.7.2 (KMC's build and the
562
+ // plain one) compile the line above, while IDO 7.1's cfe ("Syntax Error") and every mwcc build
563
+ // the benchmark ships, 2.3.3, 2.4.2 and 2.4.7 ("expression syntax error"), refuse it — each
564
+ // compiled. Read by the C-family printer (`SFn.designators`) where a constant image
565
+ // (l3/constinit.ts) leaves elements out: it spells the element after a run of them by index,
566
+ // and without this spells the run as zeros, which a compiler that cannot parse the index takes.
567
+ // Absent ⇒ zeros, the spelling every C compiler parses.
568
+ designatedInitializers?: boolean;
569
+ // Does this compiler's C front end REFUSE the three pointer conversions C makes implicitly — an
570
+ // integer to a pointer, a pointer to an integer, and an object pointer to one of another
571
+ // pointee — as C++ does? Every mwcc build the benchmark ships refuses all three in C, compiled
572
+ // with `-lang=c`: 2.4.2 and 2.4.7 ("illegal implicit conversion" of an `unsigned int` to an
573
+ // `int *`) and 2.3.3 ("cannot convert"). agbcc, IDO 7.1 and both gcc 2.7.2 builds compile the same three
574
+ // calls at their canonical flags. Read by the C-family printer (`SFn.strictConversions`), which
575
+ // then casts an argument to its callee's declared parameter and a pointer of another pointee
576
+ // into a pointer slot exactly as it does for C++ (backend/cfamily.ts `argConversion`,
577
+ // `legalizePointerWrites`), and by the store of an integer into a declared pointer cell
578
+ // (structure/pointer-spelling.ts `intoPtrCell`, which declines that store under C++, where only a
579
+ // cast to the cell's own type converts). Each cast is the conversion the compiler would
580
+ // otherwise have made, so it changes no instruction. Absent ⇒ C's implicit conversions.
581
+ strictPointerConversions?: boolean;
457
582
  // Arguments past the argument registers are staged into an area this function RESERVES at the
458
583
  // BOTTOM of its own frame — `[sp,#0]` upward, one word each — rather than pushed at the call
459
584
  // site. It is GCC's ACCUMULATE_OUTGOING_ARGS target macro, and it is what makes an outgoing
@@ -476,24 +601,87 @@ export interface TargetDescription {
476
601
  // every call, with every local and spill above it, and the caller never reads a word of it
477
602
  // back after a call — the callee may assign to its stack parameters, so the caller re-stages
478
603
  // an argument before every call. A compiler can stage in its frame
479
- // (`stagesOutgoingArgsInFrame`) and still break this: mwcc may put a local in the words its
480
- // outgoing parameters use (`frontend/ppc.ts`, "A GUESS THAT FILLS EVERY ARGUMENT REGISTER").
604
+ // (`stagesOutgoingArgsInFrame`) and still break this. mwcc does not, though it puts a local in
605
+ // the words a call COULD take as stack arguments where no call takes any — which is why a
606
+ // guess that cannot say whether one does refuses (`frontend/ppc.ts`, "A GUESS THAT FILLS EVERY
607
+ // ARGUMENT REGISTER") — and not where one does: the area starts past them.
481
608
  //
482
- // It is a UNIVERSAL — for every source, the compiler emits no such read — and its one reader
483
- // applies the contrapositive (`frontend/stackargs.ts` `survivorBound`): a word loaded after a
484
- // call before any re-store is not in the area, so it is a local, and so is every word above it,
485
- // which lets a spill live across a call lift, and a call NO declaration covers
486
- // (`docs/level-tower.md`, THE CONTRAPOSITIVE OF A UNIVERSAL IS NOT A BACKWARDS READING). It is
609
+ // It is a UNIVERSAL — for every source, the compiler emits no such read — and its readers apply
610
+ // the contrapositive (`docs/level-tower.md`, THE CONTRAPOSITIVE OF A UNIVERSAL IS NOT A
611
+ // BACKWARDS READING): a word loaded after a call is not in the area, so it is a local, and so is
612
+ // every word above it (`frontend/frame-objects.ts` `storedNeverRead`). An own-frame word no store
613
+ // reaches on any path is a local too, but only the Thumb frontend acts on that
614
+ // (`frontend/thumb.ts`'s own-frame load arm, which reads it as uninitialised), and it was probed
615
+ // on agbcc alone; a MIPS or PPC reader needs its own probe first. A callee that assigned the
616
+ // word as its stack parameter ran before the load, so the load is after a call, and with no
617
+ // call reaching the load there is no such callee. It is
487
618
  // exactly as sound as the universal, so its residue is every producer the universal does not
488
- // cover: hand-written asm that reads its outgoing argument back after a call loses that
489
- // argument silently. The premise is this compiler's, not the ISA's.
619
+ // cover: hand-written asm that reads its outgoing argument back after a call loses that argument
620
+ // silently. The premise is this compiler's, not the ISA's.
490
621
  // Absent ⇒ every staged word stays a candidate argument.
491
622
  //
623
+ // It promises the AFTER-CALL form and no more: a compiler may still load a staged word BEFORE a
624
+ // call — o32 gcc defines REG_PARM_STACK_SPACE, so calls.c's save_fixed_argument_area loads
625
+ // staged reg-parm words around a nested call. The stronger form, no load of the area at all,
626
+ // is its own field (`outgoingAreaNeverLoadedFrom`), stated per compiler and never read off this.
627
+ //
492
628
  // Set on agbcc: thumb.h puts the locals at sp + outgoing_args_size (ACCUMULATE_OUTGOING_ARGS)
493
629
  // and `calls.c` sizes the area as the maximum over all calls; compiled, `spill10` and
494
630
  // `spillarg` keep their spills above the area, and `test/corpus/agbcc-restage.s` holds the
495
- // re-staging and the callee's write. NOT set anywhere else, for the reason above.
631
+ // re-staging, the callee's write, and no load of the area around calls inside arguments. Set
632
+ // on the three MIPS toolchains off their own probe (`test/corpus/probe-o32-frame.c`), where
633
+ // the frame-object audit is the only reader. Set on
634
+ // mwcc off `test/corpus/probe-mwcc-frame.c` on all three builds (`mwcc-frame-probes.test.ts`):
635
+ // nothing but outgoing words lies below the local area, no word there is loaded, and `twice9`
636
+ // stores its ninth argument again before its second call. The frame-object audit is the only
637
+ // reader there too, and on mwcc it is LOAD-BEARING: it is what refuses an undeclared call whose
638
+ // ninth argument the arity guess cannot see (frontend/ppc.ts `measurePpcFrame`).
496
639
  localsAboveOutgoingArea?: boolean;
640
+ // The STRONG form of `localsAboveOutgoingArea`, which it requires: from this offset up, the
641
+ // caller never loads a word of its outgoing area at all — not after a call, and not before one
642
+ // either. The offset counts from the area's start, the frame bottom: `[sp,#0]` on Thumb, `0(sp)`
643
+ // on O32 (whose area opens with the a0..a3 home), `0(r1)` past the push on PowerPC (whose
644
+ // parameter area opens at 8, past the callee's back chain and link-register words). From it up
645
+ // the area holds only what the caller staged for a callee; every argument that contains a call
646
+ // is computed before any word is staged, and nothing is saved there around a nested call. So
647
+ // ANY frame word this function loads at or above the offset is a local, and so is every word
648
+ // above it, since the area is one region at the frame bottom. Its readers apply that
649
+ // contrapositive, at exactly the soundness of the universal. Each frontend measures the lowest
650
+ // such word once (Thumb through `frontend/stackargs.ts` `loadedWordBound`, MIPS in `liftOnce`,
651
+ // PPC `ppcLoadedWordBound`), and every reader takes that number: Thumb's outgoing-argument
652
+ // analysis, `frontend/frame-objects.ts` `storedNeverRead` for its pending stores, the MIPS and
653
+ // PPC undeclared-call guesses (`frontend/mips.ts` `lowerJal`, `frontend/ppc.ts` `bl`), and PPC's
654
+ // unread-slot decline, each weighing only the words beneath it. On MIPS it also caps how far an
655
+ // undeclared call's area can reach (`o32OutgoingArea`'s `widest`), which the local area's
656
+ // partition (`measureO32Frame`) and the owned range of an uninitialised read
657
+ // (`measureOwnedFrame`) both read. A load BELOW the offset proves nothing, and no reader counts
658
+ // it. A word no store reaches on any path needs only the after-call form (see
659
+ // `localsAboveOutgoingArea`).
660
+ // Absent ⇒ only the after-call form is claimed (when `localsAboveOutgoingArea` is set).
661
+ //
662
+ // Its residue is the universal's: hand-written asm that loads a word it staged, then stages it
663
+ // for a call, loses that argument. Each value is MEASURED, and the compiled probes are committed
664
+ // (`packages/core/test/outgoing-area-probes.test.ts` reads every one):
665
+ // • agbcc, 0, off its source: thumb.h defines no REG_PARM_STACK_SPACE, so calls.c:642's
666
+ // save_fixed_argument_area — which loads staged words around a nested call — is not
667
+ // compiled; calls.c:1599-1610 precomputes every argument containing a call; and
668
+ // store_one_arg (calls.c:3420-3470) saves an argument word only when a nested call overlaps
669
+ // it, which the precompute rules out. Compiled, `test/corpus/agbcc-restage.s` loads no word
670
+ // of the area around calls in stack arguments.
671
+ // • ido7.1, gcc2.7.2kmc and gcc2.7.2, 16: `test/corpus/*-outarea.txt` at the rows' levels
672
+ // (ido -O1, -O2 and -O2 -g3; gcc 2.7.2 -O1 and -O2) and at -O0. NOT 0 — ido stages a
673
+ // by-value aggregate's register words in the a1..a3 home and loads them back before the
674
+ // call (`big1`: `lw a1,4(sp)`), and kmc and gcc 2.7.2 do it for a char-array struct (`d3`,
675
+ // `d5`): o32 gcc defines REG_PARM_STACK_SPACE, and GCC 2.9's save_fixed_argument_area
676
+ // (calls.c:648-711) covers only `[0, reg_parm_stack_space)`. No probe loads a word from
677
+ // 16(sp) up the area.
678
+ // • mwcc, 8, on all three builds at the rows' flags and at -O0,p, and on mwcc_242_81 at -O4,s
679
+ // too: no probe loads a word of the parameter area; a struct-return temp or a by-value copy
680
+ // sits at 8(r1) only where no call takes a stack word, and above the area beside one that
681
+ // does.
682
+ // Like `stagesOutgoingArgsInFrame`, a second compiler of the same ISA must state this premise
683
+ // rather than inherit it.
684
+ outgoingAreaNeverLoadedFrom?: number;
497
685
  // A frame of EXACTLY ONE reserved word whose base escapes this function holds that object and
498
686
  // nothing else. It is the layout premise the Thumb frontend's address-taken-local capability
499
687
  // rests on (`frontend/thumb.ts`, `capturedObjectIsTheWholeFrame`): a bare `mov rD, sp` names
@@ -513,6 +701,94 @@ export interface TargetDescription {
513
701
  // producer a one-word frame does NOT exclude is a <=4-byte non-integer-like struct return,
514
702
  // which the post-lift audit settles per call rather than by size.
515
703
  oneWordFrameIsTheCapturedObject?: boolean;
704
+ // A frame address this function hands out — in an argument register at a call, or stored to
705
+ // memory — is the address of a LOCAL, so its receiver may read the frame from there up and no
706
+ // word from there up is a call's outgoing argument. Its first reader is the outgoing-argument
707
+ // analysis (`frontend/stackargs.ts`, the `escape` event): a store never reloaded HERE is read
708
+ // THERE. Absent ⇒ a handed-out frame address explains nothing.
709
+ //
710
+ // Set on agbcc, compiled at 2.9-arm-000512 and its canonical flags, and re-measurable from the
711
+ // committed probe (`scripts/regen-outgoing-area-probes.ts` → `test/corpus/agbcc-escape.s`, read
712
+ // by `frame-escape-probes.test.ts`). An address-taken local sits ABOVE the outgoing area
713
+ // (`f1`: `add r0, sp, #4` beside `h5(1,2,3,4,x)`), and at [sp,#0] only when no call in the
714
+ // function takes a stack word (`f0`).
715
+ //
716
+ // Its RESIDUE is a by-value struct argument's block copy (`mov r0, sp` / `add r0, sp, #4`, then
717
+ // `bl memcpy`), whose base IS the outgoing area. agbcc stages a call's outgoing words in
718
+ // ASCENDING offset order, the copy included (`qh5`, `a2`, `a3`): a word staged before the copy
719
+ // lies below its base, and one staged after it is still pending at the call. That is why the
720
+ // event ends pending words PER PROGRAM POINT and only from its offset up — read
721
+ // flow-insensitively, it would set aside `qh5`'s fifth `h5` word and lift `h5(1, 2, 3, 4)`. The
722
+ // cost is false alarms that decline loud (`if (x) gt(&s); else h(y);`); where the analysis also
723
+ // reads the event whole-function, it only refuses or excuses a word no call takes
724
+ // (stackargs.ts). `oneWordFrameIsTheCapturedObject` is the flow-insensitive licence for any
725
+ // receiver, sound only where no block copy fits.
726
+ //
727
+ // Its SECOND reader is the Thumb frame model's [sp,#0] capture (`frontend/thumb.ts`, beside
728
+ // `noCallTakesAStackWord`), which reads the universal whole-function at offset 0: where the
729
+ // frame base is handed as the hidden pointer of a callee DECLARED to return a struct through
730
+ // memory, it is a local's address, so no word of the frame is any call's outgoing argument and
731
+ // the area is empty — whatever the other calls are (sret2knr: `u8 rnd();` beside two temps).
732
+ // This premise is read only where some call may take a stack word: where none does there is
733
+ // no outgoing area to confuse, and an escaping frame base is a local's address on any target,
734
+ // whoever receives it (a DMA source published to the device's registers, `dmafill_wh`).
735
+ // The residue cannot reach it: a block copy's base goes to `memcpy`, no struct-returning
736
+ // callee. That reading is agbcc's layout, and is measured beyond `f0`: thumb.h:573
737
+ // STARTING_FRAME_OFFSET 0 and :620-621 add `current_function_outgoing_args_size` to every
738
+ // elimination to sp (:628 ACCUMULATE_OUTGOING_ARGS); function.c:2629 puts STACK_DYNAMIC_OFFSET
739
+ // past the same size; calls.c:1598-1609 precomputes into a temp every argument containing a
740
+ // call whenever the call has stack arguments, so a struct result is never built straight into
741
+ // an outgoing word. Compiled at the rows' flags: `mk(..)` by value as the 5th argument, as the
742
+ // 1st followed by four ints, `mk(..).a` as the 5th, a 20-byte struct by value, and a 5-argument
743
+ // K&R `rnd(1, 2, 3, 4, 5)` beside a temp each put the temp at sp+4 or higher, with [sp,#0] the
744
+ // outgoing word. A frame whose sp moves between two accesses is refused before this is read
745
+ // (thumb.ts `frameDepthBlocker`). Hand-written asm that builds a struct return at [sp,#0] and
746
+ // also stores a stack argument there declines; one that hands that word, unstored, to an
747
+ // undeclared callee as its fifth argument loses it from the callee's guessed arity.
748
+ escapedFrameAddressNamesALocal?: boolean;
749
+ // WHERE AN O32 FUNCTION KEEPS ITS LOCALS in the one frame `addiu sp,sp,-N` reserves — the area
750
+ // the MIPS frontend hands the frame-object audit as `ownedLocals`/`declaredLocals`. Below it
751
+ // lies the OUTGOING area, the 16-byte home of a0..a3 every calling function reserves and then
752
+ // the stack words its widest call takes (none in a function that makes no call); the saved
753
+ // registers and the locals share the rest, and which way round is the compiler's:
754
+ // 'above-outgoing-area' — from the outgoing area's end, rounded up to the 8-byte stack
755
+ // alignment, to the lowest saved register;
756
+ // 'above-saved-registers' — from just above the highest saved register (or the outgoing
757
+ // area's end, where that is higher) to the frame top N.
758
+ // Spill temps live in the same area on both, so it is the locals' and the spills' together.
759
+ //
760
+ // MEASURED off `corpus/probe-o32-frame.c` (`scripts/regen-o32-frame-probes.ts`, read by
761
+ // `o32-frame-probes.test.ts`): a leaf's array at sp+0 on all three MIPS toolchains; with a call
762
+ // taking no stack word, gcc 2.7.2 (both builds) at sp+16 under the saves and IDO at the frame top
763
+ // above `ra`; with one stack word, gcc at sp+24 (20 rounded) and IDO still at the top; and IDO's
764
+ // spill of a value live across a call between `ra` and the array.
765
+ //
766
+ // Absent ⇒ no partition is claimed, and a captured frame address keeps its refusal.
767
+ o32LocalArea?: 'above-outgoing-area' | 'above-saved-registers';
768
+ // WHERE A FUNCTION THAT PUSHES ONE FRAME KEEPS ITS LOCALS, on a target whose frame starts with a
769
+ // fixed LINKAGE area — the back chain and the callee's link-register save word, which the ABI
770
+ // fixes and the frontend states (`frontend/ppc.ts` `PARAM_AREA_AT`, where the caller's parameter
771
+ // area starts). Above it the OUTGOING area holds the stack words of the widest call (none in a
772
+ // function whose calls take none), and the locals and spill temps share what is left below the
773
+ // lowest saved register:
774
+ // [linkage + 4 * (stack words of the widest call), lowest saved register)
775
+ // in post-push offsets — O32's `above-outgoing-area` rule (`o32LocalArea`) past a linkage area
776
+ // instead of the a0..a3 home, without the 8-byte rounding (`areaAboveOutgoing`). The PPC
777
+ // frontend hands it to the frame-object audit as `ownedLocals`/`declaredLocals`; a call it
778
+ // lowers takes no stack word, so it refuses one whose declaration may and draws the area from
779
+ // the linkage's end. No object start inside it is fixed: the compiler chooses where in the area
780
+ // each local goes.
781
+ //
782
+ // 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
783
+ // `corpus/probe-mwcc-frame.c` (`scripts/regen-mwcc-frame-probes.ts`, read by
784
+ // `mwcc-frame-probes.test.ts`): every local and every home or spill lies in the area, a leaf
785
+ // with a local still pushes a frame whose area starts at 8, one or two stack words of a call push
786
+ // the locals past them, and the widest call sizes the area for every call. The first local's
787
+ // offset inside it differs per build and per function (8, 12 or 16 on 2.3.3; a homed parameter
788
+ // takes 8 at -O0,p), which is why this is an area and not a start.
789
+ //
790
+ // Absent ⇒ no partition is claimed, and a captured frame address keeps its refusal.
791
+ linkageLocalArea?: 'above-outgoing-area';
516
792
  // How this compiler calls through a register: `bl <prefix><reg>` enters a runtime thunk that
517
793
  // branches to the address in <reg> with every register as the caller left it, so the call is
518
794
  // an indirect call through <reg> and the thunk is no function a source names. `regs` are the
@@ -521,6 +797,16 @@ export interface TargetDescription {
521
797
  //
522
798
  // ABSENT ⇒ a `bl` names a function on this target, whatever it is called.
523
799
  callThunks?: { readonly prefix: string; readonly regs: readonly string[] };
800
+ // How this compiler marks a BRANCH it had to spell as a call: a `bl <label>` whose line carries
801
+ // a comment matching `comment` (tested against the text from its `@` to the end of the line) is
802
+ // a relay to that label of the same function, not a call. Two of its facts are this compiler's,
803
+ // and the Thumb frontend's lowering rests on them (`lowerFarJumpRelays`): what the mark means,
804
+ // and that the compiler pushes lr in every function with one, so the lr the relay writes is
805
+ // never the one the function returns through.
806
+ //
807
+ // ABSENT ⇒ no comment makes a `bl` anything but a call: a `bl` to a label of the function keeps
808
+ // its decline, however its line is commented.
809
+ farJumpRelay?: { readonly comment: RegExp };
524
810
  // Regime-A switch recovery: accept a RELATIONAL test as a case where the scrutinee values that
525
811
  // can reach it — narrowed by the tests above it — leave exactly one on a side this compiler's
526
812
  // dispatch lands a case body on: `'taken'` for its BRANCH only, `'either'` for both sides
@@ -629,6 +915,34 @@ export interface TargetDescription {
629
915
  // and PPC lanes put the addend in the instruction by construction (`lui`/`%lo`, `lis`/`ori`),
630
916
  // so a surviving offset carries no information there. Absent ⇒ the row is never offered.
631
917
  foldsConstAddrOffset?: boolean;
918
+ // Does a STORE to a struct member past the ISA's displacement range compile differently from the
919
+ // same store through a cast of the base (`p->f38 = v` against `*((u8 *)p + 38) = v`)? agbcc's
920
+ // does: expand_assignment (gcc/expr.c) expands only the base of a COMPONENT_REF target (:2535)
921
+ // and evaluates the right-hand side first in store_field (:2624), and it expands a cast target
922
+ // whole (:2689) before the right-hand side (:2747). Measured on agbcc for a loaded, a constant, a
923
+ // computed and a parameter right-hand side: every pair builds two objects, and a hoisted
924
+ // `u8 *d = &p->f38; *d = v;` builds the cast's. True reads a moved store whose right-hand side
925
+ // is computed before its address as struct evidence (raise/structs.ts `memberStoreOrder`).
926
+ // ido, KMC, GCC 2.7.2 and the three mwcc builds put the two spellings in one object, a loaded
927
+ // right-hand side included, so they leave it absent. `/far-cast`, the lift that keeps such a
928
+ // store on its materialised address, is offered on every target and does not read this.
929
+ farMemberStoreOrder?: boolean;
930
+ // Does a member past the displacement range get its OWN materialised base at every access, so
931
+ // that one base read or written at two distinct offsets is a pointer the source held? Stated
932
+ // as the displacement `past` which that holds: a base materialised with a displacement whose
933
+ // magnitude exceeds it was built for one access. agbcc's is 0, every base: `p->f38 + p->f39`
934
+ // compiles to `add r0, #0x26; ldrb [r0]` and `add r1, #0x27; ldrb [r1]`, while
935
+ // `u8 *d = &p->f38; d[0] + d[1]` shares one `add r1, #0x26` between `ldrb [r1]` and
936
+ // `ldrb [r1, #1]`, and only the pointer spelling rebuilds those bytes. ido's is 32767, the
937
+ // `addiu` reach: inside it ido shares one `addiu at, a0, 32767` across adjacent members
938
+ // (`sb a1, 7232(at); sb a2, 7233(at)`), and past it the assembler expands every member access
939
+ // to its own `lui at, 3; addu at, at, a0` while a held `u8 *d = &p->b` (b at 200000) shares one
940
+ // `lui at, 3; addu v0, a0, at` between `sb t6, 3392(v0)` and `sw zero, 3396(v0)`. True refuses
941
+ // raise/structs.ts's displaced-access fold for such a base (`held-pointer-offsets`). The three
942
+ // mwcc builds share one `addis` base across members and a held pointer alike, and KMC and
943
+ // GCC 2.7.2 lift the held pair as members at the same bytes, so they leave it absent. Absent ⇒
944
+ // the fold reads a shared base as members.
945
+ farMemberBasePerAccess?: { past: number };
632
946
  // Does this compiler fold a pointer local's OWN ADVANCE back into the memory operand —
633
947
  // `*p = a; p = p + 1; *p = b;` → `strh [r3, #0]` + `strh [r3, #2]`, no `add` — so the advanced
634
948
  // spelling emits the indexed one's stores wherever the pointee is not volatile? agbcc does, on
@@ -654,6 +968,25 @@ export interface TargetDescription {
654
968
  // does not declare it: the matching suite compiles each witness on one compiler. Absent ⇒
655
969
  // offered.
656
970
  pointerIntConversionIsFree?: boolean;
971
+ // Does this compiler keep the constant addend of a POINTER sum's offset where the source put it,
972
+ // and move an INTEGER sum's out to the base? agbcc does: `(u8 *)((u32)g + ((a << 4) + 772))`
973
+ // computes `(g + 772) + (a << 4)` (fold-const.c:5033-5058, `A + (V + C)` → `(A + C) + V`), while
974
+ // `(u8 *)g + ((a << 4) + 772)` computes `g + ((a << 4) + 772)` (c-typeck.c:2663-2692 distributes
975
+ // the constant, fold-const.c:4997-5011 folds it back), which is what the project's own
976
+ // `g->arr[a]` compiles to. Compiled at each toolchain's canonical flags, with `g` a struct
977
+ // pointer, a `void *` and a `u32`, as a call argument and under a dereference: agbcc,
978
+ // gcc2.7.2kmc and gcc2.7.2 build the two spellings into two objects wherever the offset has a
979
+ // constant addend and into one where it has none; ido7.1 builds one object every time, and
980
+ // mwcc_242_81 is CodeWarrior's index-first order (structure/pointer-spelling.ts, THE INTEGER
981
+ // SUM). Two rules read it. True ⇒ the arithmetic table's value row (ARITH_ROWS) spells an
982
+ // undeclared pointer global plus an integer with a constant term as the pointer sum where the
983
+ // sum is a value rather than an access's address; and the structurer declares `u8 *` a temp
984
+ // holding a pointer global's value that is the base of such a sum (`declaresBytePointer`),
985
+ // which an integer temp would compile as `(t + K) + x`. Through a temp ido7.1 does build two
986
+ // objects, the byte pointer's index first where the integer temp and the struct source keep the
987
+ // base first, and mwcc_242_81 builds one (the cli matching suite's pointer-spelling.test.ts).
988
+ // Absent ⇒ the integer sum and the integer temp.
989
+ keepsPointerSumAddend?: boolean;
657
990
  // Does this compiler EMIT a memory read in the block the source SPELLED it in? One direction
658
991
  // only: the def-block placement rule (StructureOptions.readsStayWhereWritten) re-spells a read
659
992
  // at the block the asm performed it in, which reproduces the asm iff nothing sinks a spelled
@@ -692,6 +1025,16 @@ export interface TargetDescription {
692
1025
  // three descriptions that measured false (four compilers) set it anyway, so absent means
693
1026
  // UNMEASURED rather than "no".
694
1027
  reloadsLocalReread?: boolean;
1028
+ // Does this compiler convert a constant to a NARROW lvalue's type before it materializes it
1029
+ // for the store? Then the stored register's upper bits say the lvalue's signedness whenever
1030
+ // the narrow sign bit is set: `u16 *p; p[6] = -1;` loads 0xffff and `s16 *p; p[6] = 0xFFFF;`
1031
+ // loads -1. Measured on every bench compiler at its rows' flags by lifting the probe in
1032
+ // packages/cli/test/matching/narrow-store-constant.test.ts and reading the const each store
1033
+ // takes. Read by raise/storesign.ts (through raise/pre-recovery.ts).
1034
+ //
1035
+ // ABSENT ⇒ the seed stands down and a narrow store keeps the unsigned view; a compiler opts IN
1036
+ // on its own probe.
1037
+ narrowStoreConstantTakesLvalueType?: boolean;
695
1038
  // A LAYOUT fact rather than a canonicalization one, kept here because it is the COMPILER's:
696
1039
  // the size, in bytes, it aligns and rounds EVERY struct and union to, whatever its members.
697
1040
  // agbcc: 4 — `sizeof(struct { u16 h; })` is 4, and `struct { u8 a; union { u16 h; u8 b[2]; } u; }`
@@ -699,12 +1042,13 @@ export interface TargetDescription {
699
1042
  // answer 2 and 2: the natural layout, 1. Compiled as `return sizeof …` / `return (int)&((T *)0)->m`
700
1043
  // at each row's flags; gcc2.7.2 (-O1), mwcc_233_163n and mwcc_247_107 answer the same as their
701
1044
  // descriptions' measured compilers. Read by raise/structs.ts (through raise/pre-recovery.ts),
702
- // which sizes a recovered UNION member with it; raise/struct-arrays.ts sizes element structs
703
- // without it, which on agbcc is a known gap.
1045
+ // which sizes a recovered UNION member with it, and by raise/struct-arrays.ts, which declines
1046
+ // an element struct whose stride it would round (agbcc: a stride-6 `u16 [3]` row is 8 bytes).
704
1047
  //
705
- // ABSENT ⇒ unmeasured, and the union recovery declines every union narrower than a word: no
706
- // value is safe to assume, since one too small mislays the fields after it on agbcc and one too
707
- // large drops the pad in front of them everywhere else.
1048
+ // ABSENT ⇒ unmeasured: the union recovery declines every union narrower than a word, and the
1049
+ // struct-array recovery declines every element struct. No value is safe to assume, since one
1050
+ // too small mislays the fields after it on agbcc (and mis-strides an element struct) and one
1051
+ // too large drops the pad in front of them everywhere else.
708
1052
  aggregateBoundary?: number;
709
1053
  // The largest alignment any member of a struct takes, in bytes (BIGGEST_ALIGNMENT): agbcc 4,
710
1054
  // thumb.h:358, so a `long long` member sits at a word. With `aggregateBoundary` it is what sizes
@@ -712,8 +1056,8 @@ export interface TargetDescription {
712
1056
  largestAlignment?: number;
713
1057
  // HOW A STRUCT OR UNION RETURNED BY VALUE COMES BACK, which decides whether a call to a function
714
1058
  // declared to return one hands it a hidden pointer as argument 0 and moves every declared
715
- // argument one register up (`aggregate.ts` `returnsInMemory`, read by frontend/thumb.ts and
716
- // frontend/ppc.ts).
1059
+ // argument one register up (`aggregate.ts` `returnsInMemory`, read by the call lowering every
1060
+ // frontend shares, frontend/call-plan.ts).
717
1061
  // • 'apcs' — in memory when bigger than a word, when a struct has a second member that is not
718
1062
  // a bitfield, or when a union has a member that would be; in the return register otherwise.
719
1063
  // agbcc, thumb.c:1423-1493 (compiled: `{u8 a,b,c,d}` through memory; `{u32}`, `{u32 w[1]}`,
@@ -722,10 +1066,55 @@ export interface TargetDescription {
722
1066
  // included); in memory otherwise. mwcc_242_81, mwcc_233_163n and mwcc_247_107 alike
723
1067
  // (compiled: 1 to 8 bytes, `{float}`, `{double}` and a union come back in r3/r3:r4; a 9- and a
724
1068
  // 12-byte struct are handed `addi r3,…` ahead of the call).
725
- // ABSENT ⇒ unmeasured, and such a call declines. IDO 7.1 returns every aggregate through memory,
726
- // a one-word one included (compiled: `struct {u32 x;} mkw(s32)` is called `addiu a0,sp,28 / jal
727
- // mkw / lw v0,28(sp)`), and states nothing: the MIPS frontend lowers no call to read it.
728
- aggregateReturn?: 'apcs' | 'svr4';
1069
+ // • 'memory' — always in memory, whatever its size or members. ido7.1, gcc2.7.2kmc and gcc2.7.2
1070
+ // alike (compiled: `{u8}`, `{u16}`, `{u32}`, `{float}`, `{double}`, 4-, 8- and 12-byte
1071
+ // structs, `union {u32; u16;}` and `{u32 a:8; u32 b:8;}` are each handed `addiu a0,sp,N`
1072
+ // ahead of the `jal`, with the declared arguments from a1 on, and the callee returns that
1073
+ // pointer, `move v0,a0`).
1074
+ // Under every rule the caller's half is lowered (frontend/call-plan.ts), and a function that
1075
+ // itself returns a struct through memory lifts as that struct where its declaration says so and
1076
+ // the lift proves the hidden pointer is only its storage (frontend/own-struct-return.ts); it is
1077
+ // read as the pointer function it compiles to where nothing declares it, or where a context's
1078
+ // declaration does and the proof fails (pipeline.ts `returnOwnStruct`).
1079
+ // ABSENT ⇒ unmeasured, and such a call declines.
1080
+ aggregateReturn?: 'apcs' | 'svr4' | 'memory';
1081
+ // WHETHER A FUNCTION THAT RETURNS A STRUCT THROUGH MEMORY HANDS THE HIDDEN POINTER BACK in the
1082
+ // return register — a COMPILER fact, which no rule above implies. `true` for ido7.1, gcc2.7.2kmc
1083
+ // and gcc2.7.2 (`move v0,a0`, the O32 rule) and for agbcc (`add r0, r6, #0` after a call, r0
1084
+ // left alone in a leaf: compiled `struct S12 f(s32 x)` with and without a call to `fill(t)`).
1085
+ // `false` for mwcc_242_81, mwcc_233_163n and mwcc_247_107, whose callee clobbers r3 before the
1086
+ // `blr`: the same `struct S12 b4(s32 x)` saves the pointer in r30/r31, stores through it, and
1087
+ // returns with r3 = a reload of a frame word. It decides what the proof that a function's own
1088
+ // struct return goes through that pointer asks of each `ret` (frontend/own-struct-return.ts):
1089
+ // where it is `true` every `ret` must hand back the pointer, and where it is `false` what the
1090
+ // return register holds is no part of the function, save a value computed for the return alone
1091
+ // (`returnedAlone`). ABSENT ⇒ unmeasured, and read as `true`, which only refuses more.
1092
+ structReturnHandsBackPointer?: boolean;
1093
+ // WHETHER A CALLER HANDS ITS OWN DESTINATION POINTER STRAIGHT ON as a callee's hidden
1094
+ // struct-return pointer, leaving the first argument register at its incoming value below the
1095
+ // arguments it sets up. `true` for ido7.1: `int b3(struct S *p){ *p = sg(1); return 0; }` is
1096
+ // `jal sg; li a1,1` with a0 never touched, the bytes of `sg(p, 1)` with its result unused.
1097
+ // `false` for gcc2.7.2kmc, gcc2.7.2, agbcc and mwcc_242_81, mwcc_233_163n and mwcc_247_107,
1098
+ // which copy through a frame temporary (a0/r0/r3 set to a frame address ahead of the call).
1099
+ // Compiled at the canonical flags, `b3` and `void b6(struct S *p, int k){ *p = sg(k, 5); }`.
1100
+ // Read by the guessed-arity gap (frontend/call-plan.ts `decideGap`): where it holds, a gap at
1101
+ // the first argument register is decided only where the call's result is read, which no
1102
+ // forwarded struct return's is (IDO reloads `p` from its home after the call in all five shapes
1103
+ // compiled: `return (*p = sg(1)).b`, `return p` and three more). ABSENT ⇒ unmeasured, and read
1104
+ // as `true`, which only refuses more.
1105
+ forwardsStructReturnDestination?: boolean;
1106
+ // A STRUCT PASSED BY VALUE TRAVELS IN CONSECUTIVE ARGUMENT WORDS, the first at the next free
1107
+ // argument word, its size rounded up to words (`aggregate.ts` `byValueCallArgs`). agbcc,
1108
+ // thumb.h:632-650 (FUNCTION_ARG places by word offset, FUNCTION_ARG_PARTIAL_NREGS splits a
1109
+ // BLKmode argument across r3 and the stack, FUNCTION_ARG_ADVANCE rounds its size to a word, and
1110
+ // no FUNCTION_ARG_PASS_BY_REFERENCE is defined); compiled, `struct A {u16 a,b,c,d; s8 e,f;}`
1111
+ // is `ldr r0..r2` off the frame copy ahead of `bl take2`. O32 too, compiled on ido7.1,
1112
+ // gcc2.7.2kmc and gcc2.7.2: `takeQ(s32 k, struct Q {u32 a, b;})` is passed `a1:a2` and a
1113
+ // three-word struct `a0..a2` (an 8-aligned one starts at an even word:
1114
+ // `evenWordAlignedArgs`); IDO also stores each word into its home slot, which the
1115
+ // call owns. ABSENT ⇒ the target passes one some other way or states nothing: mwcc passes the
1116
+ // address of a copy (frontend/ppc.ts refuses it).
1117
+ byValueAggregateArgWords?: boolean;
729
1118
  // The bytes an enum takes as a struct member (`aggregate.ts`). agbcc: 4 — flag_short_enums is 0
730
1119
  // unless `-fshort-enums` is given (toplev.c:3552-3554 with no DEFAULT_SHORT_ENUMS), so an enum
731
1120
  // whose values fit an int is an int (c-decl.c:6123-6135); compiled, `sizeof(enum {K0, K1})`
@@ -734,6 +1123,16 @@ export interface TargetDescription {
734
1123
  // (proto-context.ts). ABSENT ⇒ unmeasured,
735
1124
  // and an aggregate with an enum member is not sized.
736
1125
  enumBytes?: number;
1126
+ // Whether a plain `char` is signed, read where a context declares a global `char` and the lift
1127
+ // asks whether a narrow read of it agrees with that declaration (structure.ts
1128
+ // `storageDisagrees`). The compiler's default, with no flag; `targetFor` applies the flags that
1129
+ // change it (`-funsigned-char`/`-fsigned-char`, IDO's `-signed`, mwcc's `-char`). MEASURED,
1130
+ // `s32 g(void){ return gC; }` over `extern char gC;` at each toolchain's canonical flags: agbcc
1131
+ // `ldrb` and ido7.1 `lbu` (unsigned), gcc2.7.2 `lb` (signed; gcc2.7.2kmc's canonical
1132
+ // `-funsigned-char` makes it `lbu`), mwcc_242_81 `lbz` + `extsb` (signed). ABSENT ⇒ unmeasured,
1133
+ // and a narrow read of a declared plain `char` agrees with neither sign, so it is spelled
1134
+ // through its address.
1135
+ plainCharSigned?: boolean;
737
1136
  // How a struct's bitfields are placed (`aggregate.ts`). 'contiguous' — each at the bit after the
738
1137
  // one before, straddling a byte or a word of its declared type, with the next member that is not
739
1138
  // a bitfield at the first byte past it that its alignment allows. agbcc: thumb.h defines no
@@ -784,9 +1183,207 @@ export interface TargetDescription {
784
1183
  // ABSENT ⇒ false: a sign-extending load says nothing about the qualifier, and a device read is
785
1184
  // pinned, and a read of a declared object placed, whatever its extension.
786
1185
  volatileReadsExtendInRegister?: boolean;
1186
+ // Does this compiler RELOAD a cell stored through one spelling and read back through another —
1187
+ // two names, or a name and a numeric address, for the same byte? MEASURED at -O2, each probe
1188
+ // storing 5 to a word, 7 to one of its bytes, and reading the word back: IDO 7.1 reloads
1189
+ // (`D_80100000 = 5; ((u8 *)0x80100000)[1] = 7; return D_80100000;` is `sb; jr ra; lw v0`);
1190
+ // agbcc (`sRandCount = 5; ((u8 *)&sUnknown)[4] = 7; return sRandCount;` is `strb; mov r0,
1191
+ // #5`) and gcc 2.7.2 (`sb; jr ra; li v0, 5`) forward the stored value. IDO also reloads across
1192
+ // two externs, two statics, an array element beside a scalar, and a word-wide literal beside
1193
+ // a name. It reloads only a read that FOLLOWS the store: a read placed before a byte store
1194
+ // through another name is sunk past it (`v0 = D_80100004; ((u8 *)&D_80100000)[5] = 7;
1195
+ // return v0;` is `sb; jr ra; lw v0`). So where it holds, a pair of spellings in one block
1196
+ // outside any loop, the store first, is faithful lifted as-is, whether the asm carries both or
1197
+ // the map chose one; any other order, and every pair where it does not hold (an asm that went
1198
+ // back to memory proves the source did not spell the byte two ways), raise/offsetnames.ts
1199
+ // `cellSpelledTwice` declines.
1200
+ //
1201
+ // ABSENT ⇒ false: the compiler forwards (or is unmeasured, which declines the same pairs).
1202
+ reloadsAcrossSpellings?: boolean;
1203
+ // Does this compiler, AT THESE FLAGS, keep a plain read whose value nothing uses? A FLAG fact,
1204
+ // never written in a description: `targetFor` sets it from the optimisation level
1205
+ // (`keepsUnusedReadsAt`), so a target at other flags has it absent. Where it holds, a read
1206
+ // the lift finds unused is no evidence of `volatile`, and the dead-read pin
1207
+ // (frontend/dead-reads.ts) stamps only one the machine loaded into the register the compiler
1208
+ // discards a volatile read into (`discardsUnusedReadsInto`).
1209
+ //
1210
+ // ABSENT ⇒ false: the compiler deletes every plain read nobody uses, so one still in the target
1211
+ // was qualified.
1212
+ keepsUnusedReads?: boolean;
1213
+ // WHERE DOES THIS COMPILER PUT A READ NOBODY USES, at a level that keeps a plain one
1214
+ // (`keepsUnusedReads`)? The register a `volatile` read with no consumer is loaded into, and no
1215
+ // plain local is ever homed in. COMPILED at the keeping levels, one statement per line:
1216
+ // `*p;`, `s->b;`, `gV;` and `(*p, 1)` through a volatile lvalue load into `r0` on mwcc 2.4.2,
1217
+ // 2.4.7 and 2.3.3 at `-O0,p`, and into `$zero` on IDO 7.1 at `-O1` and `-O0`; the plain
1218
+ // `x = p[5]; x = 0;` takes a home there instead (`lwz r31`, `lw t6` and a frame store), and so
1219
+ // does a `register` local (`lwz r31`, `lw a2`). Over the 409 complete Mario Party 4 units built
1220
+ // at `-O0,p`, 924 non-frame loads nothing reads land in r17-r31 and f18-f31, and exactly one in
1221
+ // r0: WipeExecAlways's `wipe->type;`, a `volatile u8` member. So a dead load into this
1222
+ // register is a qualified read, and the dead-read pin (frontend/dead-reads.ts) stamps it at a
1223
+ // keeping level too. mwcc keeps one plain read in r0 as well, a read under an arithmetic or a
1224
+ // cast nothing uses (`p[5] + 1;`, `(char)p[5];`), which no real unit holds: that is the pin's
1225
+ // residue. A load this register took and an op then wrote to a home (`lwz r0` + `extsb r31,r0`,
1226
+ // a plain `s8` local) is not discarded (frontend/dead-reads.ts `leftInDiscardRegister`). One
1227
+ // register cannot name a pair: IDO discards a doubleword into zero:at, `lw zero,0(a0)` +
1228
+ // `lw at,4(a0)`, which frontend/mips.ts `emitLoad` reads as the pair this register heads.
1229
+ //
1230
+ // ABSENT ⇒ the compiler has no such register, or it is unmeasured. agbcc, gcc 2.7.2 and KMC
1231
+ // gcc 2.7.2 at `-O0` have none: a volatile `*p;` is an `ldr r0`/`lw v0` nothing stores, the
1232
+ // same instructions as a `register` local assigned and overwritten, so the pin cannot tell the
1233
+ // two apart there and refuses rather than drop the read.
1234
+ discardsUnusedReadsInto?: string;
1235
+ // MAY A DISCARDED READ LAND IN AN ARGUMENT REGISTER, at flags that delete a plain read nobody
1236
+ // uses? Where it may, a value a load left in an argument register is no proof that the next
1237
+ // call takes it. The discard register is the allocator's first free one, not a fixed register,
1238
+ // so a single read on its own says nothing: the probe that decides is one where the usual
1239
+ // discard register is busy. `true` for agbcc, which loads a discarded `volatile` read into the
1240
+ // lowest free argument register (`*(volatile int *)K; g(s)` is `ldr r1,[r1]; bl g`, the bytes of
1241
+ // `g(s, *(int *)K)`). `true` for gcc2.7.2kmc and gcc2.7.2, which discard into v0/v1 until those
1242
+ // are busy and then into a1: `int a = ga * 3; *(volatile int *)&gx; *(volatile int *)&gx;
1243
+ // ga = a + 1; return logv(s) + 1;` is `lw a1,0(v0); lw v0,0(v0); ... jal logv` (`lw a1,0(v1)` on
1244
+ // gcc2.7.2), the bytes of the twin that passes the first read. `true` for mwcc_242_81,
1245
+ // mwcc_233_163n and mwcc_247_107, which discard into r0 until it is busy and then into r4: the
1246
+ // same function's first read is `lwz r4,gx` on all three, and with three reads mwcc_233_163n's
1247
+ // last one is `lwz r4,gx` right before `bl logv`, byte-identical to the twin that passes it.
1248
+ // `false` for ido7.1 alone: `lw zero`, `lbu zero`, `lh zero`, and `lw zero` + `lw at` for a
1249
+ // doubleword, with four discarded reads stacked under the same pressure too. Compiled at the
1250
+ // canonical flags. Read by the guessed-arity gap (frontend/call-plan.ts `decideGap`), which a
1251
+ // load decides only where this is `false`. ABSENT ⇒ unmeasured, and read as `true`, which only
1252
+ // refuses more.
1253
+ discardedReadMayFillArgRegister?: boolean;
1254
+ // DOES THE COMPILER CROSS-JUMP RETURN TAILS? Where two arms end in the same code before the
1255
+ // same return, does it keep ONE copy and jump the other arm into it? Where it does, the source
1256
+ // may have written the tail into every arm (`h(10, 0); return;` in each) and the object still
1257
+ // holds it once. So a join in the CFG is no evidence the source wrote one. What tells the two
1258
+ // spellings apart is where the surviving copy sits, and which arms became one block. Read by
1259
+ // the return-tail chooser (pipeline.ts `structureReturnTailReading`): it publishes the copies
1260
+ // where the target's layout shows that merge (structure/cross-jumped-tail.ts), and never reads
1261
+ // the layout where this is absent.
1262
+ //
1263
+ // gcc 2.x's jump2 pass does it: jump.c, in the `JUMP_CROSS_JUMP` run of `jump_optimize` that
1264
+ // toplev.c makes at `optimize > 0`.
1265
+ // - An unconditional jump is first cross-jumped into the code before its own target label
1266
+ // (`find_cross_jump (insn, JUMP_LABEL (insn), 1)`).
1267
+ // - Only then is it tried against the other jumps to that label, and that search runs only
1268
+ // `if (INSN_UID (JUMP_LABEL (insn)) < max_uid)`.
1269
+ // - `do_cross_jump` keeps the LATER copy and jumps to a label before it. `get_label_before`
1270
+ // creates that label, so a jump already redirected is never searched for siblings again.
1271
+ // So the copies spelling leaves three shapes in the object (structure/cross-jumped-tail.ts):
1272
+ // - an arm that does not use the tail stays laid out after the surviving copy, and that copy
1273
+ // ends in a jump over it;
1274
+ // - identical arm bodies stay separate blocks, where the join's one original label would have
1275
+ // merged them;
1276
+ // - `find_cross_jump` compares backward and stops at the first pair that differs, so a call
1277
+ // the tail opens with can be cut from the argument setup each arm keeps.
1278
+ // MEASURED on two probe pairs, `corpus/probe-rettail-{pa,pb}-{copies,join}.c`: a three-arm `if`
1279
+ // and a seven-case `switch`. Each is compiled at each toolchain's canonical flags by
1280
+ // `scripts/regen-rettail-probes.ts`, and cross-jumped-tail.test.ts reads the objects:
1281
+ // - agbcc: the `pa` copies keep the tail mid-construct, ending in `b`, with the else arm laid
1282
+ // out after it. The `pb` copies hold the tail's store ONCE, in six case bodies; the join
1283
+ // has three bodies.
1284
+ // - gcc2.7.2 (-O1) and gcc2.7.2kmc (-O2): `pb` is the same, one store, six bodies against
1285
+ // three. The `pa` copies and join compile byte-identical, because the fall-through arm's
1286
+ // call merged too.
1287
+ // - ido7.1 and mwcc_242_81: NO. The `pb` copies keep all seven stores, each before its own
1288
+ // return, and the `pa` copies keep three calls.
1289
+ // So `true` on ARMV4T_AGBCC and on MIPS_GCC. Each toolchain MIPS_GCC serves owes its own pair
1290
+ // (the KEYED BY DESCRIPTION note at `spillSlotOrder`), and both are committed. ABSENT means no
1291
+ // such merge: the chooser's size bound alone decides. mwcc_233_163n and mwcc_247_107 are
1292
+ // unprobed, and PPC_MWCC claims nothing for them. Read only where the join is the default
1293
+ // (`joinPastReturnsByDefault`), which both descriptions declaring this field leave out, so the
1294
+ // marks decide no shipped default; the probe pairs still pin each one.
1295
+ crossJumpsReturnTails?: boolean;
1296
+ // DOES THE COMPILER FOLD THE CONSTANT A BRANCH PROVED INTO A RETURN ON THE BRANCH'S TAKEN EDGE?
1297
+ // After `if (x == 0)`, a `return x` the jump reaches is `return 0` where it does. gcc 2.x's cse
1298
+ // records the branch's equivalence on the NOT-taken edge of every jump (`record_jump_equiv
1299
+ // (insn, 0)`, cse.c:7597), and on the taken edge only along a path it follows (`record_jump_equiv
1300
+ // (insn, 1)`, cse.c:8545). It follows one only under -fcse-follow-jumps, which toplev.c:3606-3607
1301
+ // sets at `optimize >= 2` (-fcse-skip-blocks skips a block without recording the edge:
1302
+ // `cseFollowsJumpsUnder`). (Line numbers are agbcc's gcc 2.9 tree; for
1303
+ // 2.7.2 the probe below is the evidence.) So where this is `false`, a return of the tested value
1304
+ // left UNFOLDED in the object was the jump-target arm of its branch in the source: an `else`.
1305
+ // The join past early returns spells it `if (x == 0) return x;`, the then arm, which folds.
1306
+ // Read by the return-tail chooser (pipeline.ts `structureReturnTailReading`), which publishes the
1307
+ // copies at such a return (structure/unfolded-branch-constant.ts), and only where the join is
1308
+ // the default (`joinPastReturnsByDefault`), which MIPS_GCC, the one description declaring this
1309
+ // field, leaves out: so it decides no shipped default, and the probes below still pin it.
1310
+ // `false` only by the flags (`targetFor`), never declared.
1311
+ //
1312
+ // MEASURED on the probe pair `corpus/probe-rettail-pg-{copies,join}.c`, the `return gB` after a
1313
+ // `gB` tested against 0, compiled by scripts/regen-rettail-probes.ts. The `beqz`'s delay slot:
1314
+ // - gcc2.7.2 at -O1 (canonical): copies `nop`, join `move v0,zero`. With the arms of the
1315
+ // copies flipped (the return the then arm), and with `-O1 -fcse-follow-jumps`, `move v0,zero`.
1316
+ // - gcc2.7.2 at -O2, and gcc2.7.2kmc at -O2 (canonical): `move v0,zero` under every spelling.
1317
+ // So `true` on MIPS_GCC, and `false` from the flags where cse follows no jump (level 1 without
1318
+ // -fcse-follow-jumps, or -fno-cse-follow-jumps: `cseFollowsJumpsUnder`). Absent elsewhere: agbcc
1319
+ // at -O2 re-loads `gB` for the copies' `else` and does not fold it, which no value of this field
1320
+ // says; ido7.1 and mwcc are unprobed.
1321
+ foldsBranchConstantOnTakenEdge?: boolean;
1322
+ // IS THE JOIN PAST EARLY RETURNS THE DEFAULT READING HERE? Not a compiler behaviour but a MEASURED
1323
+ // DEFAULT, kept beside the behaviours because it is per compiler and read by the chooser they
1324
+ // feed (pipeline.ts `structureReturnTailReading`), which the size bound and the marks above
1325
+ // steer only where this is `true`. ABSENT ⇒ the return-tail copies are published, as before the
1326
+ // join was a default, and the join is still structured and kept in the ranked fan as
1327
+ // `/ret-join`. A compiler the held-out measurement has not reached publishes the copies first,
1328
+ // and the join only where the copies refuse, so it lifts every function it lifted before and
1329
+ // may lift one the old default declined (pipeline.ts `copiesByDefault`).
1330
+ // docs/level-tower.md (A MEASURED DEFAULT IS THE ONE EXCEPTION) says why a field whose readings
1331
+ // split site by site may still be declared here, and what re-measures it.
1332
+ //
1333
+ // MEASURED over held-out real functions, every function of a project outside the 620 the marks
1334
+ // were chosen on, both readings really compiled against the project's own objects (2026-11-06),
1335
+ // and over 160 generated functions written with the copies (2026-11-09). `true` only where the
1336
+ // size bound with every mark loses no match the copies have on every measured set:
1337
+ // - mwcc_233_163n (Pikmin at its own flags, 2733 functions lifted, 66 re-spelled): 0 lost,
1338
+ // 3 gained (two `actByEvent` methods 4/22 -> 0/20, `stickSlot__6PelletFi` 3/28 -> 0/26);
1339
+ // the generated functions: every source identical to the copies';
1340
+ // - mwcc_242_81 (ac-decomp and Mario Party 4, every unit compiled at this build although 9 of
1341
+ // Mario Party 4's rows are mwcc_247_107; 141 re-spelled): 0 lost, 0 gained; the generated
1342
+ // functions: every source identical to the copies'.
1343
+ // So `true` on PPC_MWCC (mwcc_242_81) and PPC_MWCC_233; PPC_MWCC_247 (mwcc_247_107) leaves it
1344
+ // out, no held-out unit measured at that build. Left out where it loses, the copies' default
1345
+ // given back with the join's gains:
1346
+ // - gcc2.7.2kmc: held out (Snowboard Kids 2, 78 re-spelled) 0 lost, 2 gained
1347
+ // (`func_8003513C_35D3C`, `func_80045564_46164`); generated, 3 lost (the copies match, the
1348
+ // join is 2 to 8 lines off) and 5 gained. Given up: those 2 held out and the 5 generated.
1349
+ // - gcc2.7.2: held out (Mario Party 3, all 10019 functions, 150 re-spelled) 0 lost once the
1350
+ // unfolded return is read (`foldsBranchConstantOnTakenEdge`), none gained; generated, 1 lost
1351
+ // (4 lines off) and 2 gained. No mark read from the layout or the unfolded return separates
1352
+ // the 4 generated losses on the two lanes. Given up: the 2 generated, and the held-out and
1353
+ // 620 functions the join brought closer.
1354
+ // - agbcc (pokeemerald, 15062 functions, 963 re-spelled): 9 lost, 18 gained. None of the 9 is
1355
+ // marked: switch and comparison-tree sites, a call cross-jumped across arms that pass it
1356
+ // different arguments, and a third source spelling the copies happen to share. Given up:
1357
+ // 18 held out and 2 of the 620.
1358
+ // - ido7.1 (Animal Forest, 11363 functions, 252 re-spelled): 4 lost, one shape, 18 gained. ido
1359
+ // never cross-jumps, so no cross-jump mark applies. Counting copies of the arm's store does
1360
+ // not separate the readings either: compiled, the join of `func_80A42DB4_jp` ALSO keeps two
1361
+ // `sw zero,0(a3)`, because ido fills a `beql`'s likely slot with a copy of its target's first
1362
+ // word. What differs is the branch the slot copy rides: the copies' arm schedules its own
1363
+ // store into the slot of a plain `b`, the join's copy sits in a `beql`'s. That is the
1364
+ // candidate signal, unmeasured: frontend/mips.ts `normaliseBranchLikely` rewrites both to one
1365
+ // shape (its `likely` and `freed` sets still tell them apart) before the layout is recorded,
1366
+ // so no layout entry carries it. Given up: 18 held out and 7 of the 620.
1367
+ joinPastReturnsByDefault?: boolean;
787
1368
  };
788
1369
  }
789
1370
 
1371
+ /** THE SPELLINGS ONE REGISTER HAS in an ISA's asm, as `[number, alias]` pairs — the one place they
1372
+ * are stated. A frontend keys registers by spelling, so a function that names one register two
1373
+ * ways is rewritten to one of them first (frontend/ssa.ts `unifyRegisterSpellings`, which both
1374
+ * frontends hand these tables); and every register list a target states names both spellings of a
1375
+ * pair or neither, which `target-register-aliases.test.ts` checks against these tables. A pair
1376
+ * missing here splits one register into two partition keys: saved as `sl` and written as `r10`, its
1377
+ * writes read as a global's the epilogue undoes. */
1378
+ export const ARM_REGISTER_ALIASES: readonly (readonly [string, string])[] = [
1379
+ ['r9', 'sb'],
1380
+ ['r10', 'sl'],
1381
+ ['r11', 'fp'],
1382
+ ['r12', 'ip'],
1383
+ ];
1384
+ /** O32's `$30`, which objdump prints `s8` and hand-written asm also writes `fp` (`ARM_REGISTER_ALIASES`). */
1385
+ export const O32_REGISTER_ALIASES: readonly (readonly [string, string])[] = [['s8', 'fp']];
1386
+
790
1387
  export const ARMV4T_AGBCC: TargetDescription = {
791
1388
  id: 'armv4t',
792
1389
  compiler: 'agbcc',
@@ -803,6 +1400,17 @@ export const ARMV4T_AGBCC: TargetDescription = {
803
1400
  // `mov ip, r1` in two switch arms, no save anywhere, and a `mov r0, ip` past a third arm that
804
1401
  // writes nothing — an uninitialised local by construction.
805
1402
  scratchRegs: ['r12', 'ip'],
1403
+ // MEASURED at the canonical flags, `register int g __asm__("rN")` for each of r4-r11: `f(){ return
1404
+ // g; }` is `add r0, r4, #0` / `mov r0, r8` / `mov r0, sl`, and `s(a){ g = a; }` the reverse, with no
1405
+ // save. agbcc prints r9 as `r9`, r10 as `sl` and r11 as `fp`; hand-written asm also writes `sb`.
1406
+ globalRegisterVariables: {
1407
+ regs: {
1408
+ ...Object.fromEntries(['r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10', 'r11'].map((r) => [r, r])),
1409
+ sb: 'r9',
1410
+ sl: 'r10',
1411
+ fp: 'r11',
1412
+ },
1413
+ },
806
1414
  // AAPCS: r0-r3 pass arguments and return, r12 (`ip`) is the intra-procedure-call scratch, and lr
807
1415
  // holds the return address the `bl` itself overwrites. agbcc's own machine description says the
808
1416
  // same (`thumb.h` CALL_USED_REGISTERS). Both spellings of r12 for the reason `nonArgRegs` carries
@@ -883,20 +1491,40 @@ export const ARMV4T_AGBCC: TargetDescription = {
883
1491
  nearBaseSpan: 255,
884
1492
  volatileReadsExtendInRegister: true,
885
1493
  foldsConstAddrOffset: true,
1494
+ farMemberStoreOrder: true,
1495
+ farMemberBasePerAccess: { past: 0 },
886
1496
  foldsPointerAdvance: true,
887
1497
  pointerIntConversionIsFree: true,
1498
+ keepsPointerSumAddend: true,
888
1499
  readsStayWhereWritten: true,
889
1500
  switchBoundCase: 'taken',
890
1501
  switchArmsFollowLayout: true,
891
1502
  switchRequiresFrontLoadedTests: true,
1503
+ // MEASURED on the probe pairs at the field (gcc 2.x jump.c's cross-jump). Read only where the join
1504
+ // is the default (`joinPastReturnsByDefault`), which agbcc leaves out: the join loses 9 of
1505
+ // pokeemerald's matches no mark separates.
1506
+ crossJumpsReturnTails: true,
892
1507
  hoistsSingleSetArm: true,
893
1508
  eightByteReturnScratch: 'r2',
894
1509
  arrayShapeFromStride: true,
895
1510
  reloadsLocalReread: true,
1511
+ // MEASURED: the probe at the field. A signed constant often lifts as `mov #1; neg` or
1512
+ // `mov #0x80; lsl #8`, a `neg`/`shl` the seed does not read. agbcc also materializes one
1513
+ // constant ONCE for several narrow stores through the same base, whatever their signedness:
1514
+ // `struct { s32 a; s16 b; u16 c; } *p; p->b = -300; p->c = -300;` is one `ldr r1,=0xfed4` for
1515
+ // both `strh`. A stamp here then names one store's type, not necessarily each's (the `s16`
1516
+ // store is stamped unsigned, which is also the convention).
1517
+ narrowStoreConstantTakesLvalueType: true,
896
1518
  aggregateBoundary: 4,
897
1519
  largestAlignment: 4,
898
1520
  aggregateReturn: 'apcs',
1521
+ structReturnHandsBackPointer: true,
1522
+ // MEASURED: the probes at the fields.
1523
+ forwardsStructReturnDestination: false,
1524
+ discardedReadMayFillArgRegister: true,
1525
+ byValueAggregateArgWords: true,
899
1526
  enumBytes: 4,
1527
+ plainCharSigned: false,
900
1528
  bitfieldPacking: 'contiguous',
901
1529
  roundTripsDoubleLiterals: true,
902
1530
  // agbcc 2.9 (gcc/varasm.c `assemble_variable`, gcc/thumb.h): an array takes its element's
@@ -914,16 +1542,23 @@ export const ARMV4T_AGBCC: TargetDescription = {
914
1542
  // operand rows and nothing else, from two locals declared the other way round) and its
915
1543
  // control `synthetic:spillorder_rev` (the same body in the order asmlift already emits, which
916
1544
  // must stay a MATCH), plus `synthetic:dma_fill_uninit`, a row this did not author.
1545
+ designatedInitializers: true, // agbcc compiles `{ 1, [8] = 2 }` (see the field)
917
1546
  spillSlotOrder: 'ascending',
1547
+ addressTakenArrayOrder: 'ascending', // MEASURED, the probe at the field
1548
+ addressTakenArrayAlignment: 4, // MEASURED, the probe at the field
918
1549
  // agbcc reserves the outgoing area with the rest of the frame (`add sp, sp, #-N` covers both)
919
1550
  // and stages arguments 5+ into it at [sp,#0] upward — thumb.h's ACCUMULATE_OUTGOING_ARGS.
920
1551
  stagesOutgoingArgsInFrame: true,
921
1552
  // …one area for every call, below the locals (thumb.h:573/600-622/628, calls.c:1675), which
922
1553
  // agbcc re-stages before each call and never reads back after one (agbcc-restage.s).
923
1554
  localsAboveOutgoingArea: true,
1555
+ // …nor loads at all, before a call or after: no REG_PARM_STACK_SPACE (see the field).
1556
+ outgoingAreaNeverLoadedFrom: 0,
924
1557
  // the two producer tables behind this are compiled, at agbcc 2.9-arm-000512 and the rows' own
925
1558
  // flags, and they are written out where the predicate reads it (`frontend/thumb.ts`)
926
1559
  oneWordFrameIsTheCapturedObject: true,
1560
+ // compiled: the committed probe `test/corpus/agbcc-escape.s` (`scripts/regen-outgoing-area-probes.ts`)
1561
+ escapedFrameAddressNamesALocal: true,
927
1562
  // agbcc's `*call_indirect` and `*call_value_indirect` emit `bl _call_via_%0` (gcc/thumb.md:997-1021),
928
1563
  // and libgcc defines `_call_via_<reg>` as `bx <reg>` for r0-r9, sl, fp, ip, sp and lr
929
1564
  // (libgcc/lib1thumb.asm:595-633). Neither `sp` (the frame) nor `lr` (which the `bl` itself
@@ -932,6 +1567,13 @@ export const ARMV4T_AGBCC: TargetDescription = {
932
1567
  prefix: '_call_via_',
933
1568
  regs: ['r0', 'r1', 'r2', 'r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'sl', 'fp', 'ip'],
934
1569
  },
1570
+ // agbcc spells a branch past the ±2 KB reach of Thumb `b` as `bl <label>` with this comment,
1571
+ // from three patterns: `jump` (`bl %l0\t@ far jump`, gcc/thumb.md:837), and the relays of
1572
+ // `*cond_branch` (`.LCB`, :930) and `*cond_branch_reversed` (`.LCBR`, :957), both
1573
+ // `bl %l0\t@far jump`. thumb.c `far_jump_used_p` (:715, read at :785 and :1065) makes it push lr
1574
+ // in any function that has one. Only the WHOLE comment is the mark: a longer comment that
1575
+ // mentions a far jump is not the compiler's.
1576
+ farJumpRelay: { comment: /^@\s*far jump\s*$/ },
935
1577
  },
936
1578
  };
937
1579
 
@@ -939,6 +1581,58 @@ export const ARMV4T_AGBCC: TargetDescription = {
939
1581
  * halves (`$f13`, `$f15`) carry the other word of a DOUBLE argument, which no frontend lifts. */
940
1582
  const O32_FPU: TargetDescription['fpu'] = { argRegs: ['$f12', '$f14'], returnReg: '$f0', slots: 'leading' };
941
1583
 
1584
+ /** O32's register partition, shared by every compiler that targets it (`nonArgRegs` and
1585
+ * `scratchRegs`). Every general register but the four argument registers and the ones that are
1586
+ * never a local's home (`zero`, `k0`/`k1`, `gp`, `sp`, `ra`). `s8` and `fp` are the two spellings
1587
+ * of $30, listed both as `ARMV4T_AGBCC` lists `r12` and `ip`: objdump prints `s8`, and a spelling
1588
+ * nobody listed keeps its existing treatment. */
1589
+ const O32_NON_ARG_REGS = [
1590
+ 'at',
1591
+ 'v0',
1592
+ 'v1',
1593
+ ...Array.from({ length: 10 }, (_, i) => `t${i}`),
1594
+ ...Array.from({ length: 8 }, (_, i) => `s${i}`),
1595
+ 's8',
1596
+ 'fp',
1597
+ ] as const;
1598
+ /** Of those, the ones O32 lets a callee use with no save — `at`, `v0`/`v1` and `t0`..`t9`, the
1599
+ * caller-saved registers that pass no argument. MEASURED on GCC_KMC_TOOLCHAIN and the stock gcc
1600
+ * 2.7.2 toolchain at their synthetic flags: a leaf homes an uninitialised local in v0
1601
+ * (`uninit_join`: `bgtzl a0,…; sll v0,a0,1; jr ra; ori v0,v0,1`) and in v1 (`uninit_sw`).
1602
+ *
1603
+ * Every one of them is also a `hiddenInputRegs` entry, because O32's hand-written asm hands values
1604
+ * over in them: af's famicom_emu routines are handed a value in t0 or v1 (`sw t0,6348(s8)` is
1605
+ * its first instruction), and a label inside a function starts with its registers live.
1606
+ * MEASURED over every function and label symbol of the vendored snowboardkids2 (KMC),
1607
+ * marioparty3 (gcc 2.7.2) and af (IDO) objects, 29,399 in all, the refusal below fires on 17 —
1608
+ * twelve labels, three hand-written routines (two of famicom_emu's and libkmc's `div64_64_1`) and
1609
+ * two data symbols disassembled as code — and on no compiled function. A
1610
+ * compiled local is assigned on some path, so its entry value meets that assignment at a join;
1611
+ * `uninitfirst` also reads one before the switch that assigns it, which a join allows. The join
1612
+ * must take a definition the entry value does not feed, because an input carried round a loop
1613
+ * (`addiu t0,t0,-1; bnez t0,top`) meets only its own next value at the loop header. famicom_emu's
1614
+ * hand-written Yaz0 reader `func_8082E4E0_jp` is NOT that shape:
1615
+ * its t0 is a bit buffer loaded whenever the count runs out (and the count starts at 0), then
1616
+ * shifted each trip, and it lifts with t0 an uninitialised local no path reads before the load.
1617
+ * The cost is a local NO path assigns
1618
+ * (`int r; return r + a;` is `jr ra; addu v0,v0,a0` on gcc, and `int x, y; if (c) x = y;` joins
1619
+ * v0 with v1), which refuses: none of the 29,399 is one.
1620
+ *
1621
+ * THE RULE'S BLIND SIDE is a count a hand-written routine is handed and RELOADS on an outer arm
1622
+ * (`… addiu t0,t0,-1; bnez t0,inner; …; bnezl a1,inner; li t0,8`): the outer arm's constant is a
1623
+ * definition of its own at the inner loop's header, so the entry value counts as met and the first
1624
+ * pass reads an undeclared local — the input is dropped. Nothing tells it from compiled
1625
+ * `int t; do { do { … } while (--t); t = 8; } while (--n);`, and none of the 29,399 is either.
1626
+ * The same side holds an input RELOADED FROM MEMORY on one arm and then transformed: hand
1627
+ * func_8082E4E0_jp its count in a2 as well (drop its `move a2,zero`), and the arm that runs when
1628
+ * the count is not 0 reads the caller's bit buffer in t0, but the loop header's join meets the
1629
+ * shift `sll t0,t0,1`, which reaches t0's entry value only through the join the load `lw t0,0(a0)`
1630
+ * also feeds. So the shift is a definition of its own, the routine lifts with `int code;` read on
1631
+ * that first pass, and the caller's t0 is dropped. Nothing tells it from compiled `int code;`
1632
+ * with the bit count a parameter, and none of the 29,399 is that shape: the real routine zeroes
1633
+ * a2 before the loop, so its load runs first. */
1634
+ const O32_SCRATCH_REGS = ['at', 'v0', 'v1', ...Array.from({ length: 10 }, (_, i) => `t${i}`)] as const;
1635
+
942
1636
  /** MIPS-II / IDO 7.1 target. IDO is the IRIX C compiler,
943
1637
  * statically recompiled to run natively (ido-static-recomp). Unlike agbcc
944
1638
  * it emits no textual asm, so asmlift's input is the DISASSEMBLED object (`mips-linux-gnu-
@@ -949,6 +1643,8 @@ export const MIPS_IDO: TargetDescription = {
949
1643
  compiler: 'ido',
950
1644
  argRegs: ['a0', 'a1', 'a2', 'a3'],
951
1645
  returnReg: 'v0',
1646
+ // MEASURED on ido7.1, gcc2.7.2kmc and gcc2.7.2: the probes at the field.
1647
+ evenWordAlignedArgs: true,
952
1648
  // O32: at, v0-v1, a0-a3, t0-t9 and ra are all caller-saved. `frontend/mips.ts` reads the rest, and
953
1649
  // `ra`, as the registers a frame store SAVES.
954
1650
  callerSaved: [
@@ -971,6 +1667,13 @@ export const MIPS_IDO: TargetDescription = {
971
1667
  't9',
972
1668
  'ra',
973
1669
  ],
1670
+ // O32's partition (`O32_NON_ARG_REGS`). The frontend narrows it to the scratch half and the
1671
+ // callee-saved registers its prologue saves (frontend/mips.ts `prologueSavedRegs`).
1672
+ nonArgRegs: O32_NON_ARG_REGS,
1673
+ scratchRegs: O32_SCRATCH_REGS,
1674
+ // Every scratch register, for the measurement at `O32_SCRATCH_REGS`.
1675
+ hiddenInputRegs: O32_SCRATCH_REGS,
1676
+ kernelRegs: ['k0', 'k1'],
974
1677
  runtimeHelpers: IDO_RUNTIME_HELPERS,
975
1678
  // MEASURED with this toolchain's own flags: `float f(float a, float b){ return a + b; }` is
976
1679
  // `jr ra; add.s $f0,$f12,$f14`, and the `'leading'` rule's two halves are the pair in `fpu`'s note.
@@ -979,6 +1682,7 @@ export const MIPS_IDO: TargetDescription = {
979
1682
  // `switchAllowsNeqCase: false` — IDO's switch dispatch uses `==`/`<`, never `!=` cases;
980
1683
  // leaving it permissive mis-recognises `!=`-rooted if-else chains as switches.
981
1684
  compilerBehaviors: {
1685
+ plainCharSigned: false,
982
1686
  contractsFloatProducts: false,
983
1687
  coalesceLoopInit: true,
984
1688
  preserveDivergentBranchSense: true,
@@ -986,7 +1690,19 @@ export const MIPS_IDO: TargetDescription = {
986
1690
  switchAllowsNeqCase: false,
987
1691
  // MEASURED — the pair at the field compiles to one load of `p[1]` for every local spelling.
988
1692
  reloadsLocalReread: false,
1693
+ // MEASURED: the probe at the field (`lw v0` after the byte store).
1694
+ reloadsAcrossSpellings: true,
1695
+ // MEASURED: the probe at the field.
1696
+ narrowStoreConstantTakesLvalueType: true,
989
1697
  aggregateBoundary: 1,
1698
+ // MEASURED on all three O32 toolchains (ido7.1, gcc2.7.2kmc, gcc2.7.2): `struct {char c; double d;}`
1699
+ // and its `long long` twin put the member at 8 and size 16 (`sizeof` and the member's offset
1700
+ // compiled into `.data`).
1701
+ largestAlignment: 8,
1702
+ // MEASURED on all three O32 toolchains: the probes at the field.
1703
+ aggregateReturn: 'memory',
1704
+ structReturnHandsBackPointer: true,
1705
+ byValueAggregateArgWords: true,
990
1706
  // MEASURED at `-mips2 -O2 -32 -non_shared -G 0`: the `sll` leads the function for BOTH
991
1707
  // spellings, so the prologue position cannot decide; a narrow DECLARED parameter is the one that
992
1708
  // is both homed dead AND widened in its own argument register. raise/paramwidth.ts's header has
@@ -1007,6 +1723,26 @@ export const MIPS_IDO: TargetDescription = {
1007
1723
  // O32's caller-owned home area `[0,16)` would be read as this function's first four
1008
1724
  // declaration ranks. Shipping a direction before the partition orders by argument index.
1009
1725
  spillSlotOrder: 'unknown',
1726
+ addressTakenArrayOrder: 'descending', // MEASURED, the probe at the field
1727
+ addressTakenArrayAlignment: 4, // MEASURED, the probe at the field
1728
+ // MEASURED, the probe at the field: the local ends at the frame top, above `ra` and the spills.
1729
+ o32LocalArea: 'above-saved-registers',
1730
+ // MEASURED off the same probe (`twice`, `owns`): O32's outgoing area is the frame bottom, an
1731
+ // unchanged argument is staged again before each call, and a callee assigns to its stack
1732
+ // parameter in place. Read by the frame-object audit alone here (`storedNeverRead`).
1733
+ localsAboveOutgoingArea: true,
1734
+ // MEASURED at -O1, -O2, -O2 -g3 and -O0 (`test/corpus/ido71*-outarea.txt`): no load from 16(sp)
1735
+ // up, but the a1..a3 home words a by-value aggregate is staged in are loaded back (see the field).
1736
+ outgoingAreaNeverLoadedFrom: 16,
1737
+ // MEASURED at -O1 and -O0: the note at the field — `lw zero` for a volatile read, a `t6` load
1738
+ // and a frame store for a plain dead local, `lw a2` for a `register` one.
1739
+ discardsUnusedReadsInto: 'zero',
1740
+ // MEASURED: the probes at the fields.
1741
+ forwardsStructReturnDestination: true,
1742
+ discardedReadMayFillArgRegister: false,
1743
+ // MEASURED, the probes at the field: past the `addiu` reach every member access gets its own
1744
+ // `lui`/`addu`, and a held pointer shares one.
1745
+ farMemberBasePerAccess: { past: 32767 },
1010
1746
  },
1011
1747
  };
1012
1748
 
@@ -1018,6 +1754,8 @@ export const MIPS_GCC: TargetDescription = {
1018
1754
  compiler: 'gcc',
1019
1755
  argRegs: ['a0', 'a1', 'a2', 'a3'],
1020
1756
  returnReg: 'v0',
1757
+ // MEASURED on ido7.1, gcc2.7.2kmc and gcc2.7.2: the probes at the field.
1758
+ evenWordAlignedArgs: true,
1021
1759
  // The same O32 convention MIPS_IDO carries, read the same way.
1022
1760
  callerSaved: [
1023
1761
  'at',
@@ -1039,6 +1777,22 @@ export const MIPS_GCC: TargetDescription = {
1039
1777
  't9',
1040
1778
  'ra',
1041
1779
  ],
1780
+ // O32's partition, as at MIPS_IDO.
1781
+ nonArgRegs: O32_NON_ARG_REGS,
1782
+ scratchRegs: O32_SCRATCH_REGS,
1783
+ hiddenInputRegs: O32_SCRATCH_REGS,
1784
+ // MEASURED on gcc2.7.2kmc and gcc2.7.2 at their canonical flags: `register int g __asm__("$16");
1785
+ // int f(void){ return g; }` is `jr ra; move v0,s0`, and `$23`/`$30` give s7/s8. KMC's `-fno-asm`
1786
+ // rejects the bare `asm(` spelling, which is why every target spells `__asm__`. IDO 7.1 has no
1787
+ // such declaration (its cfe rejects both spellings), so MIPS_IDO states none.
1788
+ globalRegisterVariables: {
1789
+ regs: {
1790
+ ...Object.fromEntries(Array.from({ length: 8 }, (_, i) => [`s${i}`, `$${16 + i}`])),
1791
+ s8: '$30',
1792
+ fp: '$30',
1793
+ },
1794
+ },
1795
+ kernelRegs: ['k0', 'k1'],
1042
1796
  runtimeHelpers: MIPS_GCC_RUNTIME_HELPERS,
1043
1797
  // KMC GCC keeps a loop seeded from an argument register IN that register (coalesceLoopInit
1044
1798
  // true, like IDO): test/corpus/gcc-gcd.asm runs its whole loop on a0/a1 with no init copies,
@@ -1062,6 +1816,7 @@ export const MIPS_GCC: TargetDescription = {
1062
1816
  fpu: O32_FPU,
1063
1817
  capabilities: { endianness: 'big', hwDivide: true, hwFloat: true, flags: false },
1064
1818
  compilerBehaviors: {
1819
+ plainCharSigned: true,
1065
1820
  contractsFloatProducts: false,
1066
1821
  coalesceLoopInit: true,
1067
1822
  preserveDivergentBranchSense: true,
@@ -1101,10 +1856,29 @@ export const MIPS_GCC: TargetDescription = {
1101
1856
  // sub-trees a decline leaves, so a NESTED dispatch comes back as an `if` nest around a `switch`
1102
1857
  // over some of its arms.
1103
1858
  switchRequiresFrontLoadedTests: true,
1859
+ // MEASURED on BOTH toolchains this description serves, the probe pairs at the field.
1860
+ crossJumpsReturnTails: true,
1861
+ // MEASURED at -O2 on both toolchains this description serves, the probe pair at the field;
1862
+ // gcc2.7.2's canonical -O1 clears it (`targetFor`).
1863
+ foldsBranchConstantOnTakenEdge: true,
1864
+ // `joinPastReturnsByDefault` LEFT OUT, so the copies are published and the join is ranked as
1865
+ // `/ret-join`. On held-out real functions it lost none of the copies' matches on either toolchain
1866
+ // this description serves, but on 160 generated functions written with the copies it lost 3 on
1867
+ // gcc2.7.2kmc and 1 on gcc2.7.2, which no mark separates (the field's note).
1104
1868
  // MEASURED on BOTH toolchains this description serves (the note above): one load of `p[1]` for
1105
1869
  // every local spelling of the pair at the field, gcc2.7.2kmc at -O2 and gcc2.7.2 at -O1 alike.
1106
1870
  reloadsLocalReread: false,
1871
+ // MEASURED on BOTH toolchains this description serves, the probe at the field.
1872
+ narrowStoreConstantTakesLvalueType: true,
1873
+ // MEASURED on BOTH toolchains this description serves, the pairs at the field.
1874
+ keepsPointerSumAddend: true,
1107
1875
  aggregateBoundary: 1,
1876
+ // MEASURED on BOTH toolchains this description serves, and on ido7.1: the probe at MIPS_IDO's.
1877
+ largestAlignment: 8,
1878
+ // MEASURED on BOTH toolchains this description serves: the probes at the field.
1879
+ aggregateReturn: 'memory',
1880
+ structReturnHandsBackPointer: true,
1881
+ byValueAggregateArgWords: true,
1108
1882
  // MEASURED on BOTH toolchains this description serves: `int f(s8 x){return x;}` and
1109
1883
  // `int f(s32 x){return (s8)x;}` compile to BYTE-IDENTICAL objects, gcc2.7.2kmc at -O2 and
1110
1884
  // gcc2.7.2 at -O1 alike — so the object carries no witness at all and the pass refuses.
@@ -1118,7 +1892,21 @@ export const MIPS_GCC: TargetDescription = {
1118
1892
  // The two agreeing is not a formality. The value is per DESCRIPTION and TWO toolchains map
1119
1893
  // here, so a toolchain whose direction differed from its description's would need a
1120
1894
  // per-toolchain override this bag cannot express — see the note at `compilerBehaviors`.
1895
+ designatedInitializers: true, // both gcc 2.7.2 builds compile `{ 1, [8] = 2 }` (see the field)
1121
1896
  spillSlotOrder: 'unknown',
1897
+ addressTakenArrayOrder: 'ascending', // MEASURED on both toolchains, the probe at the field
1898
+ addressTakenArrayAlignment: 8, // MEASURED on both toolchains, the probe at the field
1899
+ // MEASURED on BOTH toolchains this description serves, the probe at the field: identical
1900
+ // layouts, the local right above the 8-aligned outgoing area and under the saves.
1901
+ o32LocalArea: 'above-outgoing-area',
1902
+ // MEASURED on BOTH toolchains this description serves, the probe at `o32LocalArea` (`twice`).
1903
+ localsAboveOutgoingArea: true,
1904
+ // MEASURED on BOTH toolchains at their rows' levels and at -O0 (`test/corpus/gcc272*-outarea.txt`):
1905
+ // no load from 16(sp) up the area; a char-array struct's home words are loaded (see the field).
1906
+ outgoingAreaNeverLoadedFrom: 16,
1907
+ // MEASURED on BOTH toolchains this description serves: the probes at the fields.
1908
+ forwardsStructReturnDestination: false,
1909
+ discardedReadMayFillArgRegister: true,
1122
1910
  },
1123
1911
  };
1124
1912
 
@@ -1134,11 +1922,37 @@ export const PPC_MWCC: TargetDescription = {
1134
1922
  // PPC EABI: r3–r10 pass integer/pointer arguments; r3 also returns.
1135
1923
  argRegs: ['r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10'],
1136
1924
  returnReg: 'r3',
1925
+ // MEASURED on 2.4.2 at the synthetic tier's flags and on 2.3.3 and 2.4.7 at -O4,p:
1926
+ // `takeLL(1, gL)` sets r3 and r5:r6 with r4 unset.
1927
+ evenWordAlignedArgs: true,
1137
1928
  // PPC EABI: r0 and r3-r12 are volatile, and lr carries the return address `bl` overwrites. r11
1138
1929
  // and r12 are the linker's stub scratch, r13 is the small-data base and r14 upward are
1139
1930
  // callee-saved.
1140
1931
  callerSaved: ['r0', 'r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10', 'r11', 'r12', 'lr'],
1932
+ // Every GPR but r1 (the stack pointer, never an SSA register here) and the eight argument
1933
+ // registers. The frontend narrows it to `scratchRegs` and the ones the prologue saved
1934
+ // (`prologueSavedRegs`). r2 and r13, the small-data bases, never reach that rule: a read of either
1935
+ // as a value refuses first (`SMALL_DATA_BASES` in frontend/ppc.ts).
1936
+ nonArgRegs: ['r0', 'r2', ...Array.from({ length: 21 }, (_, i) => `r${i + 11}`)],
1937
+ // The volatile registers no argument travels in, which a callee may use with no save. MEASURED on
1938
+ // 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
1939
+ // uninitialised local in each of them, unsaved — `int x; if (s) x = a*3; return x + a;` is
1940
+ // `mulli r0,r4,3; add r3,r0,r4`, and with all eight argument registers live two such locals land
1941
+ // in r11 and r12. 2.4.7 at MP4's flags puts them in saved registers instead.
1942
+ scratchRegs: ['r0', 'r11', 'r12'],
1943
+ // r11 is the save routines' frame address and r12 `__ptmf_scall`'s member-function pointer: a
1944
+ // read of either that no path's definition joins refuses instead of minting `uninit_r11`.
1945
+ hiddenInputRegs: ['r11', 'r12'],
1946
+ // MEASURED on 2.4.2 and 2.4.7 at -O4,p: `register int g __asm__("r14"); int f(void){ return g; }`
1947
+ // is `mr r3,r14`. They accept r14..r28 only ("supports 15 registers beginning with r14") and no
1948
+ // gap among them ("nothing at 'r19' not permitted"), in any declaration order. 2.3.3 refuses the
1949
+ // storage class, so `mwcc_233_163n` drops this field.
1950
+ globalRegisterVariables: {
1951
+ regs: Object.fromEntries(Array.from({ length: 15 }, (_, i) => [`r${14 + i}`, `r${14 + i}`])),
1952
+ contiguous: Array.from({ length: 15 }, (_, i) => `r${14 + i}`),
1953
+ },
1141
1954
  runtimeHelpers: PPC_MWCC_RUNTIME_HELPERS,
1955
+ registerSaveRoutines: PPC_MWCC_SAVE_ROUTINES,
1142
1956
  // PPC EABI with `-fp hard`, measured at the synthetic tier's flags: `fadds f1,f1,f2; blr`, a third
1143
1957
  // float argument in f3, and the GPR/FPR order of mixed parameters absent from the object.
1144
1958
  fpu: { argRegs: ['f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8'], returnReg: 'f1', slots: 'separate' },
@@ -1147,7 +1961,11 @@ export const PPC_MWCC: TargetDescription = {
1147
1961
  // defaults; coalesceLoopInit false until a CW loop fixture says otherwise — the second of the
1148
1962
  // two compiler-wide guesses standing in for the per-function observation named at MIPS_GCC.
1149
1963
  compilerBehaviors: {
1964
+ plainCharSigned: true,
1150
1965
  contractsFloatProducts: true,
1966
+ // MEASURED on all three builds in C: an integer, a pointer and a `u16 *` passed to an `s32 *`,
1967
+ // `u32` and `s32 *` parameter each fail to compile, and each compiles once cast (see the field).
1968
+ strictPointerConversions: true,
1151
1969
  coalesceLoopInit: false,
1152
1970
  preserveDivergentBranchSense: true,
1153
1971
  orderArgCopiesByWriteOrder: true,
@@ -1156,11 +1974,16 @@ export const PPC_MWCC: TargetDescription = {
1156
1974
  // `u8 v = p[3]; if ((v & 0x7f) == 0x7f) { fnA(); p[4] = v; return; }` under an `if (a)`
1157
1975
  // matches only once `read-behind-effect` stops refusing it (3/24 → MATCH 0/22).
1158
1976
  reloadsLocalReread: false,
1977
+ // MEASURED on all three builds at -O4,p, and on mwcc_247_107 at -O0,p: the probe at the field.
1978
+ narrowStoreConstantTakesLvalueType: true,
1159
1979
  aggregateBoundary: 1,
1160
1980
  // MEASURED on all three builds: `struct {u8 a; double d;}` and its `long long` twin put the
1161
1981
  // member at 8 and size 16
1162
1982
  largestAlignment: 8,
1163
1983
  aggregateReturn: 'svr4',
1984
+ // MEASURED on all three builds: r3 holds a reload of a frame word at the `blr` of a struct
1985
+ // returner that calls something
1986
+ structReturnHandsBackPointer: false,
1164
1987
  // MEASURED on mwcc_242_81 through the `.comment` alignment record: an array or a struct is at
1165
1988
  // 4 at least (`u8[1]`, a struct of two bytes), a scalar at its width; a string-literal
1166
1989
  // initializer gets no more than the array does (`char s[] = "hello!"` and a `u8` list both 4);
@@ -1187,6 +2010,15 @@ export const PPC_MWCC: TargetDescription = {
1187
2010
  // frontend lifts to the same `sext` op agbcc's shift pair folds to — the position shape, on
1188
2011
  // another ISA. `synthetic:{sextb,tos8}:mwcc_242_81` are its rows, MATCH through that pass.
1189
2012
  narrowParamWitness: 'prologue-extension',
2013
+ // MEASURED on all three builds: the note at the field.
2014
+ linkageLocalArea: 'above-outgoing-area',
2015
+ // MEASURED off the same probes, on all three builds and at -O0,p (`twice9`, and every outgoing
2016
+ // word below the area unread): Mario Party 4's -O0,p homes a parameter in the area and reloads
2017
+ // it after a call, which proves the word a local's (`ee`, `synthetic:homecall:mwcc_242_81`).
2018
+ localsAboveOutgoingArea: true,
2019
+ // MEASURED on all three builds at the rows' flags and -O0,p, and on mwcc_242_81 at -O4,s
2020
+ // (`test/corpus/mwcc*-outarea.txt`): no load of the parameter area, which starts at 8(r1).
2021
+ outgoingAreaNeverLoadedFrom: 8,
1190
2022
  // NOT MEASURED, and `'unknown'` is therefore the only honest value here rather than a withheld
1191
2023
  // one, as it is at MIPS_IDO and MIPS_GCC. No mwcc row lifts with two or more spilled user
1192
2024
  // locals, and the compiler does not spill the committed declaration-rank probe either: at
@@ -1195,9 +2027,38 @@ export const PPC_MWCC: TargetDescription = {
1195
2027
  // first: `frontend/ppc.ts` claims no `LiveInModel.declaredLocals`, so the shared stamp records
1196
2028
  // no slot home on this target at all and a direction here would order nothing until it does.
1197
2029
  spillSlotOrder: 'unknown',
2030
+ // MEASURED on all three builds at -O0,p: the note at the field — `lwz r0` for a volatile read,
2031
+ // an `r31` home for a plain or `register` dead local.
2032
+ discardsUnusedReadsInto: 'r0',
2033
+ // MEASURED on all three builds: the probes at the fields.
2034
+ forwardsStructReturnDestination: false,
2035
+ discardedReadMayFillArgRegister: true,
2036
+ // MEASURED over held-out real functions at mwcc_242_81 and mwcc_233_163n, at the field;
2037
+ // PPC_MWCC_247 leaves it out.
2038
+ joinPastReturnsByDefault: true,
1198
2039
  },
1199
2040
  };
1200
2041
 
2042
+ /** A description's behaviours less `joinPastReturnsByDefault`, for a build that shares the
2043
+ * description but was not measured for that default. */
2044
+ function withoutJoinDefault(d: TargetDescription): TargetDescription['compilerBehaviors'] {
2045
+ const { joinPastReturnsByDefault: _measuredElsewhere, ...behaviors } = d.compilerBehaviors;
2046
+ return behaviors;
2047
+ }
2048
+
2049
+ /** CodeWarrior 2.3.3b163n (Pikmin): `PPC_MWCC` but for the one declaration a probe refuted there —
2050
+ * it rejects a global register variable ("illegal storage class"), so it has no spelling for one.
2051
+ * The return-tail default is PPC_MWCC's, measured at this build over Pikmin
2052
+ * (`joinPastReturnsByDefault`). */
2053
+ export const PPC_MWCC_233: TargetDescription = {
2054
+ ...PPC_MWCC,
2055
+ globalRegisterVariables: undefined,
2056
+ };
2057
+
2058
+ /** CodeWarrior 2.4.7b107 (Mario Party 4's other build): `PPC_MWCC` less the return-tail default no
2059
+ * held-out function measured at this build (`joinPastReturnsByDefault`). */
2060
+ export const PPC_MWCC_247: TargetDescription = { ...PPC_MWCC, compilerBehaviors: withoutJoinDefault(PPC_MWCC) };
2061
+
1201
2062
  /** A toolchain: one compiler binary, named as decomp.me names it. Several toolchains may share a
1202
2063
  * description (`MIPS_GCC` serves two), and each keeps its own evidence. */
1203
2064
  export interface ToolchainTarget {
@@ -1271,7 +2132,8 @@ export const TOOLCHAIN_TARGETS = {
1271
2132
  description: PPC_MWCC,
1272
2133
  },
1273
2134
  // The other two CodeWarrior builds the GameCube projects compile with: 2.3.3b163n (Pikmin's whole
1274
- // game tree) and 2.4.7b107 (Mario Party 4's DOL). Both SHARE `PPC_MWCC`, and on their own evidence
2135
+ // game tree) and 2.4.7b107 (Mario Party 4's DOL). Both SHARE `PPC_MWCC` (2.3.3 less the global
2136
+ // register variables it refuses, `PPC_MWCC_233`), and on their own evidence
1275
2137
  // rather than because they are the same compiler family: `test/matching/ppc-compiler-behaviors.test.ts`
1276
2138
  // re-runs both of its probes on each binary at the flags that build's rows compile at, and each
1277
2139
  // reads as the description says. What moves those readings is the optimisation level — at `-O0,p`
@@ -1283,11 +2145,11 @@ export const TOOLCHAIN_TARGETS = {
1283
2145
  // are two different sets, neither of which is "the" one. See `canonicalFlags` above.
1284
2146
  mwcc_233_163n: {
1285
2147
  family: 'mwcc',
1286
- description: PPC_MWCC,
2148
+ description: PPC_MWCC_233,
1287
2149
  },
1288
2150
  mwcc_247_107: {
1289
2151
  family: 'mwcc',
1290
- description: PPC_MWCC,
2152
+ description: PPC_MWCC_247,
1291
2153
  },
1292
2154
  } as const satisfies Readonly<Record<string, ToolchainTarget>>;
1293
2155
 
@@ -1328,6 +2190,41 @@ export interface ResolvedTarget {
1328
2190
  profile: CodegenProfile;
1329
2191
  }
1330
2192
 
2193
+ /** Does a compiler of `family`, at optimisation level `level` (the profile's `O` slot), keep a plain
2194
+ * read whose value nothing uses (`compilerBehaviors.keepsUnusedReads`)? COMPILED, `int x; x = p[5];
2195
+ * 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
2196
+ * `lwz r31, 20(r3)` in a register nothing reads; IDO 7.1 at `-O0` and at `-O1` (its default) keeps
2197
+ * `lw t6, 20(a0)`; agbcc and KMC gcc 2.7.2 at `-O0` keep the read and store it to the frame. None
2198
+ * of them does at agbcc/KMC `-O1`, `-O2` or `-O3`, IDO `-O2` or `-O3`, or mwcc 2.4.2 `-O1` through
2199
+ * `-O4,p` and `-O4,s`. A plain expression statement's read (`*p;`) is gone at every level on every
2200
+ * one of them; a volatile one stays, at a keeping level in the register `discardsUnusedReadsInto`
2201
+ * names where the compiler has one.
2202
+ *
2203
+ * A LEVEL NO FLAG NAMES (`undefined`) KEEPS. Every family's unflagged compile was measured keeping:
2204
+ * agbcc's and IDO's are their `-O0` and `-O1` (codegen-flags.ts `EFFECTIVE` names them, so they
2205
+ * never reach here unnamed), KMC gcc 2.7.2 with no `-O` emits `-O0`'s object byte for byte, and
2206
+ * mwcc 2.4.2 with no level, with `-opt off` or with `-opt level=0` keeps `x = p[5]; x = 0;`'s
2207
+ * `lwz r31, 20(r3)` as at `-O0`. And the cost runs one way: answering "keeps" where the compiler
2208
+ * deletes withholds a `volatile` outside the discard register (`discardsUnusedReadsInto`): the
2209
+ * read is dropped as plain on mwcc and IDO, and the lift refuses on a compiler with none. Answering
2210
+ * "deletes" where it keeps publishes one the source never wrote, a value this stamp alone decides,
2211
+ * with no candidate to referee it (docs/level-tower.md, A PROFILE OF A COMPILER INHERITS). */
2212
+ export function keepsUnusedReadsAt(family: FlagFamily, level: string | undefined): boolean {
2213
+ if (level === undefined) {
2214
+ return true;
2215
+ }
2216
+ const n = /^\d/.exec(level)?.[0];
2217
+ return n === '0' || (family === 'ido' && n === '1');
2218
+ }
2219
+
2220
+ /** The level `keepsUnusedReadsAt` judges a profile at: its `O` slot, or none where a word the table
2221
+ * does not read may set it too — mwcc's `-opt off` and `-opt level=0` compile at `-O0`, and the
2222
+ * table reads neither, so a profile that spells one has a level nothing here knows. */
2223
+ function unusedReadLevel(profile: CodegenProfile): string | undefined {
2224
+ const optWord = profile.family === 'mwcc' && profile.unclassified.some((w) => w.startsWith('-opt'));
2225
+ return optWord ? undefined : profile.slots.O;
2226
+ }
2227
+
1331
2228
  /** The target a function compiled by `toolchain` at `cflags` decompiles against, and the profile
1332
2229
  * those flags describe. Throws on a level word the toolchain's family cannot read. */
1333
2230
  export function targetFor(toolchain: ToolchainId, cflags: readonly string[]): ResolvedTarget {
@@ -1335,15 +2232,76 @@ export function targetFor(toolchain: ToolchainId, cflags: readonly string[]): Re
1335
2232
  const profile = parseFlags(t.family, cflags);
1336
2233
  const cpp = dialectOf(profile.slots.lang) === 'c++';
1337
2234
  let target: TargetDescription = cpp ? { ...t.description, dialect: 'c++' } : t.description;
2235
+ if (keepsUnusedReadsAt(t.family, unusedReadLevel(profile))) {
2236
+ target = { ...target, compilerBehaviors: { ...target.compilerBehaviors, keepsUnusedReads: true } };
2237
+ }
1338
2238
  if (profile.slots['-fshort-enums'] === 'on') {
1339
2239
  // every enum is the smallest integer its values fit (c-decl.c:6064-6065, :6123; compiled,
1340
2240
  // `sizeof(struct {enum {K0, K1} k[2];})` is 4 under the flag and 8 without), so none is enumBytes
1341
2241
  const { enumBytes: _flagged, ...behaviors } = target.compilerBehaviors;
1342
2242
  target = { ...target, compilerBehaviors: behaviors };
1343
2243
  }
2244
+ if (target.compilerBehaviors.foldsBranchConstantOnTakenEdge === true && !cseFollowsJumpsUnder(profile)) {
2245
+ target = { ...target, compilerBehaviors: { ...target.compilerBehaviors, foldsBranchConstantOnTakenEdge: false } };
2246
+ }
2247
+ const charSigned = plainCharSignedUnder(t.family, profile.slots);
2248
+ if (charSigned !== 'default' && charSigned !== target.compilerBehaviors.plainCharSigned) {
2249
+ const { plainCharSigned: _default, ...behaviors } = target.compilerBehaviors;
2250
+ target = {
2251
+ ...target,
2252
+ compilerBehaviors: charSigned === undefined ? behaviors : { ...behaviors, plainCharSigned: charSigned },
2253
+ };
2254
+ }
1344
2255
  return { toolchain, cflags, target, profile };
1345
2256
  }
1346
2257
 
2258
+ /** Does gcc's cse follow jumps at these flags (`compilerBehaviors.foldsBranchConstantOnTakenEdge`)?
2259
+ * Only a jump cse FOLLOWS gets its taken edge's equivalence: `cse_end_of_basic_block` marks a path
2260
+ * branch TAKEN only under `follow_jumps` (cse.c:8192-8211), and the branch around a block that
2261
+ * `skip_blocks` adds is AROUND, whose block is invalidated, not recorded (cse.c:8223, :8545-8548).
2262
+ * toplev.c:3606-3607 sets `flag_cse_follow_jumps` at `optimize >= 2`, and the switch sets or clears
2263
+ * it by name either way, the level's value standing where it is not given. -O0 is answered `true`:
2264
+ * cse does not run there, which is a different compiler again, and a level no table reads keeps
2265
+ * the declaration. (cse.c line numbers are agbcc's gcc 2.9 tree.)
2266
+ *
2267
+ * MEASURED on `corpus/probe-rettail-pg` at gcc2.7.2, the copies' `beqz` delay slot: `nop` (unfolded)
2268
+ * at -O1, at `-O1 -fcse-skip-blocks`, at `-O2 -fno-cse-follow-jumps` and at `-O2
2269
+ * -fno-cse-follow-jumps -fno-cse-skip-blocks`; `move v0,zero` at -O2, at `-O2 -fno-cse-skip-blocks`
2270
+ * and at `-O1 -fcse-follow-jumps`. The join is `move v0,zero` under all seven. */
2271
+ function cseFollowsJumpsUnder(profile: CodegenProfile): boolean {
2272
+ if (profile.family !== 'gcc' && profile.family !== 'agbcc') {
2273
+ return true;
2274
+ }
2275
+ if (profile.slots.O === '0') {
2276
+ return true;
2277
+ }
2278
+ const flag = profile.slots['-fcse-follow-jumps'];
2279
+ return flag === 'on' ? true : flag === 'off' ? false : profile.slots.O !== '1';
2280
+ }
2281
+
2282
+ /** What the flags say a plain `char` is (`compilerBehaviors.plainCharSigned`): `default` where they
2283
+ * leave it to the compiler. gcc keeps one flag, `flag_signed_char`, that `-fsigned-char` and
2284
+ * `-fno-unsigned-char` set and `-funsigned-char` and `-fno-signed-char` clear, the last one given
2285
+ * winning; the profile keeps the two switches apart, so where both are given and they disagree the
2286
+ * order is lost and the answer is undefined. IDO's `-signed` makes it signed, and mwcc's `-char`
2287
+ * takes `signed` or `unsigned`. */
2288
+ function plainCharSignedUnder(
2289
+ family: FlagFamily,
2290
+ slots: Readonly<Record<string, string>>,
2291
+ ): boolean | undefined | 'default' {
2292
+ if (family === 'ido') {
2293
+ return slots.char === 'signed' ? true : 'default';
2294
+ }
2295
+ if (family === 'mwcc') {
2296
+ return slots.char === 'signed' ? true : slots.char === 'unsigned' ? false : 'default';
2297
+ }
2298
+ const said = [
2299
+ slots['-fsigned-char'] === undefined ? undefined : slots['-fsigned-char'] === 'on',
2300
+ slots['-funsigned-char'] === undefined ? undefined : slots['-funsigned-char'] === 'off',
2301
+ ].filter((x) => x !== undefined);
2302
+ return said.length === 0 ? 'default' : said.every((x) => x === said[0]) ? said[0] : undefined;
2303
+ }
2304
+
1347
2305
  /** Build the structurer's options for a target: the function's own `returnsVoid` plus every
1348
2306
  * `compilerBehaviors` field. The ONE place a target's compiler behaviors flow into the
1349
2307
  * target-agnostic structurer — a new compiler behavior is a field in `compilerBehaviors`, consumed
@@ -1359,9 +2317,8 @@ export function structureOptionsFor(
1359
2317
  returnsVoid: boolean,
1360
2318
  prototypes: Prototypes = {},
1361
2319
  ): StructureOptions {
1362
- // `littleEndian` and `deviceRegisters` are the HARDWARE capabilities the structurer consumes
1363
- // (bitfield extract recognition is LSB-first; the dead-read spelling refuses outside the device
1364
- // window); everything else is a compiler behavior.
2320
+ // `littleEndian` is the HARDWARE capability the structurer consumes (bitfield extract recognition
2321
+ // is LSB-first); everything else is a compiler behavior.
1365
2322
  //
1366
2323
  // ONE FIELD IS NOT A STRAIGHT SPREAD, and this is where the difference belongs. A frame
1367
2324
  // direction has THREE states here — `ascending`, `descending`, and `'unknown'` meaning measured
@@ -1372,16 +2329,19 @@ export function structureOptionsFor(
1372
2329
  const { spillSlotOrder, ...behaviors } = t.compilerBehaviors;
1373
2330
  return {
1374
2331
  returnsVoid,
1375
- declaredArgs: declaredArgTypes(prototypes),
2332
+ declaredArgs: declaredArgTypes(prototypes, t.dialect !== 'c++'),
1376
2333
  declaredReturns: declaredReturnTypes(prototypes),
1377
2334
  ...(t.dialect === 'c++' ? { dialect: 'c++' as const } : {}),
1378
2335
  littleEndian: t.capabilities.endianness === 'little',
1379
- ...(t.capabilities.deviceRegisters ? { deviceRegisters: t.capabilities.deviceRegisters } : {}),
1380
2336
  ...behaviors,
1381
2337
  ...(spillSlotOrder === 'ascending' || spillSlotOrder === 'descending' ? { spillSlotOrder } : {}),
1382
2338
  };
1383
2339
  }
1384
2340
 
2341
+ /** The global a register `globalRegisterVariables` names is lifted as (`asmName` its `__asm__`
2342
+ * name: `greg_r8`, and `greg_r16` for MIPS `$16`). */
2343
+ export const globalRegisterSymbol = (asmName: string): string => `greg_${asmName.replace(/^\$/, 'r')}`;
2344
+
1385
2345
  /** The candidate prelude: a `typedef` for every name in the scalar vocabulary. PRINTED from
1386
2346
  * `proto.ts`'s `PRELUDE_TYPEDEFS` rather than spelled out, because the other reader of that
1387
2347
  * vocabulary decides which type texts asmlift may PRINT into a candidate, and a second copy of
@@ -1393,18 +2353,32 @@ export const C_TYPEDEFS = `${[...PRELUDE_TYPEDEFS].map(([name, base]) => `typede
1393
2353
 
1394
2354
  /** Each declared callee's parameter types, where one can be PRINTED as a cast — the argument
1395
2355
  * conversions C++ refuses to make implicitly (backend/cfamily.ts `argConversion`), and a pointer
1396
- * cell's integer arguments (l3/ptrcell.ts). An entry this cannot spell is `undefined`, and that
1397
- * argument is printed uncast. */
1398
- function declaredArgTypes(prototypes: Prototypes): Record<string, readonly (ParamType | undefined)[]> {
2356
+ * cell's integer arguments (l3/ptrcell.ts). In C (`opaquePointers`), an object pointer to a type
2357
+ * this cannot spell — `struct S *` — is kept too: the printer casts its argument through `void *`,
2358
+ * which C converts to any object pointer implicitly, where a strict front end (mwcc) refuses
2359
+ * `use2(s)` for a `u32 s[2]` the static's bytes were typed as. Any other entry this cannot spell is
2360
+ * `undefined`, and that argument is printed uncast. */
2361
+ function declaredArgTypes(
2362
+ prototypes: Prototypes,
2363
+ opaquePointers: boolean,
2364
+ ): Record<string, readonly (ParamType | undefined)[]> {
1399
2365
  const out: Record<string, readonly (ParamType | undefined)[]> = {};
1400
2366
  for (const [name, p] of Object.entries(prototypes)) {
1401
2367
  if (Array.isArray(p.params)) {
1402
- out[name] = p.params.map((t) => (spellableType(t) && declaredWidth(t) !== undefined ? t : undefined));
2368
+ out[name] = p.params.map((t) =>
2369
+ (spellableType(t) && declaredWidth(t) !== undefined) || (opaquePointers && objectPointerType(t))
2370
+ ? t
2371
+ : undefined,
2372
+ );
1403
2373
  }
1404
2374
  }
1405
2375
  return out;
1406
2376
  }
1407
2377
 
2378
+ /** A pointer to an object, a pointer to a function excluded: `void *` converts to it implicitly in
2379
+ * C, which it does not to a function pointer. */
2380
+ const objectPointerType = (t: ParamType): boolean => /\*\s*(?:const|volatile|\s)*$/.test(t) && !t.includes('(');
2381
+
1408
2382
  /** Each declared callee's return type as the project spells it, which is never printed: whether a
1409
2383
  * pointer cell compared with the call meets a pointer (l3/ptrcell.ts). */
1410
2384
  function declaredReturnTypes(prototypes: Prototypes): Record<string, ParamType> {