@asmlift/core 0.5.0 → 0.7.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.
- package/README.md +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +270 -171
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +243 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +78 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +345 -2
- package/src/ir/opcodes.ts +176 -21
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -5
- package/src/l3/ast.ts +510 -59
- package/src/l3/basecse.ts +686 -78
- package/src/l3/coalesce.ts +432 -46
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +96 -1
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +176 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +114 -89
- package/src/l3/reindex.ts +722 -80
- package/src/l3/scopebase.ts +649 -220
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +16 -1
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +687 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +239 -16
- package/src/pipeline.ts +173 -60
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +4 -4
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +1058 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +572 -0
- package/src/raise/paramwidth.ts +201 -0
- package/src/raise/pre-recovery.ts +169 -21
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +585 -19
- package/src/raise/shortcircuit.ts +1050 -89
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +34 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank-variations.ts +760 -0
- package/src/rank.ts +2122 -326
- package/src/structure/analysis.ts +1398 -150
- package/src/structure/bitfields.ts +432 -0
- package/src/structure/globalaccess.ts +300 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +454 -0
- package/src/structure/structure.ts +3979 -612
- package/src/structure/switch-recover.ts +710 -145
- package/src/symbols.ts +188 -6
- package/src/target.ts +495 -32
- package/src/trace.ts +112 -33
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
package/src/target.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// asmlift — the Target: (isa, compiler) as first-class
|
|
1
|
+
// asmlift — the Target: (isa, compiler) as first-class fields. ABI + capabilities are DATA
|
|
2
2
|
// consumed generically by shared passes — never a target-name branch inside a shared pass
|
|
3
3
|
// (m2c's `arch.arch ==` leakage).
|
|
4
4
|
//
|
|
@@ -10,11 +10,32 @@
|
|
|
10
10
|
// pre-recovery pass, and idiom gating; a `div` on a target declaring no divider degrades to
|
|
11
11
|
// a loud opaque (exercised by packages/cli/test/matching/divmul.test.ts). `hwFloat` → idiom
|
|
12
12
|
// gating only (no float pass yet).
|
|
13
|
-
// • capabilities.endianness
|
|
14
|
-
//
|
|
15
|
-
// •
|
|
13
|
+
// • capabilities.endianness → structureOptionsFor (`littleEndian`), gating LSB-first
|
|
14
|
+
// bitfield-extract recognition in the structurer.
|
|
15
|
+
// • capabilities.flags → RESERVED, not yet read by any pass (PPC condition regs will).
|
|
16
|
+
// • capabilities.readOnlyAddressSinks → the Thumb frame-object audit: a frame address stored to
|
|
17
|
+
// one of these reached a device that only reads through it, so it does not retract `undef`.
|
|
18
|
+
// • capabilities.deviceRegisters → five readers, and they ask ONE question — "would a source
|
|
19
|
+
// have spelled this address `volatile`" — which is a question about SPELLING and may be
|
|
20
|
+
// approximate: the `/vol-store` variation's eligibility (l3/volstore.ts), rank.ts's volatility
|
|
21
|
+
// tie-break between two byte-identical spellings, the first half of `/unreduce`'s
|
|
22
|
+
// disjointness gate (l3/unreduce.ts), the `/homesplit` pairing's refusal to leave a device
|
|
23
|
+
// READ inline where the spelling it replaces would have qualified it (l3/homesplit.ts), and
|
|
24
|
+
// the structurer's refusal to SPELL a dead memory read whose address no qualifier could ever
|
|
25
|
+
// reach (structure.ts `volatileQualifiable`, threaded through StructureOptions).
|
|
26
|
+
// • capabilities.deviceMemoryWriters → the MEMORY-MODEL question, which is a different one and
|
|
27
|
+
// may NOT be approximate: "can a write to this register make the DEVICE write ordinary
|
|
28
|
+
// memory". One reader — `/unreduce`'s second half. Split from `deviceRegisters` because
|
|
29
|
+
// conflating them recorded a false premise (see the field's own comment).
|
|
30
|
+
// • compilerBehaviors.* → mostly consumed by the structurer (threaded via StructureOptions).
|
|
31
|
+
// Five exceptions are read off the target directly, their consumers not being the
|
|
32
|
+
// structurer: `nearBaseSpan` and `foldsConstAddrOffset` (rank.ts, L3 respell variations),
|
|
33
|
+
// `reloadsLocalReread` (raise/pre-recovery.ts), `hoistsSingleSetArm` (two raising passes —
|
|
34
|
+
// raise/narrowlocal.ts and raise/retsink.ts) and
|
|
35
|
+
// `arrayShapeFromStride` (raise/globalshape.ts, run on the LIFTED fn). The field names are a
|
|
36
|
+
// SUPERSET of StructureOptions' — see `structureOptionsFor`.
|
|
16
37
|
//
|
|
17
|
-
// `capabilities` (HARDWARE facts) vs `compilerBehaviors` (COMPILER canonicalization
|
|
38
|
+
// `capabilities` (HARDWARE facts) vs `compilerBehaviors` (COMPILER canonicalization decisions) are
|
|
18
39
|
// deliberately separate bags: a new compiler must set its behaviors EXPLICITLY instead of
|
|
19
40
|
// silently inheriting a universal that is really per-compiler. `coalesceLoopInit` already
|
|
20
41
|
// differs across targets (IDO true, agbcc/GCC false).
|
|
@@ -25,43 +46,302 @@ import type { StructureOptions } from './structure/structure';
|
|
|
25
46
|
|
|
26
47
|
export interface TargetDescription {
|
|
27
48
|
id: string; // the ISA — 'armv4t' / 'mips' / 'ppc'. Selects the frontend (registry.ts).
|
|
28
|
-
// The COMPILER is a first-class
|
|
49
|
+
// The COMPILER is a first-class field distinct from the ISA (matching = deoptimize to a specific
|
|
29
50
|
// compiler): two targets can share an ISA (⇒ one frontend) yet differ here — e.g. MIPS_IDO vs
|
|
30
51
|
// MIPS_GCC. Consumed by pattern gating (patternApplies) and the report. (version/flags/language
|
|
31
|
-
// are future
|
|
52
|
+
// are future fields, added when earned.)
|
|
32
53
|
compiler: string; // 'agbcc' / 'ido' / 'gcc' / 'mwcc'
|
|
33
54
|
argRegs: string[];
|
|
34
55
|
returnReg: string;
|
|
56
|
+
/** Registers this ABI does NOT pass arguments in — half of what makes a def-less live-in read an
|
|
57
|
+
* uninitialised local rather than an argument. The other half is a measurement the FRONTEND
|
|
58
|
+
* owes (did this function save the register), and the rule that combines them is in
|
|
59
|
+
* frontend/ssa.ts (LiveInModel.uninitRegs). ABSENT ⇒ no register partition is claimed, which is
|
|
60
|
+
* what MIPS and PPC take today.
|
|
61
|
+
*
|
|
62
|
+
* It must be DISJOINT from `argRegs`, and the frontend hands both to the builder so that is
|
|
63
|
+
* checked rather than trusted (`checkedLiveInModel`): a spelling that lands in both lists used
|
|
64
|
+
* to delete a parameter and emit `uninit_<reg>` in its place, silently. */
|
|
65
|
+
nonArgRegs?: readonly string[];
|
|
66
|
+
/** Of `nonArgRegs`, the ones this ABI does NOT require a callee to preserve — so the compiler may
|
|
67
|
+
* home a local in one with no prologue save at all, and the save half of the rule above does not
|
|
68
|
+
* apply to it. AAPCS's `ip` is the whole set here, and agbcc really does use it that way.
|
|
69
|
+
*
|
|
70
|
+
* UNDER-stating this list only makes the classification stricter: an unlisted register whose save
|
|
71
|
+
* the frontend cannot find falls back to being a parameter, which is what a target claiming no
|
|
72
|
+
* partition gets. OVER-stating it is the unsound direction — a callee-saved register listed here
|
|
73
|
+
* is classified with no evidence at all, which is the defect the save half exists to close. Every
|
|
74
|
+
* entry must appear in `nonArgRegs`; the frontend refuses a target where one does not. */
|
|
75
|
+
scratchRegs?: readonly string[];
|
|
35
76
|
// HARDWARE / ISA facts — independent of the compiler.
|
|
36
77
|
capabilities: {
|
|
37
78
|
endianness: 'little' | 'big'; // consumed by structureOptionsFor (bitfield extract recognition is LSB-first)
|
|
38
79
|
hwDivide: boolean; // consumed by patternApplies (idiom gating)
|
|
39
80
|
hwFloat: boolean; // consumed by patternApplies (idiom gating)
|
|
40
81
|
flags: boolean; // RESERVED — no pass reads it yet (PPC condition regs will)
|
|
82
|
+
// Addresses a device reads an object THROUGH. A frame address stored to one of these is handed
|
|
83
|
+
// over as a transfer SOURCE, and two facts together are what make that safe to model: the
|
|
84
|
+
// device only ever reads from it, and the register is WRITE-ONLY, so nobody can read the
|
|
85
|
+
// address back out and turn it into a destination. The only code that can name the frame is
|
|
86
|
+
// therefore this function's own, which the Thumb frame-object audit walks.
|
|
87
|
+
//
|
|
88
|
+
// Hardware, so it belongs here — `endianness` above is a board fact rather than an ISA one too
|
|
89
|
+
// (ARMv4T is bi-endian). ABSENT ⇒ every escape is assumed to write, which is the safe
|
|
90
|
+
// direction and what every other target gets.
|
|
91
|
+
readOnlyAddressSinks?: readonly number[];
|
|
92
|
+
// The device-register window, `[start, end)`. A cell in it changes under the program's feet,
|
|
93
|
+
// so a source that touched one all but certainly declared it `volatile`. Its readers all ask
|
|
94
|
+
// the same SPELLING question — "would a source have written `volatile` here" — and the file
|
|
95
|
+
// header's ledger names them and what each does with the answer. None of them decides for the
|
|
96
|
+
// reader: which cells a source qualified is not derivable from the asm, so both spellings are
|
|
97
|
+
// enumerated and the differ referees. ABSENT ⇒ the variation declines everywhere and the tie-break
|
|
98
|
+
// has no preference, which is the neutral direction — outside a declared window the qualifier
|
|
99
|
+
// is a claim about ordinary memory that the target does not support.
|
|
100
|
+
//
|
|
101
|
+
// IT IS NOT A MEMORY-MODEL CLAIM, and reading it as one is how a false premise got recorded
|
|
102
|
+
// in four places (`deviceMemoryWriters` below carries the correction). Approximating the
|
|
103
|
+
// range costs a candidate; approximating the memory model costs a wrong answer.
|
|
104
|
+
deviceRegisters?: readonly [number, number];
|
|
105
|
+
// Byte ranges, `[start, end)`, whose WRITE can make the DEVICE write ordinary memory. The
|
|
106
|
+
// separate, stronger claim: `deviceRegisters` says a cell is not an object a source declares,
|
|
107
|
+
// which is true and says nothing about what the DEVICE then does. A DMA controller reads a
|
|
108
|
+
// control word and writes memory on the program's behalf, so a loop whose every write is a
|
|
109
|
+
// "device register" write can still rewrite any cell — including one a moved read reads.
|
|
110
|
+
//
|
|
111
|
+
// GBA: the four DMA channel CONTROL halfwords (DMAnCNT_H). Bit 15 is the channel enable, and
|
|
112
|
+
// writing it with the bit set starts the transfer immediately; the other three registers of a
|
|
113
|
+
// channel (SAD, DAD, CNT_L) only stage it — which is the same split `readOnlyAddressSinks`
|
|
114
|
+
// above already reasons about from the source side. A store is a trigger when its BYTE RANGE
|
|
115
|
+
// touches one of these, so the 32-bit `DMA3CNT` write every GBA DMA macro ends with
|
|
116
|
+
// (`*(vu32 *)0x040000DC = 0x84000020`) is one, and a halfword write to `DMA3CNT_L` is not.
|
|
117
|
+
//
|
|
118
|
+
// ABSENT ⇒ the target claims nothing, and the one reader treats EVERY device write as a
|
|
119
|
+
// possible memory write — the conservative direction, and what every non-GBA target takes.
|
|
120
|
+
deviceMemoryWriters?: readonly (readonly [number, number])[];
|
|
41
121
|
};
|
|
42
|
-
// COMPILER BEHAVIORS — the specific compiler's canonicalization
|
|
43
|
-
// hardware `capabilities`.
|
|
122
|
+
// COMPILER BEHAVIORS — the specific compiler's canonicalization decisions, distinct from
|
|
123
|
+
// hardware `capabilities`. Mostly consumed by the structurer (threaded through StructureOptions);
|
|
124
|
+
// the exceptions are listed at the top of this file and each says so at its own field.
|
|
44
125
|
compilerBehaviors: {
|
|
45
126
|
// When a loop induction variable's initial value comes from an argument register, some
|
|
46
127
|
// compilers keep mutating that register across the loop (coalesce → no init copy); others
|
|
47
|
-
// copy to a fresh local. IDO -O2
|
|
48
|
-
// fresh (false).
|
|
128
|
+
// copy to a fresh local. IDO -O2 and KMC GCC -O2 reuse the arg register (true); agbcc
|
|
129
|
+
// allocates fresh (false).
|
|
49
130
|
coalesceLoopInit?: boolean;
|
|
50
131
|
// Divergent-if (both arms terminate, no join): reproduce the source branch DIRECTION by
|
|
51
132
|
// emitting the forward-branch-on-negated-condition (taken arm as `else`). IDO/MIPS preserves
|
|
52
133
|
// source direction so this must be on to be byte-exact; agbcc/GCC canonicalize either way so
|
|
53
134
|
// true is a safe default there. A compiler that inverts branch canonicalization sets it
|
|
54
|
-
// false. Absent ⇒ true; a compiler opts OUT.
|
|
135
|
+
// false. Absent ⇒ true; a compiler opts OUT. It carries the JOINED case with it:
|
|
136
|
+
// StructureOptions.negateJoinedBranchSense defaults to this value, so the first compiler that
|
|
137
|
+
// preserves divergent sense and inverts joined sense splits them by promoting that option to a
|
|
138
|
+
// field here — never by an `arch ==` branch in the structurer.
|
|
55
139
|
preserveDivergentBranchSense?: boolean;
|
|
56
|
-
// Order the parallel-copy assignments at a CFG edge by the order
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
140
|
+
// Order the parallel-copy assignments at a CFG edge by the order the PREDECESSOR WROTE THEIR
|
|
141
|
+
// DESTINATIONS — the frontend's own measurement (ir/core.ts `WriteOrder`), falling back to a
|
|
142
|
+
// def-position proxy on a predecessor no frontend measured. Not "computation order": a
|
|
143
|
+
// destination written with a value defined elsewhere is a plain register copy, and it ranks by
|
|
144
|
+
// where that copy sits, not by where its value was computed. Uniform (true) across all current
|
|
145
|
+
// compilers; absent ⇒ true, and a compiler that opts OUT turns the sort off entirely and emits
|
|
146
|
+
// in source/param order. WHICH order a measured edge takes is not this flag's question and
|
|
147
|
+
// cannot be: the benchmark has rows on both sides inside one compiler (mwcc), so that decision is
|
|
148
|
+
// refereed per row by `/copy-defpos` (rank.ts), never declared per compiler here.
|
|
149
|
+
orderArgCopiesByWriteOrder?: boolean;
|
|
61
150
|
// Regime-A switch recovery: accept an `x != K` test as a case (the EQUAL side is the case
|
|
62
151
|
// body). GCC freely emits `!=`; IDO prefers `==`/`<`. Absent ⇒ true (permissive); the
|
|
63
152
|
// decline path keeps recovery sound either way.
|
|
64
153
|
switchAllowsNeqCase?: boolean;
|
|
154
|
+
// The compiler collapses `if (…) x = a; else x = b;` into `x = b; if (…) x = a;` when both
|
|
155
|
+
// arms are ONE speculatable SET — gcc 2.x's `jump_optimize` (`gcc/jump.c:443-445`, guard at
|
|
156
|
+
// `:471-502`). Absent ⇒ false, and every clause below never admits. `structureOptionsFor`
|
|
157
|
+
// spreads it onto StructureOptions like every other field here, but NO structurer code reads
|
|
158
|
+
// it: both readers are raising passes, threaded from their driver's own `target`.
|
|
159
|
+
//
|
|
160
|
+
// TWO READERS, ONE FACT, BOTH READING IT BACKWARDS. One field rather than one per reader,
|
|
161
|
+
// because a second boolean for the same guard lets a round that measures another compiler's
|
|
162
|
+
// `jump_optimize` set one and leave the other false, with both comments reading as
|
|
163
|
+
// authoritative.
|
|
164
|
+
//
|
|
165
|
+
// • raise/narrowlocal.ts's `edge-extends`: a diamond this compiler would have collapsed and
|
|
166
|
+
// did NOT is evidence the source DECLARED the local narrow, because `gcc/thumb.h:344`
|
|
167
|
+
// PROMOTE_MODE expands a narrow-declared assignment past one SET.
|
|
168
|
+
// • raise/retsink.ts's `compiler-hoists-single-set-arm`: a merge-variable select whose arms
|
|
169
|
+
// this guard would have collapsed never comes back as a diamond, so a TARGET holding one
|
|
170
|
+
// was written with early returns and its returns should be sunk.
|
|
171
|
+
//
|
|
172
|
+
// Set on agbcc, where the 2x2 in raise/narrowlocal.ts's header was compiled and scored and
|
|
173
|
+
// where retsink's seven-function spelling pair was compiled and committed
|
|
174
|
+
// (`packages/core/test/corpus/agbcc-select-{merge,early}.s`). NOT set on MIPS_GCC despite it
|
|
175
|
+
// being the same compiler family: nothing has measured the pair there, the clause reaches 0 of
|
|
176
|
+
// its benchmark rows on either reader, and `docs/level-tower.md`'s rule for an unmeasured
|
|
177
|
+
// compiler behavior is to claim nothing. The evidence a future round needs is one run of
|
|
178
|
+
// `scripts/regen-select-spelling-probes.ts` retargeted at the compiler in question.
|
|
179
|
+
hoistsSingleSetArm?: boolean;
|
|
180
|
+
// A subscript over a DECLARED ARRAY OBJECT expands its base ahead of the index, where every
|
|
181
|
+
// pointer or cast base expands it last — so the instruction order in the target's own assembly
|
|
182
|
+
// says which of the two the source wrote, and `raise/globalshape.ts` may derive an array shape
|
|
183
|
+
// for a global no symbol map describes. Absent ⇒ the derivation is empty and every indexed
|
|
184
|
+
// global keeps today's `((T *)&gSym)[i]` cast spelling.
|
|
185
|
+
//
|
|
186
|
+
// "Expands it last" is about the SUBSCRIPT, and a second consumer reads the same flag for a
|
|
187
|
+
// question that is not: `orderLicensedGlobals` asks only where the base was materialized, and a
|
|
188
|
+
// pointer LOCAL materializes it in its own initializer STATEMENT, before the subscript runs —
|
|
189
|
+
// so `u16 *p = (u16 *)&gTbl; p[i]` is base-first in the object while `(p = (u16 *)&gTbl)[i]` is
|
|
190
|
+
// index-first, both through this same fork (compiled; raise/globalshape.ts's header carries the
|
|
191
|
+
// four-way table). This flag is therefore NARROWER than that consumer's mechanism — statement
|
|
192
|
+
// ordering needs no fork, only a compiler that does not schedule — so the home variation is denied
|
|
193
|
+
// to ido/kmc/mwcc for a reason that is not its own. Under-reach, unmeasured, and the fix when a
|
|
194
|
+
// row asks for it is a datum of its own rather than a widening of this one.
|
|
195
|
+
//
|
|
196
|
+
// Set on agbcc, where the fork is `gcc/c-typeck.c build_array_ref`'s
|
|
197
|
+
// `TREE_CODE (TREE_TYPE (array)) == ARRAY_TYPE && TREE_CODE (array) != INDIRECT_REF` and both
|
|
198
|
+
// spellings were compiled against the same target. NOT set anywhere else: whether ido, kmc or
|
|
199
|
+
// mwcc distinguish them at all is unmeasured, and `docs/level-tower.md`'s rule for an
|
|
200
|
+
// unmeasured compiler behavior is to claim nothing. Read off the target by a raising pass
|
|
201
|
+
// (`inferGlobalArrays`), not by the structurer.
|
|
202
|
+
arrayShapeFromStride?: boolean;
|
|
203
|
+
// Which way this compiler hands out FRAME SLOTS against a spilled local's DECLARATION RANK:
|
|
204
|
+
// `ascending` = the earlier-declared spilled local takes the LOWER `[sp,#k]`. Consumed by the
|
|
205
|
+
// structurer (StructureOptions.spillSlotOrder) and applied at emit time by l3/slotorder.ts.
|
|
206
|
+
//
|
|
207
|
+
// `'unknown'` and absent both REFUSE the ordering — there is deliberately no default
|
|
208
|
+
// direction, because the wrong one reorders every declaration list on that target for no
|
|
209
|
+
// reason. That is why three of the four descriptions below ship `'unknown'`: no MIPS or PPC
|
|
210
|
+
// benchmark row lifts with two or more spilled user locals, so no row on those tiers can
|
|
211
|
+
// referee a value, and a value no row can falsify does not earn the level. Two of the three
|
|
212
|
+
// have a direction MEASURED against a committed probe and withheld for that reason; the third
|
|
213
|
+
// (mwcc) has no direction at all, because the probe does not spill there. Each says which it
|
|
214
|
+
// is, and its flip condition, at its own site.
|
|
215
|
+
//
|
|
216
|
+
// KEYED BY DESCRIPTION, WHILE THE FACT IS PER TOOLCHAIN — the first field in this bag with a
|
|
217
|
+
// stated instance of that gap. `MIPS_GCC` serves BOTH `gcc2.7.2kmc` (Snowboard Kids 2's Kyoto
|
|
218
|
+
// build at -O2) and `gcc2.7.2` (Mario Party 3's at -O1); they agree here, and a committed probe
|
|
219
|
+
// says so, but nothing in this bag could express it if they did not. Any behavior that can
|
|
220
|
+
// differ between two toolchains sharing one description is mis-keyed by construction.
|
|
221
|
+
spillSlotOrder?: 'ascending' | 'descending' | 'unknown';
|
|
222
|
+
// Regime-A switch recovery: accept a RELATIONAL test whose BRANCH admits exactly one scrutinee
|
|
223
|
+
// value as that case (`cmp r0, #1 / bcc` is `case 0:` of an unsigned switch) rather than as
|
|
224
|
+
// navigation.
|
|
225
|
+
//
|
|
226
|
+
// A DEFAULT rather than a candidate variation because for agbcc the asm determines the source: at
|
|
227
|
+
// -O2 fold-const rewrites a bounded unsigned comparison into an equality before codegen, so
|
|
228
|
+
// `x < 1u` compiles to `cmp r0, #0 / bne` and `x > 0u` to `cmp r0, #0 / beq` — no source-level
|
|
229
|
+
// comparison chain emits a bound test at all. `emit_case_nodes` runs after folding and does:
|
|
230
|
+
// it jumps straight to `node->left->code_label` on LT once `node_is_bounded (node->left)`, so
|
|
231
|
+
// the remaining value's own test is never emitted. One producer, one reading.
|
|
232
|
+
//
|
|
233
|
+
// Absent ⇒ false, and inheriting it would be wrong rather than merely unmeasured: on the MIPS
|
|
234
|
+
// lanes `sltiu rd, rs, 1` is the ordinary spelling of `!x`, and it lifts to `icmp_ult rs, 1`
|
|
235
|
+
// with no equality fold anywhere — the identical IR shape, from a producer that is not a
|
|
236
|
+
// dispatch. Each compiler opts in on its own dispatch's evidence.
|
|
237
|
+
switchAllowsBoundCase?: boolean;
|
|
238
|
+
// Switch recovery: emit the case arms in the order the ASSEMBLY lays their bodies out, rather
|
|
239
|
+
// than sorted by ascending case value. True claims the compiler emits case bodies as it walks
|
|
240
|
+
// the arms and never MOVES one afterwards — neither reordering basic blocks nor scheduling
|
|
241
|
+
// across them. agbcc declares it from its own sources: `stmt.c` expand_end_case takes
|
|
242
|
+
// `before_case = get_last_insn()` AFTER the bodies are expanded in source order and its closing
|
|
243
|
+
// `reorder_insns` moves only the DISPATCH in front of them, and the Makefile's SRCS compiles
|
|
244
|
+
// neither sched.c nor reorg.c. SCOPE — SRCS does compile jump.c, whose cross-jump merges two
|
|
245
|
+
// identical arm bodies into ONE block, so a merged pair's own order is gone from the asm; that
|
|
246
|
+
// surfaces as two case values sharing a body, which switch-recover.ts ties by ascending value.
|
|
247
|
+
// Absent ⇒ ascending case value, where ido/kmc-gcc/mwcc sit: each has a scheduler and none has
|
|
248
|
+
// been put through that evidence. A compiler opts in on its own, never by inheriting.
|
|
249
|
+
switchArmsFollowLayout?: boolean;
|
|
250
|
+
// Switch recovery: DECLINE a comparison tree whose own layout INTERLEAVES a test block with a
|
|
251
|
+
// case body, on the reading that the source wrote an if/else-if LADDER there. True claims the
|
|
252
|
+
// compiler emits a source `switch`'s whole dispatch AHEAD of every arm body — the same
|
|
253
|
+
// `expand_end_case` closing `reorder_insns` `switchArmsFollowLayout` is read off, used for the
|
|
254
|
+
// other half of what it does — while a ladder's tests stay above their own bodies.
|
|
255
|
+
//
|
|
256
|
+
// IT NEEDS HALF OF `switchArmsFollowLayout`'s PREMISE, AND THE IMPLICATION RUNS ONE WAY ONLY.
|
|
257
|
+
// That flag PLACES the arms and so needs the whole no-reordering claim (nothing moved a block
|
|
258
|
+
// at all); this one only asks whether any BODY sits above a test, so a compiler that moves
|
|
259
|
+
// instructions, fills delay slots, or reorders within a block can still declare it. A compiler
|
|
260
|
+
// that declares the PLACING one has therefore already said what this one needs — never the
|
|
261
|
+
// converse. `MIPS_GCC` is the standing counterexample to the converse: it declares this flag on
|
|
262
|
+
// its own pairs (below) and deliberately does NOT declare `switchArmsFollowLayout`, because it
|
|
263
|
+
// has a scheduler. Declaring this one is not evidence for that one.
|
|
264
|
+
//
|
|
265
|
+
// agbcc declares it, and its own pair of objects says the reading is not vacuous: at
|
|
266
|
+
// TOOLCHAIN.agbccFlags the same two-case body is 20 bytes (0x14, ten Thumb instructions)
|
|
267
|
+
// written either way and is a DIFFERENT object — the `switch` emits `cmp #0x1e; beq` then
|
|
268
|
+
// `cmp #0x64; bne` before either body, sorted ascending and so in the reverse of the written
|
|
269
|
+
// order; the ladder emits `cmp #0x64; bne` directly above its own body and reaches `cmp #0x1e`
|
|
270
|
+
// only after it. The pair is committed: `corpus/agbcc-sw{frontload,ladder}.s` from
|
|
271
|
+
// `corpus/probe-agbcc-sw{frontload,ladder}.c`, regenerated by
|
|
272
|
+
// `scripts/regen-switch-spelling-probes.ts`, asserted in switch-arms.test.ts.
|
|
273
|
+
//
|
|
274
|
+
// Absent ⇒ every recoverable tree is still spelled `switch`, which is where ido/mwcc sit: each
|
|
275
|
+
// has a scheduler that may move a body above a test, and neither has been put through the pair.
|
|
276
|
+
// A compiler opts in on its own compiled evidence, never by inheriting — and where one
|
|
277
|
+
// description serves two toolchains (`MIPS_GCC`), each toolchain owes its own pair, because the
|
|
278
|
+
// field cannot distinguish them (the KEYED BY DESCRIPTION note at `spillSlotOrder`).
|
|
279
|
+
switchRequiresFrontLoadedTests?: boolean;
|
|
280
|
+
// Commutative load pairs re-spell in def (evaluation) order (structure.ts lowerDef). Absent
|
|
281
|
+
// ⇒ true — verified byte-exact on agbcc and IDO; a compiler whose scheduler is shown
|
|
282
|
+
// re-ordering independent loads opts OUT here.
|
|
283
|
+
defOrderLoadPairs?: boolean;
|
|
284
|
+
// The single-add-immediate derivation reach for the /nearbase variation (l3/nearbase.ts):
|
|
285
|
+
// neighbor absolute addresses within this many bytes may share one base local. Thumb's
|
|
286
|
+
// `add rd, #imm8` reaches 255. Absent ⇒ the variation stands down for this target.
|
|
287
|
+
nearBaseSpan?: number;
|
|
288
|
+
// Does this compiler CONSTANT-FOLD a constant SUBSCRIPT into the literal address it
|
|
289
|
+
// materializes for an inline constant-address access? agbcc does: `((u8 *)0x3001100)[3]`
|
|
290
|
+
// emits `.word 0x3001103` + `ldrb [r1]` where `u8 *p = (u8 *)0x3001100; p[3]` keeps
|
|
291
|
+
// `.word 0x3001100` + `ldrb [r1, #0x3]`. True is what lets an offset surviving into the memory
|
|
292
|
+
// operand say anything about the source at all; what l3/basecse.ts's `/basefold` admission
|
|
293
|
+
// does with it — and why that is a differ-refereed candidate rather than a default — is that
|
|
294
|
+
// file's header. A compiler opts in on its own compiled pair and never by inheriting: the MIPS
|
|
295
|
+
// and PPC lanes put the addend in the instruction by construction (`lui`/`%lo`, `lis`/`ori`),
|
|
296
|
+
// so a surviving offset carries no information there. Absent ⇒ the row is never offered.
|
|
297
|
+
foldsConstAddrOffset?: boolean;
|
|
298
|
+
// Does this compiler fold a pointer local's OWN ADVANCE back into the memory operand —
|
|
299
|
+
// `*p = a; p = p + 1; *p = b;` → `strh [r3, #0]` + `strh [r3, #2]`, no `add` — so the advanced
|
|
300
|
+
// spelling emits the indexed one's stores wherever the pointee is not volatile? agbcc does, on
|
|
301
|
+
// its own compiled evidence: the four corners in test/advance.test.ts's header, each built
|
|
302
|
+
// through the benchmark's agbcc against `kleod:StreamCmd_SetWindowRegs`'s object, and the pair
|
|
303
|
+
// `TARGET_BEHAVIOR_READINGS` compiles to one object in the matching suite. True ⇒
|
|
304
|
+
// the `advance` registry entry's target gate withholds the UN-QUALIFIED variation, whose spelling this compiler cannot
|
|
305
|
+
// distinguish from the indexed one it already offers; `/advance/volatile` still rides, because
|
|
306
|
+
// `volatile` is what bars the fold and that product is the match on this row. Absent ⇒ falsy ⇒
|
|
307
|
+
// the plain variation ships, which is the conservative reading for a compiler whose pair nobody has
|
|
308
|
+
// compiled — a compiler opts in on its own evidence and never by inheriting.
|
|
309
|
+
foldsPointerAdvance?: boolean;
|
|
310
|
+
// Does this compiler EMIT a memory read in the block the source SPELLED it in? One direction
|
|
311
|
+
// only: the def-block placement rule (StructureOptions.readsStayWhereWritten) re-spells a read
|
|
312
|
+
// at the block the asm performed it in, which reproduces the asm iff nothing sinks a spelled
|
|
313
|
+
// read past a branch and nothing lifts one to a dominator. The CONVERSE — the asm's read block
|
|
314
|
+
// is where the source read — is FALSE even here, and no default may be declared as if it held.
|
|
315
|
+
//
|
|
316
|
+
// agbcc (gcc 2.9-arm, -O2) declares TRUE from its own sources plus a compiled pair: gcc's
|
|
317
|
+
// Makefile SRCS compiles neither sched.c nor reorg.c and toplev.c never mentions
|
|
318
|
+
// flag_schedule_insns, so there is no scheduler; gcse.c calls one_code_hoisting_pass only
|
|
319
|
+
// `if (optimize_size)`, which toplev.c sets only for -Os, so at -O2 the hoister is compiled in
|
|
320
|
+
// and never runs (a -Os project would NOT get this declaration); and `s = *g; if (c) A(s);
|
|
321
|
+
// else B(s);` against `if (c) A(*g); else B(*g);` emits one ldrb + one pool word versus one of
|
|
322
|
+
// each PER ARM, moving neither. The two passes that DO move a read between blocks at -O2 —
|
|
323
|
+
// loop invariant motion, and the PRE that makes the converse false — are refusals the rule
|
|
324
|
+
// owes; structure/analysis.ts carries them.
|
|
325
|
+
//
|
|
326
|
+
// ABSENT ⇒ the rule stands down, where ido/kmc-gcc/mwcc sit: each has a scheduler and none has
|
|
327
|
+
// been put through that pair. A compiler opts in on its own evidence, never by inheriting.
|
|
328
|
+
readsStayWhereWritten?: boolean;
|
|
329
|
+
// Does a LOCAL initialised with a memory read its dominating TEST already performed cost this
|
|
330
|
+
// compiler a SECOND load? The pair is the arm of `if (a && (p[1] & 0x7f) == 0x7f) { … }` spelled
|
|
331
|
+
// `u8 v = p[1]; p[2] = v;` against `p[2] = p[1];`, and again with a store and with a call
|
|
332
|
+
// between the local and its use. agbcc loads `p[1]` TWICE for every local spelling and once
|
|
333
|
+
// for the inline one; ido7.1, gcc2.7.2kmc, gcc2.7.2 and mwcc_242_81 load it ONCE for every
|
|
334
|
+
// local spelling, holding the register across the store and across the call. agbcc is the
|
|
335
|
+
// odd one out of five, so the one agbcc-shaped claim that rested on it — raise/shortcircuit.ts's
|
|
336
|
+
// `read-behind-effect`, "a copy analysis.ts spells as a local costs a load" — reads it here
|
|
337
|
+
// rather than running on every target — on mwcc it costs the probe named at PPC_MWCC's value
|
|
338
|
+
// its byte-match.
|
|
339
|
+
// Read off the target by a raising pass (raise/pre-recovery.ts), not by the structurer.
|
|
340
|
+
//
|
|
341
|
+
// ABSENT ⇒ false: the refusal stands down, and a compiler opts IN on its own compiled pair. The
|
|
342
|
+
// three descriptions that measured false (four compilers) set it anyway, so absent means
|
|
343
|
+
// UNMEASURED rather than "no".
|
|
344
|
+
reloadsLocalReread?: boolean;
|
|
65
345
|
};
|
|
66
346
|
}
|
|
67
347
|
|
|
@@ -70,8 +350,70 @@ export const ARMV4T_AGBCC: TargetDescription = {
|
|
|
70
350
|
compiler: 'agbcc',
|
|
71
351
|
argRegs: ['r0', 'r1', 'r2', 'r3'],
|
|
72
352
|
returnReg: 'r0',
|
|
73
|
-
|
|
74
|
-
|
|
353
|
+
// AAPCS passes four in r0-r3, so nothing above them can be an argument. The ATPCS aliases are
|
|
354
|
+
// the spellings this ISA's asm actually uses: censused over the vendored ARM asm, `sb`/`sl`/`ip`/
|
|
355
|
+
// `fp` all occur as operands and no `v<n>`/`a<n>` form does. `sp`, `lr` and `pc` are deliberately
|
|
356
|
+
// absent — sp is the frame, lr is the return address, and neither is a value a source declared.
|
|
357
|
+
nonArgRegs: ['r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10', 'r11', 'r12', 'sb', 'sl', 'fp', 'ip'],
|
|
358
|
+
// AAPCS makes r4-r11 callee-saved and leaves r12 (`ip`, the intra-procedure-call scratch) to the
|
|
359
|
+
// caller, so a local in `ip` needs no save and agbcc puts one there: `dma_fill_uninit` compiles to
|
|
360
|
+
// `mov ip, r1` in two switch arms, no save anywhere, and a `mov r0, ip` past a third arm that
|
|
361
|
+
// writes nothing — an uninitialised local by construction.
|
|
362
|
+
scratchRegs: ['r12', 'ip'],
|
|
363
|
+
// GBA hardware, which this target implies: agbcc is the GBA compiler and this is the only
|
|
364
|
+
// armv4t entry, so `armv4t + agbcc` is the platform. Stated because nothing else states it.
|
|
365
|
+
capabilities: {
|
|
366
|
+
endianness: 'little',
|
|
367
|
+
hwDivide: false,
|
|
368
|
+
hwFloat: false,
|
|
369
|
+
flags: true,
|
|
370
|
+
// The four DMA SOURCE registers (DMA0..3 SAD). Every vendored project spells the transfer the
|
|
371
|
+
// same way — `DmaSet(n, src, dest, control)` takes `vu32 *dmaRegs = REG_ADDR_DMA<n>SAD` and
|
|
372
|
+
// writes `dmaRegs[0] = src`, `dmaRegs[1] = dest`, `dmaRegs[2] = control` — so +0 is the address
|
|
373
|
+
// the engine reads from and the destination is 4 bytes above it. Source Address Control has
|
|
374
|
+
// three legal settings (increment, decrement, fixed) and every one of them is a read; the
|
|
375
|
+
// reload mode that could re-arm a transfer exists only on the DESTINATION side.
|
|
376
|
+
//
|
|
377
|
+
// The idiom this exists for is their `DMA_FILL`: `vu16 tmp = value;
|
|
378
|
+
// DmaSet(n, &tmp, dest, … DMA_SRC_FIXED …)`, where the frame local is the source.
|
|
379
|
+
readOnlyAddressSinks: [0x040000b0, 0x040000bc, 0x040000c8, 0x040000d4],
|
|
380
|
+
// The GBA I/O register file — one page from 0x04000000, the last live register being
|
|
381
|
+
// 0x04000301 (HALTCNT). Everything a source reaches through `REG_*` is in here, and nothing
|
|
382
|
+
// else is: IWRAM, EWRAM, palette, VRAM and OAM are ordinary memory a source does not qualify.
|
|
383
|
+
deviceRegisters: [0x04000000, 0x04000400],
|
|
384
|
+
// DMA0..3 CNT_H — the channel-enable halfwords. Writing one with bit 15 set arms the transfer,
|
|
385
|
+
// and the transfer writes ordinary memory at [DMAnDAD]. Every other I/O register on this board
|
|
386
|
+
// is read or written by the CPU alone.
|
|
387
|
+
deviceMemoryWriters: [
|
|
388
|
+
[0x040000ba, 0x040000bc],
|
|
389
|
+
[0x040000c6, 0x040000c8],
|
|
390
|
+
[0x040000d2, 0x040000d4],
|
|
391
|
+
[0x040000de, 0x040000e0],
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
compilerBehaviors: {
|
|
395
|
+
coalesceLoopInit: false,
|
|
396
|
+
preserveDivergentBranchSense: true,
|
|
397
|
+
orderArgCopiesByWriteOrder: true,
|
|
398
|
+
nearBaseSpan: 255,
|
|
399
|
+
foldsConstAddrOffset: true,
|
|
400
|
+
foldsPointerAdvance: true,
|
|
401
|
+
readsStayWhereWritten: true,
|
|
402
|
+
switchAllowsBoundCase: true,
|
|
403
|
+
switchArmsFollowLayout: true,
|
|
404
|
+
switchRequiresFrontLoadedTests: true,
|
|
405
|
+
hoistsSingleSetArm: true,
|
|
406
|
+
arrayShapeFromStride: true,
|
|
407
|
+
reloadsLocalReread: true,
|
|
408
|
+
// agbcc: reload walks pseudos ascending handing each global-alloc loser a fresh slot, a user
|
|
409
|
+
// local's pseudo number is its `expand_decl` position, and the Thumb frame grows UPWARD
|
|
410
|
+
// (FRAME_GROWS_DOWNWARD is commented out in thumb.h). So the earlier-declared spilled local
|
|
411
|
+
// takes the lower offset. The rows that referee it are `synthetic:spillorder` (six `[sp,#k]`
|
|
412
|
+
// operand rows and nothing else, from two locals declared the other way round) and its
|
|
413
|
+
// control `synthetic:spillorder_rev` (the same body in the order asmlift already emits, which
|
|
414
|
+
// must stay a MATCH), plus `synthetic:dma_fill_uninit`, a row this did not author.
|
|
415
|
+
spillSlotOrder: 'ascending',
|
|
416
|
+
},
|
|
75
417
|
};
|
|
76
418
|
|
|
77
419
|
/** MIPS-II / IDO 7.1 target. IDO is the IRIX C compiler,
|
|
@@ -90,8 +432,26 @@ export const MIPS_IDO: TargetDescription = {
|
|
|
90
432
|
compilerBehaviors: {
|
|
91
433
|
coalesceLoopInit: true,
|
|
92
434
|
preserveDivergentBranchSense: true,
|
|
93
|
-
|
|
435
|
+
orderArgCopiesByWriteOrder: true,
|
|
94
436
|
switchAllowsNeqCase: false,
|
|
437
|
+
// MEASURED — the pair at the field compiles to one load of `p[1]` for every local spelling.
|
|
438
|
+
reloadsLocalReread: false,
|
|
439
|
+
// MEASURED `descending` (the earlier-declared spilled local takes the HIGHER offset) and NOT
|
|
440
|
+
// SHIPPED. The probe is COMMITTED — `packages/core/test/corpus/probe-declrank.c` and its
|
|
441
|
+
// reversed-declaration twin, with this compiler's objects beside them — and a test reads the
|
|
442
|
+
// correspondence off it: 16 of 16 spills, and rank → offset unchanged when the declaration
|
|
443
|
+
// list is reversed, which is what separates declaration rank from the order of the
|
|
444
|
+
// assignments. No ido7.1 benchmark row lifts with two or more spilled user locals — the only
|
|
445
|
+
// spilling shape in the corpus carries a call, and this frontend declines a call — so no row
|
|
446
|
+
// can tell a wrong value from a right one here.
|
|
447
|
+
//
|
|
448
|
+
// FLIP CONDITION, and it has TWO parts because the second is easy to miss. (1) The first
|
|
449
|
+
// ido7.1 row that lifts with two spilled locals. (2) `frontend/mips.ts` must first claim a
|
|
450
|
+
// frame partition (`LiveInModel.declaredLocals`); until it does, the shared stamp refuses every
|
|
451
|
+
// MIPS slot, so this value would order nothing — and if the partition were claimed WRONGLY,
|
|
452
|
+
// O32's caller-owned home area `[0,16)` would be read as this function's first four
|
|
453
|
+
// declaration ranks. Shipping a direction before the partition orders by argument index.
|
|
454
|
+
spillSlotOrder: 'unknown',
|
|
95
455
|
},
|
|
96
456
|
};
|
|
97
457
|
|
|
@@ -103,10 +463,74 @@ export const MIPS_GCC: TargetDescription = {
|
|
|
103
463
|
compiler: 'gcc',
|
|
104
464
|
argRegs: ['a0', 'a1', 'a2', 'a3'],
|
|
105
465
|
returnReg: 'v0',
|
|
106
|
-
// KMC GCC
|
|
107
|
-
//
|
|
466
|
+
// KMC GCC keeps a loop seeded from an argument register IN that register (coalesceLoopInit
|
|
467
|
+
// true, like IDO): test/corpus/gcc-gcd.asm runs its whole loop on a0/a1 with no init copies,
|
|
468
|
+
// and the row it comes from matches only with the parameters as the loop's homes. The other
|
|
469
|
+
// structuring compiler behaviors take the universal default until a KMC fixture says otherwise.
|
|
470
|
+
//
|
|
471
|
+
// THIS IS A COMPILER-WIDE GUESS STANDING IN FOR A PER-FUNCTION OBSERVATION the assembly states
|
|
472
|
+
// outright: whether the compiler kept a loop's induction variable in its argument register. What
|
|
473
|
+
// would say it is "the header param's register key IS the key the entry value already lives in" —
|
|
474
|
+
// known to the SSA builder (`frontend/ssa.ts` `phiKey`) and to this file (`argRegs`), unexposed.
|
|
475
|
+
// Exposing it would replace two booleans (here, and PPC_MWCC's "false until a CW loop fixture
|
|
476
|
+
// says otherwise") with a measurement.
|
|
477
|
+
// NOT the obvious proxy for it, which was built and measured: adopting the entry value's name
|
|
478
|
+
// when the forward predecessor did not WRITE the param's key moves 36 of the 736 synthetic rows
|
|
479
|
+
// and costs four matches net (continueloop, countpos and loopif on mwcc plus dmafill, dmaptrsrc
|
|
480
|
+
// and dmastride on agbcc lost; maxarr and preupdate_exit_call on agbcc gained) — because a pred
|
|
481
|
+
// that computes the initial value INTO the param's own register wrote the key and still
|
|
482
|
+
// coalesces.
|
|
108
483
|
capabilities: { endianness: 'big', hwDivide: true, hwFloat: true, flags: false },
|
|
109
|
-
compilerBehaviors: {
|
|
484
|
+
compilerBehaviors: {
|
|
485
|
+
coalesceLoopInit: true,
|
|
486
|
+
preserveDivergentBranchSense: true,
|
|
487
|
+
orderArgCopiesByWriteOrder: true,
|
|
488
|
+
// DECLARED ON A PAIR FROM EACH TOOLCHAIN THIS DESCRIPTION SERVES, never inherited from agbcc's
|
|
489
|
+
// and never from one sibling to the other. This description is keyed per DESCRIPTION while the
|
|
490
|
+
// fact is per TOOLCHAIN (the note at `spillSlotOrder`), and `MIPS_GCC` serves two, so both owe
|
|
491
|
+
// a pair: `corpus/gcc272kmc-sw{frontload,ladder}.asm` from GCC_KMC_TOOLCHAIN at -O2 and
|
|
492
|
+
// `corpus/gcc272-sw{frontload,ladder}.asm` from the Mario Party 3 toolchain at -O1 — one
|
|
493
|
+
// two-case body written each way, regenerated from their committed C bodies by
|
|
494
|
+
// `scripts/regen-switch-spelling-probes.ts`, each carrying a provenance header, and each pair
|
|
495
|
+
// two different objects: the `switch` emits both `beq`s before the first `sw`, while the ladder
|
|
496
|
+
// puts an arm's `sw` above the second test. A test asserts that split off every fixture. The
|
|
497
|
+
// two toolchains come out byte-identical on this body — measured, not assumed, and their
|
|
498
|
+
// declaration-rank probe objects differ, so a sibling pair is not a formality.
|
|
499
|
+
//
|
|
500
|
+
// BOTH TOOLCHAINS EMIT BRANCH-LIKELY on some bodies, and the reading survives it — a property
|
|
501
|
+
// of the pair, not a way the two diverge. `s32 m1(s32 x, s32 *p){ switch (x) { case 6: *p = 1;
|
|
502
|
+
// break; case 7: *p = 2; break; } return 0; }` cross-jumps the two stores into one and compiles
|
|
503
|
+
// instruction for instruction identically at -O1 and at -O2, to `beq` / `beql` with the shared
|
|
504
|
+
// `sw` after both; the same body as an if/else-if ladder puts the first arm's `li v0,1` BETWEEN
|
|
505
|
+
// the two tests. Same split — not committed as a fixture only because `beql` is an unmodelled
|
|
506
|
+
// control transfer, so the row declines before PRE5 and the cross-jumped arms leave no store to
|
|
507
|
+
// read the split off. Re-measure it the day branch-likely lands. (A THREE-case body says the
|
|
508
|
+
// same more loudly, the balanced tree's `slti` bound test landing ahead of the bodies with the
|
|
509
|
+
// rest, but at three cases neither spelling reaches Regime A on this compiler, so that pair
|
|
510
|
+
// could not also serve as the recovery test.)
|
|
511
|
+
//
|
|
512
|
+
// WHAT IS WEAKER HERE THAN AT agbcc: this compiler HAS a scheduler and fills delay slots, and
|
|
513
|
+
// both fixtures show it — the ladder's `bne` carries the NEXT test's `li` in its slot. What the
|
|
514
|
+
// pair shows is that it moves no BODY above a test, which is the only claim the gate rests on,
|
|
515
|
+
// and the gate's failure direction (switch-recover.ts PRE5) is a lost `switch` spelling, never
|
|
516
|
+
// a wrong answer. A lost spelling is not always a clean ladder: recovery re-runs on the
|
|
517
|
+
// sub-trees a decline leaves, so a NESTED dispatch comes back as an `if` nest around a `switch`
|
|
518
|
+
// over some of its arms.
|
|
519
|
+
switchRequiresFrontLoadedTests: true,
|
|
520
|
+
// MEASURED on BOTH toolchains this description serves (the note above): one load of `p[1]` for
|
|
521
|
+
// every local spelling of the pair at the field, gcc2.7.2kmc at -O2 and gcc2.7.2 at -O1 alike.
|
|
522
|
+
reloadsLocalReread: false,
|
|
523
|
+
// MEASURED `ascending` on both toolchains this description serves — 7 of 7 spills each, and
|
|
524
|
+
// rank → offset unchanged under a reversed declaration list — and NOT SHIPPED, for the same
|
|
525
|
+
// reason as ido7.1: no row on either tier lifts with two or more spilled user locals. Both
|
|
526
|
+
// probes are COMMITTED beside ido7.1's (`corpus/gcc272kmc-declrank*.txt`,
|
|
527
|
+
// `corpus/gcc272-declrank*.txt`) and a test reads the direction off them.
|
|
528
|
+
//
|
|
529
|
+
// The two agreeing is not a formality. The value is per DESCRIPTION and TWO toolchains map
|
|
530
|
+
// here, so a toolchain whose direction differed from its description's would need a
|
|
531
|
+
// per-toolchain override this bag cannot express — see the note at `compilerBehaviors`.
|
|
532
|
+
spillSlotOrder: 'unknown',
|
|
533
|
+
},
|
|
110
534
|
};
|
|
111
535
|
|
|
112
536
|
/** PowerPC (GameCube/Wii) + Metrowerks CodeWarrior. The real GC/Wii matching target is
|
|
@@ -122,19 +546,58 @@ export const PPC_MWCC: TargetDescription = {
|
|
|
122
546
|
argRegs: ['r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10'],
|
|
123
547
|
returnReg: 'r3',
|
|
124
548
|
capabilities: { endianness: 'big', hwDivide: true, hwFloat: true, flags: true },
|
|
125
|
-
// CodeWarrior's structuring
|
|
126
|
-
// defaults; coalesceLoopInit false until a CW loop fixture says otherwise
|
|
127
|
-
|
|
549
|
+
// CodeWarrior's structuring compiler behaviors are UNKNOWN until fixtures reveal them — safe universal
|
|
550
|
+
// defaults; coalesceLoopInit false until a CW loop fixture says otherwise — the second of the
|
|
551
|
+
// two compiler-wide guesses standing in for the per-function observation named at MIPS_GCC.
|
|
552
|
+
compilerBehaviors: {
|
|
553
|
+
coalesceLoopInit: false,
|
|
554
|
+
preserveDivergentBranchSense: true,
|
|
555
|
+
orderArgCopiesByWriteOrder: true,
|
|
556
|
+
// MEASURED — one load of `p[1]` for every local spelling of the pair at the field, the value
|
|
557
|
+
// held in a callee-saved register across the call. And the rule it turns off pays here:
|
|
558
|
+
// `u8 v = p[3]; if ((v & 0x7f) == 0x7f) { fnA(); p[4] = v; return; }` under an `if (a)`
|
|
559
|
+
// matches only once `read-behind-effect` stops refusing it (3/24 → MATCH 0/22).
|
|
560
|
+
reloadsLocalReread: false,
|
|
561
|
+
// NOT MEASURED, and `'unknown'` is therefore the only honest value here rather than a withheld
|
|
562
|
+
// one, as it is at MIPS_IDO and MIPS_GCC. No mwcc row lifts with two or more spilled user
|
|
563
|
+
// locals, and the compiler does not spill the committed declaration-rank probe either: at
|
|
564
|
+
// sixteen locals it homes every one in a register, and at forty it sinks the whole computation
|
|
565
|
+
// past the call so nothing is live across it. And, as at MIPS_IDO, the frame partition comes
|
|
566
|
+
// first: `frontend/ppc.ts` claims no `LiveInModel.declaredLocals`, so the shared stamp records
|
|
567
|
+
// no slot home on this target at all and a direction here would order nothing until it does.
|
|
568
|
+
spillSlotOrder: 'unknown',
|
|
569
|
+
},
|
|
128
570
|
};
|
|
129
571
|
|
|
130
572
|
/** Build the structurer's options for a target: the function's own `returnsVoid` plus every
|
|
131
|
-
* `compilerBehaviors`
|
|
132
|
-
* target
|
|
133
|
-
*
|
|
573
|
+
* `compilerBehaviors` field. The ONE place a target's compiler behaviors flow into the
|
|
574
|
+
* target-agnostic structurer — a new compiler behavior is a field in `compilerBehaviors`, consumed
|
|
575
|
+
* automatically.
|
|
576
|
+
*
|
|
577
|
+
* The spread is over the WHOLE bag, so a behavior whose reader is not the structurer rides along
|
|
578
|
+
* and is simply never read: `hoistsSingleSetArm` is one (its reader is a pre-recovery pass), and
|
|
579
|
+
* `nearBaseSpan` / `foldsConstAddrOffset` are read off the target by rank.ts. So the field names
|
|
580
|
+
* are a SUPERSET of StructureOptions', not a bijection, and nothing may derive one from the other
|
|
581
|
+
* by enumerating keys. */
|
|
134
582
|
export function structureOptionsFor(t: TargetDescription, returnsVoid: boolean): StructureOptions {
|
|
135
|
-
// `littleEndian`
|
|
136
|
-
// recognition is LSB-first
|
|
137
|
-
|
|
583
|
+
// `littleEndian` and `deviceRegisters` are the HARDWARE capabilities the structurer consumes
|
|
584
|
+
// (bitfield extract recognition is LSB-first; the dead-read spelling refuses outside the device
|
|
585
|
+
// window); everything else is a compiler behavior.
|
|
586
|
+
//
|
|
587
|
+
// ONE FIELD IS NOT A STRAIGHT SPREAD, and this is where the difference belongs. A frame
|
|
588
|
+
// direction has THREE states here — `ascending`, `descending`, and `'unknown'` meaning measured
|
|
589
|
+
// and deliberately not shipped (see `spillSlotOrder` above) — and only TWO downstream: the
|
|
590
|
+
// structurer either has a direction or refuses. `'unknown'` is a fact about what this repo
|
|
591
|
+
// measured, not an instruction to a pass, so it is dropped at the translation rather than
|
|
592
|
+
// carried onto a public option type that would then need a third case nobody branches on.
|
|
593
|
+
const { spillSlotOrder, ...behaviors } = t.compilerBehaviors;
|
|
594
|
+
return {
|
|
595
|
+
returnsVoid,
|
|
596
|
+
littleEndian: t.capabilities.endianness === 'little',
|
|
597
|
+
...(t.capabilities.deviceRegisters ? { deviceRegisters: t.capabilities.deviceRegisters } : {}),
|
|
598
|
+
...behaviors,
|
|
599
|
+
...(spillSlotOrder === 'ascending' || spillSlotOrder === 'descending' ? { spillSlotOrder } : {}),
|
|
600
|
+
};
|
|
138
601
|
}
|
|
139
602
|
|
|
140
603
|
export const C_TYPEDEFS =
|