@asmlift/core 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -24
- package/package.json +1 -1
- package/src/backend/cfamily.ts +39 -11
- package/src/backend/pascal.ts +2 -2
- package/src/codegen-flags.ts +640 -0
- package/src/contracts.ts +60 -11
- package/src/frontend/disasm.ts +141 -11
- package/src/frontend/high-half.ts +149 -0
- package/src/frontend/mips.ts +458 -209
- package/src/frontend/ppc.ts +332 -67
- package/src/frontend/reloc-symbol.ts +109 -0
- package/src/frontend/splat.ts +56 -18
- package/src/frontend/ssa.ts +127 -30
- package/src/frontend/stackargs.ts +420 -0
- package/src/frontend/thumb.ts +209 -232
- package/src/ir/alias.ts +24 -0
- package/src/ir/core.ts +70 -3
- package/src/ir/opcodes.ts +52 -7
- package/src/ir/parse.ts +7 -1
- package/src/ir/simplify.ts +1 -1
- package/src/l3/address.ts +2 -2
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -6
- package/src/l3/argcopy.ts +269 -0
- package/src/l3/ast.ts +110 -22
- package/src/l3/basecse.ts +50 -30
- package/src/l3/coalesce.ts +118 -61
- package/src/l3/gates.ts +75 -1
- package/src/l3/hoist.ts +1 -1
- package/src/l3/homesplit.ts +13 -13
- package/src/l3/initfirst.ts +3 -3
- package/src/l3/inlinebase.ts +16 -16
- package/src/l3/mentions.ts +68 -5
- package/src/l3/mulfirst.ts +3 -3
- package/src/l3/nearbase.ts +4 -4
- package/src/l3/offmember.ts +5 -5
- package/src/l3/parkfirst.ts +6 -6
- package/src/l3/pollguard.ts +3 -3
- package/src/l3/ptrfield.ts +4 -4
- package/src/l3/regspell.ts +8 -8
- package/src/l3/reindex.ts +22 -17
- package/src/l3/scopebase.ts +32 -29
- package/src/l3/sinkinit.ts +7 -7
- package/src/l3/slotorder.ts +3 -3
- package/src/l3/storage.ts +1 -1
- package/src/l3/tailmerge.ts +2 -2
- package/src/l3/tailret.ts +70 -0
- package/src/l3/typing.ts +3 -3
- package/src/l3/unmerge.ts +483 -59
- package/src/l3/unreduce.ts +15 -14
- package/src/l3/volatileptr.ts +11 -11
- package/src/l3/volatileval.ts +11 -11
- package/src/l3/volstore.ts +16 -16
- package/src/l3/zerosub.ts +6 -6
- package/src/mangle.ts +49 -0
- package/src/pattern/engine.ts +132 -17
- package/src/pipeline.ts +39 -16
- package/src/proto.ts +2 -2
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/extscale.ts +345 -0
- package/src/raise/globalshape.ts +32 -12
- package/src/raise/gvn.ts +2 -2
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +4 -4
- package/src/raise/narrowlocal.ts +18 -2
- package/src/raise/paramwidth.ts +133 -3
- package/src/raise/pre-recovery.ts +100 -25
- package/src/raise/retsink.ts +389 -19
- package/src/raise/shortcircuit.ts +595 -34
- package/src/raise/structs.ts +4 -4
- package/src/raise/tailsink.ts +141 -0
- package/src/rank-declare.ts +21 -13
- package/src/{rank-axes.ts → rank-variations.ts} +319 -189
- package/src/rank.ts +1176 -805
- package/src/structure/analysis.ts +87 -90
- package/src/structure/bitfields.ts +130 -30
- package/src/structure/globalaccess.ts +30 -4
- package/src/structure/namecoalesce.ts +32 -13
- package/src/structure/retspell.ts +95 -0
- package/src/structure/structure.ts +1425 -201
- package/src/structure/switch-recover.ts +101 -8
- package/src/symbols.ts +127 -6
- package/src/target.ts +374 -44
- package/src/trace.ts +28 -19
- package/src/variation-definitions.ts +1590 -0
- package/src/variation-gates.ts +92 -0
- package/src/variation-tokens.ts +356 -0
|
@@ -0,0 +1,1590 @@
|
|
|
1
|
+
// asmlift — what every registered variation MEANS, for a reader: the webapp's Fan Explorer, the
|
|
2
|
+
// function detail's winning spelling, and anyone reading a candidate's name in a log.
|
|
3
|
+
//
|
|
4
|
+
// KEYED BY THE REGISTRY, NEVER A SECOND LIST OF NAMES. `VARIATION_DEFINITIONS` is typed by
|
|
5
|
+
// `VariationName`, which `variation-tokens.ts` derives from `VARIATION_TOKENS`, so a registry entry
|
|
6
|
+
// with no definition and a definition naming no registered variation are both type errors in this
|
|
7
|
+
// package. `packages/core/test/variation-definitions.test.ts` asserts the same at run time, and
|
|
8
|
+
// `apps/benchmark/test/variation-closure.test.ts` checks every name the committed artifact
|
|
9
|
+
// publishes against it.
|
|
10
|
+
//
|
|
11
|
+
// WHY CORE AND NOT `@asmlift/bench-schema`, where the feature vocabulary lives. A feature tag is a
|
|
12
|
+
// fact about a benchmark ROW, authored in the dataset and derived by `apps/benchmark`; a variation
|
|
13
|
+
// is a fact about core's ENUMERATION, minted in `rank.ts`, printed by the CLI and shown by the
|
|
14
|
+
// playground as much as by the benchmark pages. Its name set is already here, and the bench-schema
|
|
15
|
+
// package takes no dependencies.
|
|
16
|
+
//
|
|
17
|
+
// A READING OF THE CODE, NEVER A SOURCE FOR IT. Nothing here is consulted by enumeration. Each entry
|
|
18
|
+
// paraphrases the argument at its mint site in `rank.ts` / `rank-variations.ts` and at the pass
|
|
19
|
+
// `implementedIn` names; where they disagree, the code is right and this entry is the defect. Three
|
|
20
|
+
// parts are held to the code rather than trusted: each `example` compiles to two different objects
|
|
21
|
+
// (`packages/cli/test/matching/variation-examples.test.ts`), each `offeredWhen` names the tables and
|
|
22
|
+
// the export that decide it (`packages/core/test/variation-offers.test.ts`), and the drawer's target
|
|
23
|
+
// line is the registry entry's `target`, which enumeration asks.
|
|
24
|
+
//
|
|
25
|
+
// Pure data: this module stays browser-safe. `offeredWhen` names admission tables by key; their rules
|
|
26
|
+
// are `variation-gates.ts`, which a reader of a title or a summary never loads.
|
|
27
|
+
import type { FlagFamily } from './codegen-flags';
|
|
28
|
+
import type { CanonicalToolchainId } from './target';
|
|
29
|
+
import type { GateTableName } from './variation-gates';
|
|
30
|
+
import type { GatingBehavior, VariationKind, VariationName } from './variation-tokens';
|
|
31
|
+
|
|
32
|
+
export interface ReaderWord {
|
|
33
|
+
word: string;
|
|
34
|
+
meaning: string;
|
|
35
|
+
/** the command that shows it, for a contributor; the table's cell is `meaning` then `command` */
|
|
36
|
+
command?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The words a reader of candidate enumeration needs, as `docs/vocabulary.md` defines them. The
|
|
40
|
+
* test holds that table to this one, text for text. */
|
|
41
|
+
export const READER_WORDS: readonly ReaderWord[] = [
|
|
42
|
+
{
|
|
43
|
+
word: 'candidate',
|
|
44
|
+
meaning:
|
|
45
|
+
'One complete C source asmlift emits for a function. Each is compiled and scored against the target object.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
word: 'fan',
|
|
49
|
+
meaning: 'Every candidate asmlift enumerated for one function, whether it built or not.',
|
|
50
|
+
command: '`pnpm bench fan <row>` lists it.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
word: 'winner',
|
|
54
|
+
meaning: "The best-scoring candidate among those that may be published. Its source is the function's result.",
|
|
55
|
+
command: '`pnpm bench fan <row> --show winner` prints it.',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
word: 'variation',
|
|
59
|
+
meaning:
|
|
60
|
+
'One way asmlift can write a function differently, e.g. `defsite`, `unmerge`, `raw-globals`. Signedness (`unsigned` / `signed`) is a variation too.',
|
|
61
|
+
},
|
|
62
|
+
{ word: 'dropped', meaning: 'A candidate the scorer refused: its source did not build.' },
|
|
63
|
+
{
|
|
64
|
+
word: 'withheld',
|
|
65
|
+
meaning: 'A candidate that compiled and scored, but was refused publication for want of a byte-exact proof.',
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
export interface VariationKindDefinition {
|
|
70
|
+
title: string;
|
|
71
|
+
meaning: string;
|
|
72
|
+
/** registered variations of this kind, as a candidate's name spells them */
|
|
73
|
+
examples: readonly string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The five variation kinds, as `docs/vocabulary.md`'s kinds table defines them, in kind order. */
|
|
77
|
+
export const VARIATION_KIND_DEFINITIONS: { readonly [K in VariationKind]: VariationKindDefinition } = {
|
|
78
|
+
signedness: {
|
|
79
|
+
title: 'Signedness',
|
|
80
|
+
meaning:
|
|
81
|
+
"Whether the function's parameters are read as signed or unsigned. Every candidate carries one, as its first variation.",
|
|
82
|
+
examples: ['unsigned', 'signed'],
|
|
83
|
+
},
|
|
84
|
+
lift: {
|
|
85
|
+
title: 'Lift',
|
|
86
|
+
meaning:
|
|
87
|
+
'How the instructions are read before any C is built: which moves set up a call, how a chain of tests joins, whether paths share a return.',
|
|
88
|
+
examples: ['setup-args', 'connective', 'shared-ret', 'shared-tail'],
|
|
89
|
+
},
|
|
90
|
+
structure: {
|
|
91
|
+
title: 'Structure',
|
|
92
|
+
meaning:
|
|
93
|
+
'How the checked control flow becomes C: which way a test reads, where a loop starts, what reaches a merge (the point where two paths meet), where a value is kept, and how a read or a compare is spelled.',
|
|
94
|
+
examples: ['flip-branch', 'defsite', 'loop-entry', 'reread-globals', 'uns-cmp'],
|
|
95
|
+
},
|
|
96
|
+
respell: {
|
|
97
|
+
title: 'Respell',
|
|
98
|
+
meaning:
|
|
99
|
+
'A rewrite of the finished C that keeps what it does: where a value lives, whether an address is held in a pointer, how statements are ordered.',
|
|
100
|
+
examples: ['unmerge', 'offmember', 'livebase', 'coalesce-v0-v1', 'volatile'],
|
|
101
|
+
},
|
|
102
|
+
'symbol-map': {
|
|
103
|
+
title: 'Symbol map',
|
|
104
|
+
meaning:
|
|
105
|
+
"Globals are written as raw addresses instead of the names the project's symbol map gives them. Always the last variation.",
|
|
106
|
+
examples: ['raw-globals'],
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** A compiler an example is built with: a compiler family, as the benchmark's rows name it. */
|
|
111
|
+
export type ExampleCompiler = FlagFamily;
|
|
112
|
+
|
|
113
|
+
/** The toolchain each example compiler builds with. An example and a witness are claims at that
|
|
114
|
+
* toolchain's canonical flags (`TOOLCHAIN_TARGETS`), which is what the matching suite compiles them
|
|
115
|
+
* at — so the toolchain named here must be one that HAS them, which the type says. */
|
|
116
|
+
export const EXAMPLE_COMPILER_TOOLCHAINS: { readonly [C in ExampleCompiler]: CanonicalToolchainId } = {
|
|
117
|
+
agbcc: 'agbcc',
|
|
118
|
+
ido: 'ido7.1',
|
|
119
|
+
gcc: 'gcc2.7.2kmc',
|
|
120
|
+
mwcc: 'mwcc_242_81',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** Each example compiler as a reader knows it. */
|
|
124
|
+
export const EXAMPLE_COMPILER_NAMES: { readonly [C in ExampleCompiler]: string } = {
|
|
125
|
+
agbcc: 'agbcc',
|
|
126
|
+
ido: 'IDO 7.1',
|
|
127
|
+
gcc: 'KMC gcc',
|
|
128
|
+
mwcc: 'CodeWarrior (mwcc)',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** Where a spelling sits in its example's translation unit. No C token is spelled `@`. */
|
|
132
|
+
export const EXAMPLE_HOLE = '@';
|
|
133
|
+
|
|
134
|
+
/** The function every example's translation unit defines, and the one its two objects are compared on. */
|
|
135
|
+
export const EXAMPLE_FUNCTION = 'example';
|
|
136
|
+
|
|
137
|
+
/** A minimal pair the matching suite compiles (`packages/cli/test/matching/variation-examples.test.ts`):
|
|
138
|
+
* `unit` with `before` in its hole and with `after` in it must be two different objects under
|
|
139
|
+
* `compiler`. The drawer shows the two fragments; the unit is what makes them C. */
|
|
140
|
+
export interface VariationExample {
|
|
141
|
+
/** the spelling without the variation */
|
|
142
|
+
before: string;
|
|
143
|
+
/** the spelling with it */
|
|
144
|
+
after: string;
|
|
145
|
+
compiler: ExampleCompiler;
|
|
146
|
+
/** a complete translation unit defining `EXAMPLE_FUNCTION`, holding `EXAMPLE_HOLE` exactly once */
|
|
147
|
+
unit: string;
|
|
148
|
+
note?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** An export of a core source file, relative to the repository root: where a decision is made. */
|
|
152
|
+
export interface CodePointer {
|
|
153
|
+
symbol: string;
|
|
154
|
+
file: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** What holds a reading to the compiler: two spellings in `unit`'s hole, compiled by the compiler of
|
|
158
|
+
* every shipped target that declares the behavior, that build one object (`same`) or two. A
|
|
159
|
+
* behavior no pair of spellings can show says why instead. */
|
|
160
|
+
export type BehaviorWitness =
|
|
161
|
+
| { compiler: ExampleCompiler; unit: string; spellings: readonly [string, string]; compiles: 'same' | 'different' }
|
|
162
|
+
| { uncompiled: string };
|
|
163
|
+
|
|
164
|
+
/** A compiler behavior as a reader reads it after "a target whose compiler", and its witness. */
|
|
165
|
+
export interface TargetBehaviorReading {
|
|
166
|
+
reads: string;
|
|
167
|
+
witness: BehaviorWitness;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Each compiler behavior a registered variation's target gate names. The gate itself is the registry
|
|
171
|
+
* entry's `target`; the matching suite compiles each witness. */
|
|
172
|
+
export const TARGET_BEHAVIOR_READINGS: { readonly [B in GatingBehavior]: TargetBehaviorReading } = {
|
|
173
|
+
foldsConstAddrOffset: {
|
|
174
|
+
reads: 'folds a constant address offset into the literal it loads',
|
|
175
|
+
witness: {
|
|
176
|
+
compiler: 'agbcc',
|
|
177
|
+
unit: 'void example(u8 a) { @ }',
|
|
178
|
+
spellings: ['((u8 *)0x3001100)[3] = a;', '*(u8 *)0x3001103 = a;'],
|
|
179
|
+
compiles: 'same',
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
arrayShapeFromStride: {
|
|
183
|
+
reads: "loads a declared array's base before it scales the index",
|
|
184
|
+
witness: {
|
|
185
|
+
compiler: 'agbcc',
|
|
186
|
+
unit: 'extern u16 gTbl[]; s32 example(s32 i) { return @; }',
|
|
187
|
+
spellings: ['gTbl[i]', '((u16 *)&gTbl)[i]'],
|
|
188
|
+
compiles: 'different',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
nearBaseSpan: {
|
|
192
|
+
reads: 'declares how far one base local may reach a neighbouring address',
|
|
193
|
+
witness: {
|
|
194
|
+
uncompiled:
|
|
195
|
+
'the span is how far one instruction can add to a base register, a fact of the instruction set that no pair of spellings decides',
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
foldsPointerAdvance: {
|
|
199
|
+
reads: 'folds a stepped pointer back into an offset load',
|
|
200
|
+
witness: {
|
|
201
|
+
compiler: 'agbcc',
|
|
202
|
+
unit: 'void example(s32 a, s32 b) { @ }',
|
|
203
|
+
spellings: [
|
|
204
|
+
'u16 *p = (u16 *)0x04000048; *p = a; p[1] = b;',
|
|
205
|
+
'u16 *p = (u16 *)0x04000048; *p = a; p = p + 1; *p = b;',
|
|
206
|
+
],
|
|
207
|
+
compiles: 'same',
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/** When enumeration offers a variation. Wherever it changes nothing, the candidate it would add
|
|
213
|
+
* repeats a source an earlier candidate has, and is not enumerated.
|
|
214
|
+
*
|
|
215
|
+
* - `'always'`: on every function.
|
|
216
|
+
* - `judges` and `gates`: for each thing `judges` names, a noun phrase, that no rule of `gates`
|
|
217
|
+
* refuses. The rules are read from the tables themselves.
|
|
218
|
+
* - `when` and `decidedBy`: where no table decides, one sentence and the export that does. `gates`
|
|
219
|
+
* names the tables that export applies for part of the decision.
|
|
220
|
+
*
|
|
221
|
+
* A variation offered only on some compilers says so in its registry entry's `target`. */
|
|
222
|
+
export type OfferedWhen =
|
|
223
|
+
| 'always'
|
|
224
|
+
| { judges: string; gates: readonly GateTableName[] }
|
|
225
|
+
| { when: string; decidedBy: CodePointer; gates?: readonly GateTableName[] };
|
|
226
|
+
|
|
227
|
+
export interface VariationDefinition {
|
|
228
|
+
/** a short heading: the catalogue row, the drawer title */
|
|
229
|
+
title: string;
|
|
230
|
+
/** one line: the catalogue subtitle and a chip's tooltip */
|
|
231
|
+
summary: string;
|
|
232
|
+
/** what the variation changes in the emitted C, and why the assembly leaves that open */
|
|
233
|
+
detail: string;
|
|
234
|
+
/** the compiler behavior that makes the two spellings different objects, where one is known */
|
|
235
|
+
compilerBehavior?: string;
|
|
236
|
+
offeredWhen: OfferedWhen;
|
|
237
|
+
/** what the trailing `-…` names. Present exactly when the registry entry takes a subject. */
|
|
238
|
+
subject?: { meaning: string; examples: readonly string[] };
|
|
239
|
+
/** a minimal pair: the spelling without the variation, then with it */
|
|
240
|
+
example: VariationExample;
|
|
241
|
+
/** the file holding the rewrite, relative to the repository root */
|
|
242
|
+
implementedIn: string;
|
|
243
|
+
seeAlso?: readonly VariationName[];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The callees the examples call. Unprototyped, so each takes whatever its call passes. */
|
|
247
|
+
const CALLS =
|
|
248
|
+
'void A(); void B(); void C(); void D(); void X(); void Y(); void P(); void Q(); s32 f(); s32 g(); s32 h(); void use();\n';
|
|
249
|
+
|
|
250
|
+
const RANK = 'packages/core/src/rank.ts';
|
|
251
|
+
const RANK_VARIATIONS = 'packages/core/src/rank-variations.ts';
|
|
252
|
+
const SYMBOLS = 'packages/core/src/symbols.ts';
|
|
253
|
+
const STRUCTURE = 'packages/core/src/structure/structure.ts';
|
|
254
|
+
const ANALYSIS = 'packages/core/src/structure/analysis.ts';
|
|
255
|
+
const BASECSE = 'packages/core/src/l3/basecse.ts';
|
|
256
|
+
const l3 = (file: string): string => `packages/core/src/l3/${file}.ts`;
|
|
257
|
+
|
|
258
|
+
/** A losing candidate is the control its winner was scored against: every entry below describes an
|
|
259
|
+
* alternative that rides BESIDE the spelling without it, never one that replaces it. */
|
|
260
|
+
export const VARIATION_DEFINITIONS: { readonly [N in VariationName]: VariationDefinition } = {
|
|
261
|
+
// ── signedness ──────────────────────────────────────────────────────────────────────────────
|
|
262
|
+
unsigned: {
|
|
263
|
+
title: 'Unsigned parameters',
|
|
264
|
+
summary: 'the scalar entry parameters are declared unsigned',
|
|
265
|
+
detail:
|
|
266
|
+
'A register holds bits, not a signedness, so the declaration of a parameter is not in the assembly. ' +
|
|
267
|
+
'Every scalar entry parameter whose type is still unknown or a 32-bit int is pinned `u32` before type ' +
|
|
268
|
+
'recovery. A recovered pointer or aggregate parameter is never pinned, and neither is one narrowed by ' +
|
|
269
|
+
'its own extension, which already states its signedness. On a function with nothing to pin every ' +
|
|
270
|
+
'candidate still carries `unsigned`, and it changed nothing.',
|
|
271
|
+
compilerBehavior:
|
|
272
|
+
'Signedness picks the instruction: an arithmetic or a logical right shift, a signed or an unsigned ' +
|
|
273
|
+
'branch after a compare, a sign or a zero extension of a narrower value.',
|
|
274
|
+
offeredWhen: 'always',
|
|
275
|
+
example: {
|
|
276
|
+
compiler: 'agbcc',
|
|
277
|
+
unit: '@ { return a0 >> a1; }',
|
|
278
|
+
before: 's32 example(s32 a0, s32 a1)',
|
|
279
|
+
after: 's32 example(u32 a0, u32 a1)',
|
|
280
|
+
},
|
|
281
|
+
implementedIn: RANK,
|
|
282
|
+
seeAlso: ['signed', 'uns-cmp'],
|
|
283
|
+
},
|
|
284
|
+
signed: {
|
|
285
|
+
title: 'Signed parameters',
|
|
286
|
+
summary: 'the scalar entry parameters are declared signed',
|
|
287
|
+
detail:
|
|
288
|
+
'The other signedness: every pinnable scalar entry parameter is declared `s32` before type recovery. ' +
|
|
289
|
+
'Every candidate carries exactly one of `unsigned` and `signed`, so the two counts of one fan add up ' +
|
|
290
|
+
'to its size.',
|
|
291
|
+
compilerBehavior: 'The same as `unsigned`: shifts, compare branches and extensions follow the declaration.',
|
|
292
|
+
offeredWhen: {
|
|
293
|
+
when: 'Some scalar entry parameter is left for the pin to declare.',
|
|
294
|
+
decidedBy: { symbol: 'NO_PIN_KINDS', file: RANK_VARIATIONS },
|
|
295
|
+
},
|
|
296
|
+
example: {
|
|
297
|
+
compiler: 'agbcc',
|
|
298
|
+
unit: '@ { return a0 >> a1; }',
|
|
299
|
+
before: 's32 example(u32 a0, u32 a1)',
|
|
300
|
+
after: 's32 example(s32 a0, s32 a1)',
|
|
301
|
+
},
|
|
302
|
+
implementedIn: RANK,
|
|
303
|
+
seeAlso: ['unsigned'],
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
// ── lift ────────────────────────────────────────────────────────────────────────────────────
|
|
307
|
+
'setup-args': {
|
|
308
|
+
title: 'Only the arguments the call set up',
|
|
309
|
+
summary: 'a call with a guessed arity passes only what its own block wrote',
|
|
310
|
+
detail:
|
|
311
|
+
'With no declared prototype, the arity of a call is guessed from the argument registers, and by ' +
|
|
312
|
+
'default a register whose value merely survives from an earlier block still counts as an argument. ' +
|
|
313
|
+
'This lift cuts each guessed call to the arguments the calling block itself set up. Dropping an ' +
|
|
314
|
+
'argument changes what every later stage sees, which is why it is a lift and not a rewrite of the ' +
|
|
315
|
+
'finished tree.',
|
|
316
|
+
compilerBehavior:
|
|
317
|
+
'agbcc leaves a value already in `r0` where it is and branches to the call, so `if (x) f(x);` and ' +
|
|
318
|
+
'`if (x) f();` usually compile alike. Under an equality guard the compiler proves the argument ' +
|
|
319
|
+
'constant and has to load it, and a missing load rules the wider reading out.',
|
|
320
|
+
offeredWhen: {
|
|
321
|
+
when: 'A call whose arity was guessed passes a register its own block did not set.',
|
|
322
|
+
decidedBy: { symbol: 'hasSetupArgsNarrowing', file: 'packages/core/src/frontend/ssa.ts' },
|
|
323
|
+
},
|
|
324
|
+
example: {
|
|
325
|
+
compiler: 'agbcc',
|
|
326
|
+
unit: CALLS + 'void example(s32 a0) { @ }',
|
|
327
|
+
before: 'if (a0 == 5) f(a0);',
|
|
328
|
+
after: 'if (a0 == 5) f();',
|
|
329
|
+
},
|
|
330
|
+
implementedIn: 'packages/core/src/frontend/ssa.ts',
|
|
331
|
+
},
|
|
332
|
+
connective: {
|
|
333
|
+
title: 'Comparison chain as `||`',
|
|
334
|
+
summary: 'a chain of constant tests on one value is spelled with `||` instead of as a switch',
|
|
335
|
+
detail:
|
|
336
|
+
'A chain of `x == K` tests on one value can be recovered as a `switch` or folded into a short-circuit ' +
|
|
337
|
+
'condition, and one reading of the assembly cannot do both: a folded `||` is no longer the comparison a switch is ' +
|
|
338
|
+
'recovered from. This lift lets the short-circuit fold take the chain. It also reaches functions whose ' +
|
|
339
|
+
'switch recovery declined entirely and came out as nested `if` statements.',
|
|
340
|
+
compilerBehavior:
|
|
341
|
+
'With one group of cases and a `default:` the two spellings are one object on agbcc. With a second ' +
|
|
342
|
+
'group they differ: the switch builds a balanced dispatch where the chain tests one value after another.',
|
|
343
|
+
offeredWhen: {
|
|
344
|
+
when: 'The short-circuit fold refused a chain only because it reads as a comparison tree.',
|
|
345
|
+
decidedBy: { symbol: 'runPreRecovery', file: 'packages/core/src/raise/pre-recovery.ts' },
|
|
346
|
+
gates: ['ARM_REREAD_GATES'],
|
|
347
|
+
},
|
|
348
|
+
example: {
|
|
349
|
+
compiler: 'agbcc',
|
|
350
|
+
unit: CALLS + 'void example(s32 x) { @ }',
|
|
351
|
+
before: 'switch (x) { case 0: case 2: A(); break; case 5: B(); break; }',
|
|
352
|
+
after: 'if (x == 0 || x == 2) { A(); } else if (x == 5) { B(); }',
|
|
353
|
+
},
|
|
354
|
+
implementedIn: 'packages/core/src/raise/shortcircuit.ts',
|
|
355
|
+
seeAlso: ['site-sense'],
|
|
356
|
+
},
|
|
357
|
+
'shared-ret': {
|
|
358
|
+
title: 'Shared code after an early return',
|
|
359
|
+
summary: 'code both arms of an `if` reach before returning is written once, after the `if`',
|
|
360
|
+
detail:
|
|
361
|
+
'An `if` whose arms never meet again before the function returns has no follow, so by default a ' +
|
|
362
|
+
'region both arms reach is written in each of them. This structures the same raised function again ' +
|
|
363
|
+
"with that shared region as the `if`'s follow: it is written once after the `if`, and every other " +
|
|
364
|
+
'path leaves through an early `return`.',
|
|
365
|
+
offeredWhen: {
|
|
366
|
+
when: 'Some `if` has arms that reach a common `return` block and no common block before it.',
|
|
367
|
+
decidedBy: { symbol: 'hasDivergentSharedRet', file: STRUCTURE },
|
|
368
|
+
},
|
|
369
|
+
example: {
|
|
370
|
+
compiler: 'agbcc',
|
|
371
|
+
unit: CALLS + 'void example(s32 c, s32 x) { @ }',
|
|
372
|
+
before: 'if (c) { A(); B(); } else { if (x) { C(); return; } B(); }',
|
|
373
|
+
after: 'if (c) { A(); } else { if (x) { C(); return; } } B();',
|
|
374
|
+
},
|
|
375
|
+
implementedIn: STRUCTURE,
|
|
376
|
+
seeAlso: ['shared-tail'],
|
|
377
|
+
},
|
|
378
|
+
'shared-tail': {
|
|
379
|
+
title: 'Store tail copied back',
|
|
380
|
+
summary: 'a store the compiler merged into one tail is copied back into each path, then shared',
|
|
381
|
+
detail:
|
|
382
|
+
"agbcc's global common-subexpression pass can move a trailing store into every predecessor of a join, " +
|
|
383
|
+
'and the paths then jump into one `store; return` tail. This lift copies that tail back into each ' +
|
|
384
|
+
'path that branches to it, and then structures with the shared follow `shared-ret` uses, so the ' +
|
|
385
|
+
'store the source wrote once is written after the `if` and the others before an early `return`. It is ' +
|
|
386
|
+
'enumerated apart from `shared-ret` because the sink can delete the shared `return` that variation needs.',
|
|
387
|
+
compilerBehavior:
|
|
388
|
+
'Two sources that lift to the same code (one with the store written once, one with it in each arm) ' +
|
|
389
|
+
'compile to different register assignments, so only the differ can tell which one it was.',
|
|
390
|
+
offeredWhen: {
|
|
391
|
+
when: 'The sink changed the function and some `if` of the result still shares a `return`.',
|
|
392
|
+
decidedBy: { symbol: 'sinkStoreTails', file: 'packages/core/src/raise/tailsink.ts' },
|
|
393
|
+
},
|
|
394
|
+
example: {
|
|
395
|
+
compiler: 'agbcc',
|
|
396
|
+
unit:
|
|
397
|
+
CALLS +
|
|
398
|
+
'void fnA(void); void fnB(void); struct Q { void (*cur)(void); }; extern struct Q gQ; void example(s32 c, s32 x) { void (*v)(void); @ }',
|
|
399
|
+
before: 'if (c) { A(); v = fnB; } else if (x) { v = fnA; } else { v = fnB; } gQ.cur = v;',
|
|
400
|
+
after: 'if (c) { A(); } else if (x) { gQ.cur = fnA; return; } gQ.cur = fnB;',
|
|
401
|
+
note: 'the shape of `synthetic:gcsetail`',
|
|
402
|
+
},
|
|
403
|
+
implementedIn: 'packages/core/src/raise/tailsink.ts',
|
|
404
|
+
seeAlso: ['shared-ret'],
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
// ── structure ───────────────────────────────────────────────────────────────────────────────
|
|
408
|
+
'flip-branch': {
|
|
409
|
+
title: 'Flipped branch sense',
|
|
410
|
+
summary: 'an `if` whose arms never rejoin is spelled with the opposite condition',
|
|
411
|
+
detail:
|
|
412
|
+
'An `if` can be written `if (c) A else B` or `if (!c) B else A`, and the assembly does not say which ' +
|
|
413
|
+
'one the source used. By default a divergent `if` (one whose arms do not meet again) follows the ' +
|
|
414
|
+
'layout: the arm the branch falls into is `then`. This spells every divergent `if` of the function the ' +
|
|
415
|
+
"other way. The name is relative to the target's default sense.",
|
|
416
|
+
compilerBehavior:
|
|
417
|
+
'A compiler that keeps source order lays the `then` arm out first, so the two spellings are two ' +
|
|
418
|
+
'layouts and two objects.',
|
|
419
|
+
offeredWhen: 'always',
|
|
420
|
+
example: {
|
|
421
|
+
compiler: 'agbcc',
|
|
422
|
+
unit: CALLS + 'void example(s32 c) { @ }',
|
|
423
|
+
before: 'if (c) { A(); return; } B();',
|
|
424
|
+
after: 'if (!c) { B(); return; } A();',
|
|
425
|
+
},
|
|
426
|
+
implementedIn: STRUCTURE,
|
|
427
|
+
seeAlso: ['flip-join', 'site-sense', 'sense'],
|
|
428
|
+
},
|
|
429
|
+
defsite: {
|
|
430
|
+
title: 'Constant written where defined',
|
|
431
|
+
summary: "a merge's constant is written where the assembly loaded it, not on the path into the merge",
|
|
432
|
+
detail:
|
|
433
|
+
'When one path into a merge carries a constant, the structurer normally writes it on that edge, as an ' +
|
|
434
|
+
'`else` arm. The assembly shows where the constant was loaded, and this writes it there instead: ' +
|
|
435
|
+
'above the `if`, with the other path overwriting it. Where the source wrote it is still open, so both ' +
|
|
436
|
+
'placements are enumerated, crossed with branch sense, because emptying an arm changes which sense ' +
|
|
437
|
+
'matches.',
|
|
438
|
+
offeredWhen: 'always',
|
|
439
|
+
example: {
|
|
440
|
+
compiler: 'agbcc',
|
|
441
|
+
unit: CALLS + 's32 example(s32 c) { s32 v; @ return v; }',
|
|
442
|
+
before: 'if (c) { v = f(); } else { v = 0; }',
|
|
443
|
+
after: 'v = 0; if (c) { v = f(); }',
|
|
444
|
+
},
|
|
445
|
+
implementedIn: STRUCTURE,
|
|
446
|
+
seeAlso: ['loop-entry', 'fresh-merge'],
|
|
447
|
+
},
|
|
448
|
+
'loop-entry': {
|
|
449
|
+
title: "Loop's entry constant above its guard",
|
|
450
|
+
summary: "a loop's starting constant is written above the guard, not on the edge into the loop",
|
|
451
|
+
detail:
|
|
452
|
+
"The same placement question as `defsite`, asked of a loop header's entry constant. It is a second " +
|
|
453
|
+
'decision rather than a wider `defsite`: a function holding both kinds of constant has three ' +
|
|
454
|
+
'spellings, and one switch for both would make the middle one unreachable. The two are enumerated as ' +
|
|
455
|
+
'a chain (neither, `defsite`, then `defsite/loop-entry`), so this never appears without `defsite`.',
|
|
456
|
+
offeredWhen: 'always',
|
|
457
|
+
example: {
|
|
458
|
+
compiler: 'agbcc',
|
|
459
|
+
unit: CALLS + 'void example(s32 n) { s32 i = 0; s32 s; @ use(s); }',
|
|
460
|
+
before: 'if (0 < n) { s = 0; do { s += i; i++; } while (i < n); }',
|
|
461
|
+
after: 's = 0; if (0 < n) { do { s += i; i++; } while (i < n); }',
|
|
462
|
+
},
|
|
463
|
+
implementedIn: STRUCTURE,
|
|
464
|
+
seeAlso: ['defsite', 'initfirst'],
|
|
465
|
+
},
|
|
466
|
+
'flip-join': {
|
|
467
|
+
title: 'Flipped sense of a rejoining `if`',
|
|
468
|
+
summary: 'a two-armed `if` whose arms rejoin is spelled with the opposite condition',
|
|
469
|
+
detail:
|
|
470
|
+
'The sibling of `flip-branch` for an `if` whose arms meet again. Its default reads the same layout ' +
|
|
471
|
+
'evidence, and three things invert it, each per `if` where this variation is per function: a ' +
|
|
472
|
+
"short-circuit fold choosing the orientation, a branch relayed past Thumb's branch range, and a " +
|
|
473
|
+
"loop's zero-trip guard, an `if` no source wrote. The name is relative to the target's default sense.",
|
|
474
|
+
compilerBehavior: 'agbcc emits different bytes for the arms-swapped spelling wherever such an `if` exists.',
|
|
475
|
+
offeredWhen: 'always',
|
|
476
|
+
example: {
|
|
477
|
+
compiler: 'agbcc',
|
|
478
|
+
unit: CALLS + 'void example(s32 c) { @ }',
|
|
479
|
+
before: 'if (c) { A(); } else { B(); } D();',
|
|
480
|
+
after: 'if (!c) { B(); } else { A(); } D();',
|
|
481
|
+
},
|
|
482
|
+
implementedIn: STRUCTURE,
|
|
483
|
+
seeAlso: ['flip-branch', 'site-sense', 'sense'],
|
|
484
|
+
},
|
|
485
|
+
sense: {
|
|
486
|
+
title: 'Per-site branch sense (measurement)',
|
|
487
|
+
summary: 'chosen branch-sense sites are spelled the other way, one bit per site',
|
|
488
|
+
detail:
|
|
489
|
+
'`flip-branch` and `flip-join` flip every `if` of a function at once, which cannot spell a function ' +
|
|
490
|
+
'whose `if` statements were written in opposite senses. This measurement crosses the whole fan with every mask ' +
|
|
491
|
+
'over the first sites, which costs a factor of two per site, to price that gap and to learn whether ' +
|
|
492
|
+
"a target's mix is reachable at all. The CLI asks for it through `ASMLIFT_PERSITE_SENSE`.",
|
|
493
|
+
offeredWhen: {
|
|
494
|
+
when: 'Only when the caller asks for per-site sense bits; no default fan carries it.',
|
|
495
|
+
decidedBy: { symbol: 'enumerateCandidates', file: RANK },
|
|
496
|
+
},
|
|
497
|
+
subject: {
|
|
498
|
+
meaning:
|
|
499
|
+
'A decimal bitmask over the branch-sense sites in the order structuring first visits them: bit i set ' +
|
|
500
|
+
'spells site i the other way. `sense-5` flips sites 0 and 2.',
|
|
501
|
+
examples: ['sense-1', 'sense-5'],
|
|
502
|
+
},
|
|
503
|
+
example: {
|
|
504
|
+
compiler: 'agbcc',
|
|
505
|
+
unit: CALLS + 'void example(s32 a, s32 b) { @ }',
|
|
506
|
+
before: 'if (a) { X(); } else { Y(); } if (b) { P(); } else { Q(); }',
|
|
507
|
+
after: 'if (!a) { Y(); } else { X(); } if (b) { P(); } else { Q(); }',
|
|
508
|
+
note: 'as the variation `sense-1` spells it',
|
|
509
|
+
},
|
|
510
|
+
implementedIn: STRUCTURE,
|
|
511
|
+
seeAlso: ['flip-branch', 'flip-join', 'site-sense'],
|
|
512
|
+
},
|
|
513
|
+
'no-bitfield': {
|
|
514
|
+
title: 'Shifts instead of a bitfield member',
|
|
515
|
+
summary: 'a bitfield read keeps its shift arithmetic instead of the name the symbol map gives it',
|
|
516
|
+
detail:
|
|
517
|
+
'With a symbol map that declares bitfield members, a `(x << a) >> b` extract of a struct global is ' +
|
|
518
|
+
'spelled as the named member. This keeps the shifts over the loaded word.',
|
|
519
|
+
compilerBehavior:
|
|
520
|
+
"A named bitfield read compiles at the declaration's access width. Where that differs from the width " +
|
|
521
|
+
'the assembly loaded, only the shift spelling reproduces the load.',
|
|
522
|
+
offeredWhen: {
|
|
523
|
+
when: 'The symbol map declares a bitfield member, and only on the candidates that use the map.',
|
|
524
|
+
decidedBy: { symbol: 'declaresBitfields', file: SYMBOLS },
|
|
525
|
+
},
|
|
526
|
+
example: {
|
|
527
|
+
compiler: 'agbcc',
|
|
528
|
+
unit: 'struct Packed { u8 hearts : 2; u8 stars : 3; u16 dreamStones : 7; u32 unk4; }; extern struct Packed gPacked; u32 example(void) { @ }',
|
|
529
|
+
before: 'return gPacked.dreamStones;',
|
|
530
|
+
after: 'return (*(u32 *)&gPacked << 20) >> 25;',
|
|
531
|
+
note: '`synthetic:bfwordread`',
|
|
532
|
+
},
|
|
533
|
+
implementedIn: STRUCTURE,
|
|
534
|
+
seeAlso: ['no-ptr-elem', 'raw-globals'],
|
|
535
|
+
},
|
|
536
|
+
'no-ptr-elem': {
|
|
537
|
+
title: 'Byte arithmetic through a pointer member',
|
|
538
|
+
summary: 'an element read through a pointer member keeps its byte arithmetic',
|
|
539
|
+
detail:
|
|
540
|
+
'With a symbol map that declares a sized pointer member, an element-scaled offset through it is ' +
|
|
541
|
+
'spelled as a whole-element subscript. This keeps the byte arithmetic it replaces. The two are the ' +
|
|
542
|
+
'same address.',
|
|
543
|
+
compilerBehavior:
|
|
544
|
+
'Compiled on agbcc the two are the same instruction count and different objects: they differ in ' +
|
|
545
|
+
'which register the `add` targets.',
|
|
546
|
+
offeredWhen: {
|
|
547
|
+
when: 'The map declares a pointer member with a 1, 2 or 4-byte pointee on a global the function names.',
|
|
548
|
+
decidedBy: { symbol: 'isPtrField', file: SYMBOLS },
|
|
549
|
+
},
|
|
550
|
+
example: {
|
|
551
|
+
compiler: 'agbcc',
|
|
552
|
+
unit: 'struct BgPtrs { u16 *pMap; }; extern struct BgPtrs gBgPtrs; u16 example(s32 i) { @ }',
|
|
553
|
+
before: 'return gBgPtrs.pMap[i + 157];',
|
|
554
|
+
after: 'return *(u16 *)((i << 1) + (u8 *)gBgPtrs.pMap + 314);',
|
|
555
|
+
note: '`synthetic:ptrelem`',
|
|
556
|
+
},
|
|
557
|
+
implementedIn: STRUCTURE,
|
|
558
|
+
seeAlso: ['no-bitfield', 'flat-rank'],
|
|
559
|
+
},
|
|
560
|
+
'flat-rank': {
|
|
561
|
+
title: 'Flat arithmetic instead of subscripts',
|
|
562
|
+
summary: 'a multidimensional array access is spelled as flat arithmetic, not the declared subscripts',
|
|
563
|
+
detail:
|
|
564
|
+
'By default an access to a multidimensional array global recovers the declared subscripts from a ' +
|
|
565
|
+
'term at the row stride. That term is evidence the access crosses rows, not evidence about which of ' +
|
|
566
|
+
'the two spellings produced it.',
|
|
567
|
+
compilerBehavior:
|
|
568
|
+
'Under agbcc, KMC gcc and mwcc the two differ only in where the pool load sits; under IDO they are ' +
|
|
569
|
+
'byte-identical.',
|
|
570
|
+
offeredWhen: {
|
|
571
|
+
when: 'The function names a global that the symbol map, or its own index strides, declare a multidimensional array.',
|
|
572
|
+
decidedBy: { symbol: 'arrayInnerExtents', file: SYMBOLS },
|
|
573
|
+
},
|
|
574
|
+
example: {
|
|
575
|
+
compiler: 'agbcc',
|
|
576
|
+
unit: 'extern u16 gTbl[4][0x400]; s32 example(s32 r, s32 i) { s32 x; @ return x; }',
|
|
577
|
+
before: 'x = gTbl[r][i];',
|
|
578
|
+
after: 'x = *(u16 *)((r << 11) + (i << 1) + (u32)&gTbl);',
|
|
579
|
+
note: 'for `u16 gTbl[4][0x400]`',
|
|
580
|
+
},
|
|
581
|
+
implementedIn: STRUCTURE,
|
|
582
|
+
seeAlso: ['no-ptr-elem'],
|
|
583
|
+
},
|
|
584
|
+
'reread-globals': {
|
|
585
|
+
title: 'Global read again at each use',
|
|
586
|
+
summary: 'a global is read at each use instead of once into a variable',
|
|
587
|
+
detail:
|
|
588
|
+
'By default a read of a named global is cached in a local before a later store. This lets the read ' +
|
|
589
|
+
'render at its use across stores that provably cannot reach it, such as a store to a different ' +
|
|
590
|
+
'named global. Whether the source read it once or at each use is not in the assembly.',
|
|
591
|
+
compilerBehavior:
|
|
592
|
+
'The compiler folds the repeated reads back into one load, and agbcc has been measured landing on ' +
|
|
593
|
+
'both sides inside a single function.',
|
|
594
|
+
offeredWhen: {
|
|
595
|
+
when: 'Some load resolves to a named global.',
|
|
596
|
+
decidedBy: { symbol: 'globalCellOf', file: 'packages/core/src/ir/alias.ts' },
|
|
597
|
+
},
|
|
598
|
+
example: {
|
|
599
|
+
compiler: 'agbcc',
|
|
600
|
+
unit: CALLS + 'extern s32 gCount; extern s32 gFlag; void example(void) { @ }',
|
|
601
|
+
before: 's32 v = gCount; gFlag = 0; f(v);',
|
|
602
|
+
after: 'gFlag = 0; f(gCount);',
|
|
603
|
+
},
|
|
604
|
+
implementedIn: ANALYSIS,
|
|
605
|
+
seeAlso: ['raw-globals', 'derived-home'],
|
|
606
|
+
},
|
|
607
|
+
inplace: {
|
|
608
|
+
title: 'Overwrite in place',
|
|
609
|
+
summary: 'a loaded value feeding a merge is named, so one arm becomes an in-place overwrite',
|
|
610
|
+
detail:
|
|
611
|
+
'A load that feeds a conditional merge is given its own variable, the merge takes that name, and the ' +
|
|
612
|
+
'arm that only passes the value through disappears.',
|
|
613
|
+
compilerBehavior:
|
|
614
|
+
'The two-sided form needs a second register, at the margin a callee-saved push, and the emptied arm ' +
|
|
615
|
+
'flips the branch sense.',
|
|
616
|
+
offeredWhen: {
|
|
617
|
+
when: "A load feeds an argument of a conditional branch's merge.",
|
|
618
|
+
decidedBy: { symbol: 'STRUCTURE_VARIATIONS', file: RANK_VARIATIONS },
|
|
619
|
+
},
|
|
620
|
+
example: {
|
|
621
|
+
compiler: 'mwcc',
|
|
622
|
+
unit: 's32 example(u8 *p) { s32 t; s32 v; @ return v; }',
|
|
623
|
+
before: 't = *p; if (t > 31) { v = 32; } else { v = t; }',
|
|
624
|
+
after: 'v = *p; if (v > 31) v = 32;',
|
|
625
|
+
},
|
|
626
|
+
implementedIn: ANALYSIS,
|
|
627
|
+
seeAlso: ['merge-names', 'fresh-merge'],
|
|
628
|
+
},
|
|
629
|
+
'merge-names': {
|
|
630
|
+
title: 'One name across a copy',
|
|
631
|
+
summary: 'the two sides of a merge copy share one variable when their values never overlap',
|
|
632
|
+
detail:
|
|
633
|
+
'Two variables a merge copy would join become one where the values under them never interfere. ' +
|
|
634
|
+
'Whether the source had one variable there is not in the naming, and removing a copy is worth less ' +
|
|
635
|
+
'than it looks, because the compiler coalesces most copies itself. What moves the score is which ' +
|
|
636
|
+
'values share a register.',
|
|
637
|
+
offeredWhen: {
|
|
638
|
+
when: 'Some merge is fed by two or more edges.',
|
|
639
|
+
decidedBy: { symbol: 'STRUCTURE_VARIATIONS', file: RANK_VARIATIONS },
|
|
640
|
+
gates: ['NAME_COALESCE_GATES'],
|
|
641
|
+
},
|
|
642
|
+
example: {
|
|
643
|
+
compiler: 'agbcc',
|
|
644
|
+
unit: CALLS + 's32 example(s32 n) { s32 v1; s32 v2; s32 i; @ }',
|
|
645
|
+
before: 'v1 = f(); for (i = 0; i < n; i++) { v2 = v1; if (g(i)) v2 = 0; v1 = v2 + i; } return v1;',
|
|
646
|
+
after: 'v1 = f(); for (i = 0; i < n; i++) { if (g(i)) v1 = 0; v1 = v1 + i; } return v1;',
|
|
647
|
+
},
|
|
648
|
+
implementedIn: 'packages/core/src/structure/namecoalesce.ts',
|
|
649
|
+
seeAlso: ['coalesce', 'inplace'],
|
|
650
|
+
},
|
|
651
|
+
'addr-home': {
|
|
652
|
+
title: 'Pointer local for a computed address',
|
|
653
|
+
summary: 'a computed address used at two or more sites is held in a pointer local',
|
|
654
|
+
detail:
|
|
655
|
+
'A pure computed address dereferenced at two or more sites, and the loads through it, get locals: ' +
|
|
656
|
+
"the source's pointer local and scalar temporary. By default the address is derived again at each use.",
|
|
657
|
+
offeredWhen: {
|
|
658
|
+
when: "A symbol-map setting's own lift dereferences one computed address at two or more sites.",
|
|
659
|
+
decidedBy: { symbol: 'hasHomeableSharedAddress', file: ANALYSIS },
|
|
660
|
+
},
|
|
661
|
+
example: {
|
|
662
|
+
compiler: 'agbcc',
|
|
663
|
+
unit: CALLS + 's32 example(u32 a0, u32 a1) { @ }',
|
|
664
|
+
before:
|
|
665
|
+
'if (((u8 *)((a0 << 2) + a1 + 0x8057acc))[1] == 2) return g(((u8 *)((a0 << 2) + a1 + 0x8057acc))[0]); return 0;',
|
|
666
|
+
after: 'u8 *p = (u8 *)((a0 << 2) + a1 + 0x8057acc); if (p[1] == 2) return g(p[0]); return 0;',
|
|
667
|
+
},
|
|
668
|
+
implementedIn: ANALYSIS,
|
|
669
|
+
seeAlso: ['expr-home', 'derived-home', 'livebase'],
|
|
670
|
+
},
|
|
671
|
+
'expr-home': {
|
|
672
|
+
title: 'Named value used inside a loop',
|
|
673
|
+
summary: 'a value computed before a loop and used inside it is held in a local',
|
|
674
|
+
detail:
|
|
675
|
+
'A pure value defined outside a loop, with two or more consumers of which at least one is inside it, ' +
|
|
676
|
+
'gets a local of its recovered type: the register the compiler holds across the iterations. By ' +
|
|
677
|
+
'default it is derived again at each use.',
|
|
678
|
+
offeredWhen: {
|
|
679
|
+
when: "A symbol-map setting's own lift uses a value from before a loop two or more times, once inside it.",
|
|
680
|
+
decidedBy: { symbol: 'hasLoopSharedPureValue', file: ANALYSIS },
|
|
681
|
+
},
|
|
682
|
+
example: {
|
|
683
|
+
compiler: 'agbcc',
|
|
684
|
+
unit: CALLS + 'void example(s32 t) { s32 i; @ }',
|
|
685
|
+
before: 'for (i = 0; i < (16 << t); i++) g(i * (16 << t));',
|
|
686
|
+
after: 's32 size = 16 << t; for (i = 0; i < size; i++) g(i * size);',
|
|
687
|
+
},
|
|
688
|
+
implementedIn: ANALYSIS,
|
|
689
|
+
seeAlso: ['addr-home', 'derived-home'],
|
|
690
|
+
},
|
|
691
|
+
'derived-home': {
|
|
692
|
+
title: 'Named value derived from a read',
|
|
693
|
+
summary: 'a value computed from a memory read is held in a local, instead of the read',
|
|
694
|
+
detail:
|
|
695
|
+
'A pure value with two or more consumers, standing on a memory read, gets a local, and the read then ' +
|
|
696
|
+
'renders once inside it. By default the read gets the local and the computation is repeated at each ' +
|
|
697
|
+
'use.',
|
|
698
|
+
compilerBehavior: 'Both compile, and agbcc folds the repeated computation back, so only the score separates them.',
|
|
699
|
+
offeredWhen: {
|
|
700
|
+
when: "A symbol-map setting's own lift has a value computed from a memory read with two or more consumers.",
|
|
701
|
+
decidedBy: { symbol: 'hasDerivedReadHome', file: ANALYSIS },
|
|
702
|
+
},
|
|
703
|
+
example: {
|
|
704
|
+
compiler: 'agbcc',
|
|
705
|
+
unit: CALLS + '#define REG_KEYINPUT (*(u16 *)0x4000130)\nvoid example(void) { @ }',
|
|
706
|
+
before: 'u16 k = REG_KEYINPUT; f(0x3FF ^ k); g(0x3FF ^ k);',
|
|
707
|
+
after: 'u16 k = 0x3FF ^ REG_KEYINPUT; f(k); g(k);',
|
|
708
|
+
},
|
|
709
|
+
implementedIn: ANALYSIS,
|
|
710
|
+
seeAlso: ['expr-home', 'reread-globals'],
|
|
711
|
+
},
|
|
712
|
+
'merge-home': {
|
|
713
|
+
title: 'Value computed once above a branch',
|
|
714
|
+
summary: 'a value several paths hand to one merge is computed once, above the branch',
|
|
715
|
+
detail:
|
|
716
|
+
'A pure value that the paths into one merge hand to the same variable from two or more places gets a ' +
|
|
717
|
+
'local in the block above them, the value the source computed once before branching. By default ' +
|
|
718
|
+
'there is no name to refer to on a path, and each arm computes it again.',
|
|
719
|
+
offeredWhen: {
|
|
720
|
+
when: "A symbol-map setting's own lift hands one merge the same value from two or more places.",
|
|
721
|
+
decidedBy: { symbol: 'hasMergeFeedHome', file: ANALYSIS },
|
|
722
|
+
},
|
|
723
|
+
example: {
|
|
724
|
+
compiler: 'agbcc',
|
|
725
|
+
unit: CALLS + 's32 example(s32 c, s32 a0) { s32 v; @ return v; }',
|
|
726
|
+
before: 'if (c) { v = a0 << 2; A(); } else { v = a0 << 2; B(); }',
|
|
727
|
+
after: 's32 m = a0 << 2; if (c) { v = m; A(); } else { v = m; B(); }',
|
|
728
|
+
},
|
|
729
|
+
implementedIn: ANALYSIS,
|
|
730
|
+
seeAlso: ['expr-home', 'fresh-merge'],
|
|
731
|
+
},
|
|
732
|
+
'uns-cmp': {
|
|
733
|
+
title: 'Unsigned compare spelled unsigned',
|
|
734
|
+
summary: 'an unsigned compare carries a `(u32)` cast where its operands do not already say so',
|
|
735
|
+
detail:
|
|
736
|
+
'An unsigned comparison takes a `(u32)` cast on an operand the rendering does not already make ' +
|
|
737
|
+
'unsigned, and a declaration claimed by both kinds of use becomes `u32` when nothing under it needs ' +
|
|
738
|
+
'signed. Which the source wrote is open in one direction: a signed spelling that matched was proved ' +
|
|
739
|
+
'non-negative by the compiler, and asmlift can prove less than the compiler can.',
|
|
740
|
+
compilerBehavior:
|
|
741
|
+
'A compiler emits an unsigned branch from a signed compare only where it proved both sides non-negative.',
|
|
742
|
+
offeredWhen: {
|
|
743
|
+
when: 'The function has an unsigned comparison.',
|
|
744
|
+
decidedBy: { symbol: 'STRUCTURE_VARIATIONS', file: RANK_VARIATIONS },
|
|
745
|
+
},
|
|
746
|
+
example: {
|
|
747
|
+
compiler: 'agbcc',
|
|
748
|
+
unit: CALLS + 'void example(s32 a, s32 b) { @ A(); }',
|
|
749
|
+
before: 'if (a < b)',
|
|
750
|
+
after: 'if ((u32)a < b)',
|
|
751
|
+
},
|
|
752
|
+
implementedIn: STRUCTURE,
|
|
753
|
+
seeAlso: ['unsigned', 'signed'],
|
|
754
|
+
},
|
|
755
|
+
'fresh-merge': {
|
|
756
|
+
title: 'Own local for a merge into a parameter',
|
|
757
|
+
summary: 'a merge that would assign back into a parameter gets its own local',
|
|
758
|
+
detail:
|
|
759
|
+
'A merge whose value lives in a parameter takes its own local where the default assigns back into the ' +
|
|
760
|
+
'parameter. Both are ordinary C over the same values. A merge with its own local also lets `defsite` ' +
|
|
761
|
+
"write a constant above the branch, which a merge that adopted the parameter's name refuses.",
|
|
762
|
+
compilerBehavior:
|
|
763
|
+
'On mwcc a clamp written into the parameter and the same clamp through its own local are different ' +
|
|
764
|
+
'objects. agbcc compiles that pair alike, and a two-argument max compiles alike on both.',
|
|
765
|
+
offeredWhen: {
|
|
766
|
+
when: 'Some merge is fed a parameter on one edge and a different value on another.',
|
|
767
|
+
decidedBy: { symbol: 'hasParamRootedMerge', file: STRUCTURE },
|
|
768
|
+
gates: ['FRESH_MERGE_GATES'],
|
|
769
|
+
},
|
|
770
|
+
example: {
|
|
771
|
+
compiler: 'mwcc',
|
|
772
|
+
unit: 'u8 example(s32 a0) { s32 v0; @ }',
|
|
773
|
+
before: 'if (a0 > 255) a0 = 255; return a0;',
|
|
774
|
+
after: 'if (a0 > 255) { v0 = 255; } else { v0 = a0; } return v0;',
|
|
775
|
+
},
|
|
776
|
+
implementedIn: STRUCTURE,
|
|
777
|
+
seeAlso: ['defsite', 'merge-home'],
|
|
778
|
+
},
|
|
779
|
+
'copy-defpos': {
|
|
780
|
+
title: 'Copies in the order values were defined',
|
|
781
|
+
summary: "a path's copies into a merge are ordered by where each value was defined",
|
|
782
|
+
detail:
|
|
783
|
+
'On a path into a merge, the copies are laid out by default in the order the assembly wrote their ' +
|
|
784
|
+
'destinations. That order is only forced for a cyclic set of copies (a swap), where the spill has to ' +
|
|
785
|
+
'be the register overwritten first. For an acyclic set this orders the copies by where each value was ' +
|
|
786
|
+
'defined instead; a cyclic set keeps the recorded order either way.',
|
|
787
|
+
compilerBehavior:
|
|
788
|
+
'The benchmark answers it both ways inside one compiler: some mwcc rows match only with the record, others score better without it.',
|
|
789
|
+
offeredWhen: {
|
|
790
|
+
when: "The two orders differ somewhere in a symbol-map setting's own lift.",
|
|
791
|
+
decidedBy: { symbol: 'edgeCopyOrdersDiffer', file: STRUCTURE },
|
|
792
|
+
},
|
|
793
|
+
example: {
|
|
794
|
+
compiler: 'agbcc',
|
|
795
|
+
unit:
|
|
796
|
+
CALLS +
|
|
797
|
+
's32 example(s32 a0, s32 a1, s32 c) { s32 v1; s32 v2; if (c) { v1 = f(); v2 = g(); } else { @ } return h(v1, v2); }',
|
|
798
|
+
before: 'v2 = a1; v1 = a0;',
|
|
799
|
+
after: 'v1 = a0; v2 = a1;',
|
|
800
|
+
note: 'the copies at the end of one path into a merge',
|
|
801
|
+
},
|
|
802
|
+
implementedIn: STRUCTURE,
|
|
803
|
+
},
|
|
804
|
+
'site-sense': {
|
|
805
|
+
title: 'Branch sense read per condition',
|
|
806
|
+
summary: "each folded `&&`/`||` condition takes its sense from the fold's own evidence",
|
|
807
|
+
detail:
|
|
808
|
+
'A short-circuit fold records, for each condition it built, whether the arm both tests reach was ' +
|
|
809
|
+
'fallen into, which successor it landed in, and whether a long-branch relay sat on an edge. This ' +
|
|
810
|
+
'spells each such `if` from that record instead of from the per-function sense, so one function can ' +
|
|
811
|
+
'mix senses.',
|
|
812
|
+
compilerBehavior:
|
|
813
|
+
"The reading rests on gcc laying a condition's arms out in source order. A long branch breaks it " +
|
|
814
|
+
'(gcc inverts the last test and lays the `else` arm first), which is why a relayed edge is never read.',
|
|
815
|
+
offeredWhen: {
|
|
816
|
+
when: "This lift's raised function carries a short-circuit fold's orientation record.",
|
|
817
|
+
decidedBy: { symbol: 'STRUCTURE_VARIATIONS', file: RANK_VARIATIONS },
|
|
818
|
+
},
|
|
819
|
+
example: {
|
|
820
|
+
compiler: 'agbcc',
|
|
821
|
+
unit: CALLS + 'void example(s32 a, s32 b) { @ }',
|
|
822
|
+
before: 'if (a != 0 && b != 0) { X(); } else { Y(); }',
|
|
823
|
+
after: 'if (a == 0 || b == 0) { Y(); } else { X(); }',
|
|
824
|
+
note: 'at a site whose record puts the shared arm in the taken slot',
|
|
825
|
+
},
|
|
826
|
+
implementedIn: STRUCTURE,
|
|
827
|
+
seeAlso: ['flip-join', 'flip-branch', 'sense', 'connective'],
|
|
828
|
+
},
|
|
829
|
+
|
|
830
|
+
// ── respell ─────────────────────────────────────────────────────────────────────────────────
|
|
831
|
+
unmerge: {
|
|
832
|
+
title: 'Merged statement copied back into its arms',
|
|
833
|
+
summary: 'a statement the compiler merged after an `if` is written back inside each arm',
|
|
834
|
+
detail:
|
|
835
|
+
'agbcc merges a store the source wrote in both arms into one copy at the join (cross-jumping), so the lifted tree carries its ' +
|
|
836
|
+
"address and value on temporaries. This substitutes each arm's own definitions and writes the whole " +
|
|
837
|
+
'statement back into each arm. Unlike the other respell variations it is applied before the rest: ' +
|
|
838
|
+
"the whole respell set then runs again on its output, which is how an arm's own base pointer can " +
|
|
839
|
+
'hold the copied store. It rewrites every eligible `if` at once.',
|
|
840
|
+
compilerBehavior:
|
|
841
|
+
'Sometimes the merged and the per-arm spellings are one object; where only the value merges, the ' +
|
|
842
|
+
'per-arm spelling keeps two literal pools and a branch to the join.',
|
|
843
|
+
offeredWhen: {
|
|
844
|
+
judges: 'each `if` followed by a statement that reads what its arms define',
|
|
845
|
+
gates: [
|
|
846
|
+
'UNMERGE_SITE_GATES',
|
|
847
|
+
'UNMERGE_ARM_GATES',
|
|
848
|
+
'UNMERGE_VALUE_GATES',
|
|
849
|
+
'UNMERGE_RUNG_GATES',
|
|
850
|
+
'UNMERGE_TOTALITY_GATES',
|
|
851
|
+
],
|
|
852
|
+
},
|
|
853
|
+
example: {
|
|
854
|
+
compiler: 'agbcc',
|
|
855
|
+
unit: 'void example(s32 c, u16 a, u16 b) { u16 *v16; u16 v17; @ }',
|
|
856
|
+
before: 'if (c) { v16 = (u16 *)0x3001000; v17 = a; } else { v16 = (u16 *)0x3001008; v17 = b; } *v16 = v17;',
|
|
857
|
+
after: 'if (c) { *(u16 *)0x3001000 = a; } else { *(u16 *)0x3001008 = b; }',
|
|
858
|
+
},
|
|
859
|
+
implementedIn: l3('unmerge'),
|
|
860
|
+
seeAlso: ['initfirst', 'regionbase'],
|
|
861
|
+
},
|
|
862
|
+
argbase: {
|
|
863
|
+
title: 'Argument bases named before the call',
|
|
864
|
+
summary: "the fixed addresses a call's arguments read are loaded into pointer locals first",
|
|
865
|
+
detail:
|
|
866
|
+
"When two or more of a call's arguments each read through a different fixed address, this names " +
|
|
867
|
+
"those addresses in locals before the call. Only a pure base (a global's address, a numeric address " +
|
|
868
|
+
'or a declared global) moves, so loading it earlier cannot be observed.',
|
|
869
|
+
compilerBehavior:
|
|
870
|
+
'Inline, agbcc finishes one argument before starting the next (`ldr; ldrb; ldr; ldrb`); with named ' +
|
|
871
|
+
'bases it loads both addresses first (`ldr; ldr; ldrb; ldrb`).',
|
|
872
|
+
offeredWhen: {
|
|
873
|
+
when: 'Two or more arguments of one call read through distinct pure bases.',
|
|
874
|
+
decidedBy: { symbol: 'materializeArgBases', file: l3('argbase') },
|
|
875
|
+
},
|
|
876
|
+
example: {
|
|
877
|
+
compiler: 'agbcc',
|
|
878
|
+
unit: CALLS + 'extern u8 gEntityArray[]; void example(void) { @ }',
|
|
879
|
+
before: 'f(*(u8 *)0x4000006, gEntityArray[8]);',
|
|
880
|
+
after: 'u8 *p = (u8 *)0x4000006; u8 *e = gEntityArray; f(*p, e[8]);',
|
|
881
|
+
},
|
|
882
|
+
implementedIn: l3('argbase'),
|
|
883
|
+
seeAlso: ['scopebase', 'livebase'],
|
|
884
|
+
},
|
|
885
|
+
zerosub: {
|
|
886
|
+
title: 'Negation as `0 - x`',
|
|
887
|
+
summary: 'a negated subtraction that is also used elsewhere is written `0 - (a - b)`',
|
|
888
|
+
detail:
|
|
889
|
+
'`-x` and `0 - x` are the same C expression, but not to the folder: `-(a - b)` is rewritten into ' +
|
|
890
|
+
'`(b - a)` before common subexpressions are found, while `0 - (a - b)` keeps the subtraction itself. ' +
|
|
891
|
+
'Over a subtraction the function also uses elsewhere, that is one computation and one register ' +
|
|
892
|
+
'apart.',
|
|
893
|
+
compilerBehavior:
|
|
894
|
+
"gcc 2.9's fold turns `-(a - b)` into `b - a`. On the shared shape agbcc, IDO, KMC gcc and gcc 2.7.2 " +
|
|
895
|
+
'each emit two different functions for the two spellings; mwcc emits one.',
|
|
896
|
+
offeredWhen: {
|
|
897
|
+
when: 'A negated subtraction whose subtraction also appears elsewhere, with no effect inside it.',
|
|
898
|
+
decidedBy: { symbol: 'zeroSubNegates', file: l3('zerosub') },
|
|
899
|
+
},
|
|
900
|
+
example: {
|
|
901
|
+
compiler: 'agbcc',
|
|
902
|
+
unit: 's32 example(s32 a, s32 b) { @ return 0; }',
|
|
903
|
+
before: 'if (a - b < 0) return -(a - b);',
|
|
904
|
+
after: 'if (a - b < 0) return 0 - (a - b);',
|
|
905
|
+
},
|
|
906
|
+
implementedIn: l3('zerosub'),
|
|
907
|
+
},
|
|
908
|
+
volatile: {
|
|
909
|
+
title: 'Volatile pointer local',
|
|
910
|
+
summary: 'a pointer local holding a numeric address points at volatile data',
|
|
911
|
+
detail:
|
|
912
|
+
'A numeric address has no declaration anywhere, so whether the source read it through a `volatile` ' +
|
|
913
|
+
'pointer is not in the assembly. The qualifier only restricts what the compiler may do, so every ' +
|
|
914
|
+
'execution of the qualified spelling is one of the plain one. Which pointers the source qualified is ' +
|
|
915
|
+
'per pointer, so each subset is its own candidate. It also rides on the locals other respell ' +
|
|
916
|
+
'variations create (`livebase/volatile`, `indexed/volatile`, `inlinebase/volatile`).',
|
|
917
|
+
compilerBehavior:
|
|
918
|
+
'A volatile memory reference may not be moved or combined, which reorders the loop optimizer and lands ' +
|
|
919
|
+
'the register allocator on different homes.',
|
|
920
|
+
offeredWhen: {
|
|
921
|
+
when: "A pointer local is assigned a numeric address and never a value containing a global's address.",
|
|
922
|
+
decidedBy: { symbol: 'volatilePtrLocals', file: l3('volatileptr') },
|
|
923
|
+
},
|
|
924
|
+
subject: {
|
|
925
|
+
meaning:
|
|
926
|
+
'The locals qualified, joined by hyphens: `volatile-p1` qualifies only `p1`. With no subject every eligible ' +
|
|
927
|
+
'local is qualified. Subsets are enumerated up to three eligible locals.',
|
|
928
|
+
examples: ['volatile-p1', 'volatile-p0-p1'],
|
|
929
|
+
},
|
|
930
|
+
example: {
|
|
931
|
+
compiler: 'agbcc',
|
|
932
|
+
unit: 'void example(void) { @ while (*p == 0) { } *p = 1; }',
|
|
933
|
+
before: 'u16 *p = (u16 *)0x3000010;',
|
|
934
|
+
after: 'volatile u16 *p = (u16 *)0x3000010;',
|
|
935
|
+
},
|
|
936
|
+
implementedIn: l3('volatileptr'),
|
|
937
|
+
seeAlso: ['vol-store', 'vol-slot', 'livebase'],
|
|
938
|
+
},
|
|
939
|
+
'vol-slot': {
|
|
940
|
+
title: 'Volatile stack local',
|
|
941
|
+
summary: 'a scalar local kept in a stack slot is declared volatile',
|
|
942
|
+
detail:
|
|
943
|
+
'`volatile` on a scalar local forces its value into memory: without it the allocator may keep the ' +
|
|
944
|
+
'value in a callee-saved register across a call. A value kept in a stack slot can also come from an ' +
|
|
945
|
+
'address-taken local or from register pressure, so the assembly does not say which the source used.',
|
|
946
|
+
offeredWhen: { judges: 'each local the lift recovered as a stack slot', gates: ['VOL_SLOT_GATES'] },
|
|
947
|
+
example: {
|
|
948
|
+
compiler: 'agbcc',
|
|
949
|
+
unit: CALLS + 'void example(void) { @ sp0 = f(); g(); h(sp0); }',
|
|
950
|
+
before: 'u16 sp0;',
|
|
951
|
+
after: 'volatile u16 sp0;',
|
|
952
|
+
},
|
|
953
|
+
implementedIn: l3('volatileval'),
|
|
954
|
+
seeAlso: ['volatile', 'inlinebase'],
|
|
955
|
+
},
|
|
956
|
+
'vol-store': {
|
|
957
|
+
title: 'Volatile store to a device register',
|
|
958
|
+
summary: 'a store to a fixed device-register address is written through a volatile cast',
|
|
959
|
+
detail:
|
|
960
|
+
'The spelling a `REG_*` macro produces: the store itself is qualified, where `volatile` qualifies a ' +
|
|
961
|
+
'pointer local holding the address. Only a store whose whole address is a constant inside the ' +
|
|
962
|
+
"target's device-register window is qualified.",
|
|
963
|
+
compilerBehavior:
|
|
964
|
+
"agbcc's loop optimizer promotes an unqualified store to a fixed address into a register and writes " +
|
|
965
|
+
'it once after the loop. A qualified store stays in the loop body, so no loop driving a device ' +
|
|
966
|
+
'register matches without it.',
|
|
967
|
+
offeredWhen: { judges: 'each store through an address', gates: ['VOL_STORE_GATES'] },
|
|
968
|
+
example: {
|
|
969
|
+
compiler: 'agbcc',
|
|
970
|
+
unit: 'void example(s32 n) { s32 i; for (i = 0; i < n; i++) { @ } }',
|
|
971
|
+
before: '*(s32 *)0x40000d4 = i;',
|
|
972
|
+
after: '*(volatile s32 *)0x40000d4 = i;',
|
|
973
|
+
},
|
|
974
|
+
implementedIn: l3('volstore'),
|
|
975
|
+
seeAlso: ['volatile', 'unreduce'],
|
|
976
|
+
},
|
|
977
|
+
unreduce: {
|
|
978
|
+
title: 'Accumulator written as its closed form',
|
|
979
|
+
summary: 'a loop-carried accumulator is deleted and each read is written from the loop counter',
|
|
980
|
+
detail:
|
|
981
|
+
'Strength reduction is a compiler pass, so the assembly shows the accumulated form whichever form ' +
|
|
982
|
+
'the source had. This writes the other one: the accumulator and its step are deleted, and each read ' +
|
|
983
|
+
'becomes its value in terms of the loop counter. Published only when the candidate matches byte for ' +
|
|
984
|
+
'byte, because a device store in the loop may write memory the moved read reads; otherwise it is ' +
|
|
985
|
+
'withheld.',
|
|
986
|
+
compilerBehavior:
|
|
987
|
+
"A compiler-created induction value is initialized below the loop's hoisted invariants, a slot no C " +
|
|
988
|
+
'statement before the loop can reach.',
|
|
989
|
+
offeredWhen: {
|
|
990
|
+
judges: 'each accumulator a loop steps by a constant alongside a counter stepped by a constant',
|
|
991
|
+
gates: ['UNREDUCE_GATES'],
|
|
992
|
+
},
|
|
993
|
+
example: {
|
|
994
|
+
compiler: 'agbcc',
|
|
995
|
+
unit: '#define REG 0x40000d4\nvoid example(s32 a0, s32 a1) { s32 v0; s32 v1 = 0; @ }',
|
|
996
|
+
before: 'v0 = (v1 << 6) + a1; while (v1 <= 31) { *(s32 *)REG = v0; v0 = v0 + 64; v1 = v1 + 1; }',
|
|
997
|
+
after: 'while (v1 <= 31) { *(s32 *)REG = (v1 << 6) + a1; v1 = v1 + 1; }',
|
|
998
|
+
note: 'wins only together with `vol-store`',
|
|
999
|
+
},
|
|
1000
|
+
implementedIn: l3('unreduce'),
|
|
1001
|
+
seeAlso: ['vol-store', 'ptr-field', 'indexed'],
|
|
1002
|
+
},
|
|
1003
|
+
'ptr-field': {
|
|
1004
|
+
title: 'Word field declared a pointer',
|
|
1005
|
+
summary: 'a recovered word-wide struct field is declared a pointer, cast back at each read',
|
|
1006
|
+
detail:
|
|
1007
|
+
'A struct field is typed from its access width alone, so a 4-byte field is `s32`. On a 32-bit target ' +
|
|
1008
|
+
'`void *` fits the same load exactly. Every read is cast back to the recovered integer type, so each ' +
|
|
1009
|
+
'use computes the same value.',
|
|
1010
|
+
compilerBehavior:
|
|
1011
|
+
'A pointer and an `s32` are different alias sets. At `-O2` the loop optimizer may hoist a pointer ' +
|
|
1012
|
+
"field's load past an `s32` store it must otherwise keep behind.",
|
|
1013
|
+
offeredWhen: { judges: 'each recovered struct field', gates: ['PTR_FIELD_GATES'] },
|
|
1014
|
+
example: {
|
|
1015
|
+
compiler: 'agbcc',
|
|
1016
|
+
unit: 'struct S { s32 field_0; @ }; void example(struct S *s, s32 lo) { s32 i; for (i = lo; i < 32; i++) { *(volatile s32 *)0x40000d4 = (s32)s->field_4 + i * 64; } }',
|
|
1017
|
+
before: 's32 field_4;',
|
|
1018
|
+
after: 'void *field_4;',
|
|
1019
|
+
note: 'wins only together with `vol-store/unreduce`',
|
|
1020
|
+
},
|
|
1021
|
+
implementedIn: l3('ptrfield'),
|
|
1022
|
+
seeAlso: ['unreduce', 'vol-store'],
|
|
1023
|
+
},
|
|
1024
|
+
offmember: {
|
|
1025
|
+
title: 'Constant subscript as a struct member',
|
|
1026
|
+
summary: 'a constant subscript of a fixed address is written as a struct member',
|
|
1027
|
+
detail:
|
|
1028
|
+
'Both spellings denote the same cell. The assembly says which one the compiler was given: an offset ' +
|
|
1029
|
+
"that reached the load's own displacement got there because nothing folded it into the address. A " +
|
|
1030
|
+
'named base (`basefold`) is the other source of that shape, so both are enumerated.',
|
|
1031
|
+
compilerBehavior:
|
|
1032
|
+
'On a compiler that folds a constant address offset, a subscript folds into the literal ' +
|
|
1033
|
+
'(`.word 0x3003476` + `ldrh r0, [r0]`) while a member stays in the operand (`.word 0x3003468` + ' +
|
|
1034
|
+
'`ldrh r0, [r0, #0xe]`).',
|
|
1035
|
+
offeredWhen: {
|
|
1036
|
+
judges: 'each fixed-address base a load reads through',
|
|
1037
|
+
gates: ['OFFMEMBER_GATES'],
|
|
1038
|
+
},
|
|
1039
|
+
example: {
|
|
1040
|
+
compiler: 'agbcc',
|
|
1041
|
+
unit: 'struct S { u8 pad[14]; u16 m14; }; s32 example(void) { s32 x; @ return x; }',
|
|
1042
|
+
before: 'x = ((u16 *)0x3003468)[7];',
|
|
1043
|
+
after: 'x = ((struct S *)0x3003468)->m14;',
|
|
1044
|
+
},
|
|
1045
|
+
implementedIn: l3('offmember'),
|
|
1046
|
+
seeAlso: ['basefold', 'livebase'],
|
|
1047
|
+
},
|
|
1048
|
+
inlinebase: {
|
|
1049
|
+
title: 'Constant address written at each use',
|
|
1050
|
+
summary: 'a pointer local holding a constant address is deleted and the address written at each use',
|
|
1051
|
+
detail:
|
|
1052
|
+
'Structuring holds a constant with several consumers that lives across a call in a pointer local, the ' +
|
|
1053
|
+
'callee-saved register the compiler kept it in. A constant written at each use is folded into that ' +
|
|
1054
|
+
'same register, so the assembly does not say the source named it. A second candidate carries ' +
|
|
1055
|
+
'`volatile` onto each cast, since the deleted local was the only place a volatile pointee could be ' +
|
|
1056
|
+
'written.',
|
|
1057
|
+
compilerBehavior:
|
|
1058
|
+
"The local's assignment is scheduled ahead of the rest of the entry block, so the pool load moves in " +
|
|
1059
|
+
'front of the frame address the target materializes first.',
|
|
1060
|
+
offeredWhen: { judges: 'each local holding a constant', gates: ['INLINEBASE_GATES'] },
|
|
1061
|
+
example: {
|
|
1062
|
+
compiler: 'agbcc',
|
|
1063
|
+
unit: CALLS + 'void example(s32 n) { s32 i; @ }',
|
|
1064
|
+
before: 'u16 *p = (u16 *)0x4000208; for (i = 0; i < n; i++) { *p = i; f(); }',
|
|
1065
|
+
after: 'for (i = 0; i < n; i++) { *(u16 *)0x4000208 = i; f(); }',
|
|
1066
|
+
},
|
|
1067
|
+
implementedIn: l3('inlinebase'),
|
|
1068
|
+
seeAlso: ['vol-slot', 'volatile'],
|
|
1069
|
+
},
|
|
1070
|
+
scopebase: {
|
|
1071
|
+
title: 'Base pointer at its innermost scope',
|
|
1072
|
+
summary: 'a reused global base is named in a pointer local assigned in the scope holding its uses',
|
|
1073
|
+
detail:
|
|
1074
|
+
'The default base hoist names a reused base at the top of the function, which keeps it live across ' +
|
|
1075
|
+
'everything before an `if` arm that alone uses it. This assigns the local in the innermost statement ' +
|
|
1076
|
+
"list holding all of its uses. It also sees the bare `gSym[i]` spelling a map's declared array " +
|
|
1077
|
+
'produces, which the default hoist does not.',
|
|
1078
|
+
offeredWhen: {
|
|
1079
|
+
judges: 'each global base whose uses all sit inside one nested statement list',
|
|
1080
|
+
gates: ['SCOPEBASE_ELIGIBILITY', 'SCOPEBASE_GATES'],
|
|
1081
|
+
},
|
|
1082
|
+
example: {
|
|
1083
|
+
compiler: 'agbcc',
|
|
1084
|
+
unit: CALLS + 'extern u16 gTbl[]; void example(s32 c, u16 a) { u16 *p; @ }',
|
|
1085
|
+
before: 'p = gTbl; f(); if (c) { p[0x252] = a; p[0x272] = a + 1; }',
|
|
1086
|
+
after: 'f(); if (c) { p = gTbl; p[0x252] = a; p[0x272] = a + 1; }',
|
|
1087
|
+
},
|
|
1088
|
+
implementedIn: l3('scopebase'),
|
|
1089
|
+
seeAlso: ['regionbase', 'livebase', 'coalesce'],
|
|
1090
|
+
},
|
|
1091
|
+
regionbase: {
|
|
1092
|
+
title: 'One base pointer per region',
|
|
1093
|
+
summary: 'a base used in several separate regions gets one pointer local per region',
|
|
1094
|
+
detail:
|
|
1095
|
+
'The second region rule of `scopebase`: a base the source uses inside N disjoint regions becomes N ' +
|
|
1096
|
+
'locals, each assigned in its own region.',
|
|
1097
|
+
compilerBehavior:
|
|
1098
|
+
'agbcc distinguishes the number of locals with disjoint lifetimes, not where they are declared: the ' +
|
|
1099
|
+
'function-top and block-scoped declarations assemble identically, and a count of one does not.',
|
|
1100
|
+
offeredWhen: {
|
|
1101
|
+
judges: 'each global base, in each region that uses it',
|
|
1102
|
+
gates: ['SCOPEBASE_ELIGIBILITY', 'REGIONBASE_GATES'],
|
|
1103
|
+
},
|
|
1104
|
+
example: {
|
|
1105
|
+
compiler: 'agbcc',
|
|
1106
|
+
unit: 'extern u8 gTbl[]; void example(s32 c, u8 a, u8 b, u8 d, u8 e) { u8 *p; u8 *p0; u8 *p1; @ }',
|
|
1107
|
+
before: 'p = gTbl; if (c) { p[1] = a; p[2] = b; } else { p[3] = d; p[4] = e; }',
|
|
1108
|
+
after: 'if (c) { p0 = gTbl; p0[1] = a; p0[2] = b; } else { p1 = gTbl; p1[3] = d; p1[4] = e; }',
|
|
1109
|
+
},
|
|
1110
|
+
implementedIn: l3('scopebase'),
|
|
1111
|
+
seeAlso: ['scopebase', 'homesplit', 'vol-store'],
|
|
1112
|
+
},
|
|
1113
|
+
argcopy: {
|
|
1114
|
+
title: 'A pointer parameter copied for one region',
|
|
1115
|
+
summary: 'a region copies an incoming pointer parameter into a local and uses the copy',
|
|
1116
|
+
detail:
|
|
1117
|
+
'A pointer parameter the whole function reads pins its incoming register for the whole body. A source ' +
|
|
1118
|
+
'that copies it into a local at the head of the block that uses it gives the allocator a second name ' +
|
|
1119
|
+
'for the same address, which it may home elsewhere — freeing the parameter register for something ' +
|
|
1120
|
+
"else, such as that block's loop counter. Uses outside the chosen region keep naming the parameter, " +
|
|
1121
|
+
'which is what makes the two ranges separable.',
|
|
1122
|
+
compilerBehavior:
|
|
1123
|
+
'A braced declaration inside the region and a plain local assigned at its head were both taken through ' +
|
|
1124
|
+
'agbcc on the row this variation was built for and produced the same bytes, so the copy is emitted as ' +
|
|
1125
|
+
'a plain local and nothing here carries block scope. The variation has no target gate, so it is offered ' +
|
|
1126
|
+
'on every compiler — as /scopebase, /regionbase and /coalesce, which rest on the same allocator fact, ' +
|
|
1127
|
+
'also are. A target gate would have to claim the copy is INERT on some compiler, the way /advance claims ' +
|
|
1128
|
+
'it of a target that folds a pointer advance; nobody has compiled the pair that would say so, and a gate ' +
|
|
1129
|
+
'withholding a candidate on an untested guess costs matches rather than fan.',
|
|
1130
|
+
offeredWhen: {
|
|
1131
|
+
judges: 'each pointer parameter, in each nested statement list that reads it',
|
|
1132
|
+
gates: ['ARGCOPY_GATES', 'ARGCOPY_REGION_GATES'],
|
|
1133
|
+
},
|
|
1134
|
+
subject: {
|
|
1135
|
+
meaning:
|
|
1136
|
+
'The parameter, then the region that copies it, as a path of statement-index/list-index ' +
|
|
1137
|
+
'PAIRS — one pair per nesting level. `argcopy-a0@0.1` copies `a0` at the head of the second ' +
|
|
1138
|
+
'nested list of the first statement; `argcopy-a0@0.1.2.0` copies it in the first nested list ' +
|
|
1139
|
+
'of the third statement of that one.',
|
|
1140
|
+
examples: ['argcopy-a0@0.0', 'argcopy-a0@0.1', 'argcopy-a0@0.1.2.0'],
|
|
1141
|
+
},
|
|
1142
|
+
example: {
|
|
1143
|
+
compiler: 'agbcc',
|
|
1144
|
+
unit: CALLS + 'void example(u8 *a0, u8 a1, s32 c) { u8 *p0; s32 i; @ }',
|
|
1145
|
+
before: 'if (c) { i = 0; do { if (g(a0 + i) != 0) a0[i + 50] = a1; i = i + 1; } while (i <= 3); }',
|
|
1146
|
+
after: 'if (c) { p0 = a0; i = 0; do { if (g(p0 + i) != 0) p0[i + 50] = a1; i = i + 1; } while (i <= 3); }',
|
|
1147
|
+
},
|
|
1148
|
+
implementedIn: l3('argcopy'),
|
|
1149
|
+
seeAlso: ['scopebase', 'regionbase', 'parkfirst', 'coalesce'],
|
|
1150
|
+
},
|
|
1151
|
+
coalesce: {
|
|
1152
|
+
title: 'Two locals share one variable',
|
|
1153
|
+
summary: 'two locals whose lifetimes never overlap are merged into one',
|
|
1154
|
+
detail:
|
|
1155
|
+
'Which locals the register allocator gave one register is not in the tree, and picking the first ' +
|
|
1156
|
+
'legal merge gets it wrong, so every legal single merge is its own candidate. It is offered in two cases: the ' +
|
|
1157
|
+
'lifetimes are disjoint in statement order, or one `if` picks between the two.',
|
|
1158
|
+
offeredWhen: {
|
|
1159
|
+
judges: 'each pair of locals, by statement order and by opposite arms of one `if`',
|
|
1160
|
+
gates: ['COALESCE_GATES', 'ARM_DISJOINT_GATES'],
|
|
1161
|
+
},
|
|
1162
|
+
subject: {
|
|
1163
|
+
meaning:
|
|
1164
|
+
'The two locals, joined by a hyphen: `coalesce-v0-v1` renames `v0` to `v1` and drops the declaration of `v0`.',
|
|
1165
|
+
examples: ['coalesce-v0-v1', 'coalesce-v2-v3'],
|
|
1166
|
+
},
|
|
1167
|
+
example: {
|
|
1168
|
+
compiler: 'agbcc',
|
|
1169
|
+
unit: 'void example(u8 *a, u8 *b, s32 n) { s32 v0; s32 v1; @ }',
|
|
1170
|
+
before: 'for (v0 = 0; v0 < n; v0++) a[v0] = b[v0]; for (v1 = n; v1 < 16; v1++) a[v1] = 0;',
|
|
1171
|
+
after: 'for (v1 = 0; v1 < n; v1++) a[v1] = b[v1]; for (v1 = n; v1 < 16; v1++) a[v1] = 0;',
|
|
1172
|
+
note: 'as the variation `coalesce-v0-v1` spells it',
|
|
1173
|
+
},
|
|
1174
|
+
implementedIn: l3('coalesce'),
|
|
1175
|
+
seeAlso: ['merge-names', 'scopebase', 'livebase'],
|
|
1176
|
+
},
|
|
1177
|
+
indexed: {
|
|
1178
|
+
title: 'Indexed loop, not a pointer walk',
|
|
1179
|
+
summary: 'a pointer walk is written as the indexed loop it was reduced from',
|
|
1180
|
+
detail:
|
|
1181
|
+
'A compiler reduces a source `arr[i]` loop into a pointer walk, so the faithful lift emits the walk, ' +
|
|
1182
|
+
'and recompiling the walk rarely reproduces the bytes the indexed source produced. This writes the ' +
|
|
1183
|
+
'indexed loop back.',
|
|
1184
|
+
compilerBehavior: 'The two spellings get a different induction variable and a different register allocation.',
|
|
1185
|
+
offeredWhen: {
|
|
1186
|
+
when: 'A loop walks a pointer one element per step, or counts down the do-while agbcc emits for an indexed loop.',
|
|
1187
|
+
decidedBy: { symbol: 'reindexWalks', file: l3('reindex') },
|
|
1188
|
+
gates: ['COUNTDOWN_GATES'],
|
|
1189
|
+
},
|
|
1190
|
+
example: {
|
|
1191
|
+
compiler: 'agbcc',
|
|
1192
|
+
unit: CALLS + 'void example(u8 *base, s32 n) { u8 *p = base; s32 i; @ }',
|
|
1193
|
+
before: 'while (p < base + n) { use(*p); p = p + 1; }',
|
|
1194
|
+
after: 'for (i = 0; i < n; i++) { use(base[i]); }',
|
|
1195
|
+
},
|
|
1196
|
+
implementedIn: l3('reindex'),
|
|
1197
|
+
seeAlso: ['unreduce', 'volatile', 'livebase'],
|
|
1198
|
+
},
|
|
1199
|
+
livebase: {
|
|
1200
|
+
title: 'Base pointer held across the body',
|
|
1201
|
+
summary: 'a fixed address reused inside a loop or at one offset again and again is held in a pointer local',
|
|
1202
|
+
detail:
|
|
1203
|
+
'The default base hoist refuses a base reused inside a loop or at a repeated constant offset, ' +
|
|
1204
|
+
'predicting that the compiler loads the address again. A memory-mapped poll (store, then re-read the ' +
|
|
1205
|
+
'same register while it spins) is where that prediction is wrong: the compiler holds one register ' +
|
|
1206
|
+
'across the stores, the loop and the read-back. This hoist admits those bases. Its combinations with ' +
|
|
1207
|
+
'`indexed`, `sinkinit`, `nearbase`, `coalesce` and `homesplit` are enumerated beside it.',
|
|
1208
|
+
offeredWhen: {
|
|
1209
|
+
judges: 'each fixed-address base reached twice or more, even inside a loop or at one repeated offset',
|
|
1210
|
+
gates: ['LIVEBASE_GATES'],
|
|
1211
|
+
},
|
|
1212
|
+
example: {
|
|
1213
|
+
compiler: 'agbcc',
|
|
1214
|
+
unit: 'void example(u32 go) { @ }',
|
|
1215
|
+
before: '((u32 *)0x40000d4)[2] = go; while (((u32 *)0x40000d4)[2] & 0x80000000) {}',
|
|
1216
|
+
after: 'u32 *p = (u32 *)0x40000d4; p[2] = go; while (p[2] & 0x80000000) {}',
|
|
1217
|
+
},
|
|
1218
|
+
implementedIn: BASECSE,
|
|
1219
|
+
seeAlso: ['livebase-block', 'volatile', 'offmember'],
|
|
1220
|
+
},
|
|
1221
|
+
'livebase-block': {
|
|
1222
|
+
title: 'Base pointer for a register block only',
|
|
1223
|
+
summary: 'like `livebase`, but a base read at one fixed offset stays inline',
|
|
1224
|
+
detail:
|
|
1225
|
+
'Which of several numeric bases the source named is per base: a DMA register block wants one register ' +
|
|
1226
|
+
'held across the body while the RAM halfword beside it is loaded each time. This is `livebase` with ' +
|
|
1227
|
+
'every base reached at a single fixed offset left inline.',
|
|
1228
|
+
offeredWhen: {
|
|
1229
|
+
judges: 'each fixed-address base reached twice or more, at more than one offset',
|
|
1230
|
+
gates: ['LIVEBASE_BLOCK_GATES'],
|
|
1231
|
+
},
|
|
1232
|
+
example: {
|
|
1233
|
+
compiler: 'agbcc',
|
|
1234
|
+
unit: 'void example(u32 src, u32 go) { u32 *p = (u32 *)0x40000d4; u16 *q = (u16 *)0x3001048; @ }',
|
|
1235
|
+
before: 'p[0] = src; p[2] = go; q[0] = q[0] + 1;',
|
|
1236
|
+
after: 'p[0] = src; p[2] = go; *(u16 *)0x3001048 = *(u16 *)0x3001048 + 1;',
|
|
1237
|
+
note: 'with `p` the DMA block and `q` the halfword, both held in pointer locals by `livebase`',
|
|
1238
|
+
},
|
|
1239
|
+
implementedIn: BASECSE,
|
|
1240
|
+
seeAlso: ['livebase', 'homesplit'],
|
|
1241
|
+
},
|
|
1242
|
+
basefold: {
|
|
1243
|
+
title: 'Base pointer for an unfolded offset',
|
|
1244
|
+
summary: 'a fixed address whose offset stayed in the load gets a pointer local, even if used once',
|
|
1245
|
+
detail:
|
|
1246
|
+
'The default hoist refuses a base reached once. A load that kept its constant offset in the operand is ' +
|
|
1247
|
+
'evidence against that, on a compiler that folds a subscript into the literal: something other than a ' +
|
|
1248
|
+
'subscript put it there. `basefold/sinkinit` is the same rule with the local assigned at its first use.',
|
|
1249
|
+
compilerBehavior:
|
|
1250
|
+
"agbcc folds a constant subscript into the literal it loads and keeps a named base's offset in the " +
|
|
1251
|
+
'instruction.',
|
|
1252
|
+
offeredWhen: {
|
|
1253
|
+
judges: 'each fixed-address base whose load kept its constant offset, even one reached once',
|
|
1254
|
+
gates: ['BASEFOLD_GATES'],
|
|
1255
|
+
},
|
|
1256
|
+
example: {
|
|
1257
|
+
compiler: 'agbcc',
|
|
1258
|
+
unit: 's32 example(void) { s32 x; @ return x; }',
|
|
1259
|
+
before: 'x = ((u16 *)0x3003468)[7];',
|
|
1260
|
+
after: 'u16 *p = (u16 *)0x3003468; x = p[7];',
|
|
1261
|
+
},
|
|
1262
|
+
implementedIn: BASECSE,
|
|
1263
|
+
seeAlso: ['offmember', 'unfolded', 'sinkinit'],
|
|
1264
|
+
},
|
|
1265
|
+
unfolded: {
|
|
1266
|
+
title: 'Unfolded base pointer at first use',
|
|
1267
|
+
summary: 'a reused fixed address whose offset stayed in the load is assigned where it is first used',
|
|
1268
|
+
detail:
|
|
1269
|
+
'Admits a base reached two or more times whose offset survived into the load, and assigns its local ' +
|
|
1270
|
+
'at the first use. Where it binds a base another hoist already names it takes the name, so its count ' +
|
|
1271
|
+
'includes renames.',
|
|
1272
|
+
offeredWhen: {
|
|
1273
|
+
judges: 'each fixed-address base reached twice or more whose load kept its constant offset',
|
|
1274
|
+
gates: ['UNFOLDED_GATES'],
|
|
1275
|
+
},
|
|
1276
|
+
example: {
|
|
1277
|
+
compiler: 'agbcc',
|
|
1278
|
+
unit: CALLS + 's32 example(void) { u16 *p; s32 x; s32 y; @ return x + y; }',
|
|
1279
|
+
before: 'f(); x = ((u16 *)0x3003468)[7]; y = ((u16 *)0x3003468)[8];',
|
|
1280
|
+
after: 'f(); p = (u16 *)0x3003468; x = p[7]; y = p[8];',
|
|
1281
|
+
},
|
|
1282
|
+
implementedIn: BASECSE,
|
|
1283
|
+
seeAlso: ['basefold', 'livebase', 'sinkinit'],
|
|
1284
|
+
},
|
|
1285
|
+
orderbase: {
|
|
1286
|
+
title: 'Base pointer the load order licenses',
|
|
1287
|
+
summary: 'an array base the assembly loaded before scaling its index gets a pointer local',
|
|
1288
|
+
detail:
|
|
1289
|
+
'The only base hoist whose evidence is the instruction order: the base was materialized before the ' +
|
|
1290
|
+
"index was scaled, which is what a declared array or a pointer local's own assignment produces. It " +
|
|
1291
|
+
'can bind the `(struct S *)&gSym` base of an array-of-struct element, which no other hoist sees.',
|
|
1292
|
+
compilerBehavior:
|
|
1293
|
+
"On agbcc the array subscript expansion loads a declared array's base first and scales the index " +
|
|
1294
|
+
'first for the inline cast.',
|
|
1295
|
+
offeredWhen: {
|
|
1296
|
+
judges: 'each fixed-address base the assembly loaded before scaling its index',
|
|
1297
|
+
gates: ['ORDERBASE_GATES'],
|
|
1298
|
+
},
|
|
1299
|
+
example: {
|
|
1300
|
+
compiler: 'agbcc',
|
|
1301
|
+
unit: 'struct S { u16 f; u16 g; }; extern u8 gTbl[]; s32 example(s32 i) { s32 x; s32 y; @ return x + y; }',
|
|
1302
|
+
before: 'x = ((struct S *)&gTbl)[i].f; y = ((struct S *)&gTbl)[i].g;',
|
|
1303
|
+
after: 'struct S *p = (struct S *)&gTbl; x = p[i].f; y = p[i].g;',
|
|
1304
|
+
},
|
|
1305
|
+
implementedIn: BASECSE,
|
|
1306
|
+
seeAlso: ['orderbase-scoped', 'livebase'],
|
|
1307
|
+
},
|
|
1308
|
+
'orderbase-scoped': {
|
|
1309
|
+
title: 'Licensed base pointer inside its scope',
|
|
1310
|
+
summary: 'like `orderbase`, with the local assigned inside the nested scope holding its uses',
|
|
1311
|
+
detail:
|
|
1312
|
+
'The same admission as `orderbase`, with the assignment placed in the nested statement list holding ' +
|
|
1313
|
+
'every use. Where no nested list holds them all it declines rather than repeat the flat placement.',
|
|
1314
|
+
compilerBehavior: 'The same assignment above an `if` and inside its arm compile differently on agbcc.',
|
|
1315
|
+
offeredWhen: {
|
|
1316
|
+
judges: 'each base the assembly loaded before scaling its index, whose uses one nested statement list holds',
|
|
1317
|
+
gates: ['ORDERBASE_GATES'],
|
|
1318
|
+
},
|
|
1319
|
+
example: {
|
|
1320
|
+
compiler: 'agbcc',
|
|
1321
|
+
unit: 'struct S { u16 f; u16 g; }; extern u8 gTbl[]; s32 example(s32 c, s32 i) { struct S *p; s32 x = 0; @ return x; }',
|
|
1322
|
+
before: 'p = (struct S *)&gTbl; if (c) { x = p[i].f; }',
|
|
1323
|
+
after: 'if (c) { p = (struct S *)&gTbl; x = p[i].f; }',
|
|
1324
|
+
},
|
|
1325
|
+
implementedIn: BASECSE,
|
|
1326
|
+
seeAlso: ['orderbase', 'scopebase'],
|
|
1327
|
+
},
|
|
1328
|
+
homesplit: {
|
|
1329
|
+
title: 'One base at the top, one split per region',
|
|
1330
|
+
summary: 'after a `livebase` hoist, one withheld base is split into one local per region',
|
|
1331
|
+
detail:
|
|
1332
|
+
'Both base policies are whole-function: `livebase` holds every base it admits in a local at the top, and ' +
|
|
1333
|
+
'`regionbase` splits every base it admits. A function whose two bases want opposite answers is spelled ' +
|
|
1334
|
+
'by neither. This runs the hoist with one base withheld, then splits that base per region. It always ' +
|
|
1335
|
+
'follows `livebase` or `livebase-block` in a name.',
|
|
1336
|
+
offeredWhen: {
|
|
1337
|
+
judges: 'each base a `livebase` or `livebase-block` hoist holds in a pointer local',
|
|
1338
|
+
gates: ['HOMESPLIT_FAN_GATES', 'HOMESPLIT_GATES'],
|
|
1339
|
+
},
|
|
1340
|
+
subject: {
|
|
1341
|
+
meaning:
|
|
1342
|
+
'The withheld base, then `.`, its access width in bytes and `s` or `u` for signedness: ' +
|
|
1343
|
+
'`homesplit-0x40000d4.4s` withholds the signed 4-byte key at 0x40000d4. A symbol base is spelled by ' +
|
|
1344
|
+
'name, with a cast type in `<…>` when it has one.',
|
|
1345
|
+
examples: ['homesplit-0x40000d4.4s', 'homesplit-gFoo<u8*>.1u'],
|
|
1346
|
+
},
|
|
1347
|
+
example: {
|
|
1348
|
+
compiler: 'agbcc',
|
|
1349
|
+
unit: 'void example(s32 c) { u16 *w0; u16 *w1; @ }',
|
|
1350
|
+
before: 'u32 *d = (u32 *)0x40000d4; u16 *w = (u16 *)0x3001048; if (c) { d[2] = w[0]; } else { d[3] = w[1]; }',
|
|
1351
|
+
after:
|
|
1352
|
+
'u32 *d = (u32 *)0x40000d4; if (c) { w0 = (u16 *)0x3001048; d[2] = w0[0]; } else { w1 = (u16 *)0x3001048; d[3] = w1[1]; }',
|
|
1353
|
+
note: 'the halfword base withheld from the top and split',
|
|
1354
|
+
},
|
|
1355
|
+
implementedIn: l3('homesplit'),
|
|
1356
|
+
seeAlso: ['livebase', 'livebase-block', 'regionbase'],
|
|
1357
|
+
},
|
|
1358
|
+
mulfirst: {
|
|
1359
|
+
title: 'Product first in a sum',
|
|
1360
|
+
summary: 'a sum with one product operand puts the product first',
|
|
1361
|
+
detail:
|
|
1362
|
+
'Operands of a commutative sum are spelled in evaluation order, which recovers the source order on ' +
|
|
1363
|
+
'gcc. IDO can load the independent operand above the multiply, so evaluation order spells a ' +
|
|
1364
|
+
'product-first source the other way round.',
|
|
1365
|
+
compilerBehavior:
|
|
1366
|
+
'IDO loads a struct field `c` in `a * b + c` above the multiply, so the order of the loads does not show ' +
|
|
1367
|
+
'which operand the source wrote first. On mwcc the two orders compile to one object.',
|
|
1368
|
+
offeredWhen: {
|
|
1369
|
+
when: 'A `+` with exactly one product operand and no effect in either operand.',
|
|
1370
|
+
decidedBy: { symbol: 'mulFirstSums', file: l3('mulfirst') },
|
|
1371
|
+
},
|
|
1372
|
+
example: {
|
|
1373
|
+
compiler: 'ido',
|
|
1374
|
+
unit: 'struct Bg { s32 tiles; u8 pad[12]; u16 w; u16 h; }; s32 example(struct Bg *bg) { s32 x; @ return x; }',
|
|
1375
|
+
before: 'x = bg->tiles + bg->w * bg->h;',
|
|
1376
|
+
after: 'x = bg->w * bg->h + bg->tiles;',
|
|
1377
|
+
},
|
|
1378
|
+
implementedIn: l3('mulfirst'),
|
|
1379
|
+
},
|
|
1380
|
+
nearbase: {
|
|
1381
|
+
title: 'Neighbor cells from one base',
|
|
1382
|
+
summary: 'nearby fixed addresses are written as offsets from one base local',
|
|
1383
|
+
detail:
|
|
1384
|
+
"Raw-address accesses a few bytes apart are one object's cells: the compiler holds the object's base " +
|
|
1385
|
+
'in a register and derives each cell, where a per-cell spelling gives each address its own pool ' +
|
|
1386
|
+
'constant.',
|
|
1387
|
+
compilerBehavior:
|
|
1388
|
+
'Past the load range the compiler derives a cell with an `add` off one pool word instead of loading a ' +
|
|
1389
|
+
'second literal.',
|
|
1390
|
+
offeredWhen: {
|
|
1391
|
+
when: "Two or more distinct constant addresses fall within the target's derivation reach.",
|
|
1392
|
+
decidedBy: { symbol: 'nearBaseClusters', file: l3('nearbase') },
|
|
1393
|
+
},
|
|
1394
|
+
example: {
|
|
1395
|
+
compiler: 'agbcc',
|
|
1396
|
+
unit: 's32 example(void) { s32 x; s32 y; @ return x + y; }',
|
|
1397
|
+
before: 'x = *(u16 *)0x0300104A; y = *(u16 *)0x03001048;',
|
|
1398
|
+
after: 'u8 *b = (u8 *)0x03001048; x = *(u16 *)(b + 2); y = *(u16 *)b;',
|
|
1399
|
+
},
|
|
1400
|
+
implementedIn: l3('nearbase'),
|
|
1401
|
+
seeAlso: ['livebase', 'sinkinit', 'advance'],
|
|
1402
|
+
},
|
|
1403
|
+
advance: {
|
|
1404
|
+
title: 'Pointer advanced between accesses',
|
|
1405
|
+
summary: 'accesses the machine made through one moving register are written through a stepped pointer',
|
|
1406
|
+
detail:
|
|
1407
|
+
'The assembly held an address in a register, used it, added to it and used it again. The lift folds ' +
|
|
1408
|
+
'that pair into two constant addresses and records that it did; this writes the stepped pointer. The ' +
|
|
1409
|
+
'chain is read off constant addresses, so a symbol map that names them hides it.',
|
|
1410
|
+
compilerBehavior:
|
|
1411
|
+
'Through a plain pointer agbcc folds the step into the second access’s offset, so the step compiles to ' +
|
|
1412
|
+
'no instruction. Through a `volatile` pointer it keeps the step as an `add` between the two accesses, ' +
|
|
1413
|
+
'which the same accesses through two constant addresses do not compile to.',
|
|
1414
|
+
offeredWhen: {
|
|
1415
|
+
judges: 'each chain of accesses the machine made through one stepped register',
|
|
1416
|
+
gates: ['ADVANCE_HEAD_GATES', 'ADVANCE_MEMBER_GATES'],
|
|
1417
|
+
},
|
|
1418
|
+
example: {
|
|
1419
|
+
compiler: 'agbcc',
|
|
1420
|
+
unit: 'void example(u16 a, u16 b) { @ }',
|
|
1421
|
+
before: '*(volatile u16 *)0x04000048 = a; *(volatile u16 *)0x0400004A = b;',
|
|
1422
|
+
after: 'volatile u16 *p = (volatile u16 *)0x04000048; *p = a; p = p + 1; *p = b;',
|
|
1423
|
+
note: 'shown with `volatile`, the company agbcc offers it in',
|
|
1424
|
+
},
|
|
1425
|
+
implementedIn: l3('advance'),
|
|
1426
|
+
seeAlso: ['volatile', 'nearbase'],
|
|
1427
|
+
},
|
|
1428
|
+
parkfirst: {
|
|
1429
|
+
title: 'Parameter copies first',
|
|
1430
|
+
summary: 'copies of incoming parameters move to the front of the entry block',
|
|
1431
|
+
detail:
|
|
1432
|
+
'A copy of a parameter into a local reproduces a register park, and the park lifts to no instruction ' +
|
|
1433
|
+
'at all, so its position falls out of emission order. The compiler may have parked before anything ' +
|
|
1434
|
+
'else ran. Only plain assignments of parameters and constants in the leading run move, never across a ' +
|
|
1435
|
+
'statement that touches what they read or write.',
|
|
1436
|
+
offeredWhen: {
|
|
1437
|
+
when: 'The leading run of assignments holds a parameter or constant copy that can move ahead.',
|
|
1438
|
+
decidedBy: { symbol: 'parkParamsFirst', file: l3('parkfirst') },
|
|
1439
|
+
},
|
|
1440
|
+
example: {
|
|
1441
|
+
compiler: 'agbcc',
|
|
1442
|
+
unit: 's32 example(s32 a0, s32 a1) { s32 v0; s32 v1; s32 v2; s32 v3; s32 i; @ v2 = ((u8 *)a0)[4]; v3 = ((u8 *)a0)[5]; for (i = 0; i < a1; i++) { v0 += v1 * v2; v1 += v3; v2 += v0; v3 += v1; } return v0 + v1 + v2 + v3; }',
|
|
1443
|
+
before: 'v0 = *(u8 *)a0; v1 = a1;',
|
|
1444
|
+
after: 'v1 = a1; v0 = *(u8 *)a0;',
|
|
1445
|
+
},
|
|
1446
|
+
implementedIn: l3('parkfirst'),
|
|
1447
|
+
seeAlso: ['sinkinit'],
|
|
1448
|
+
},
|
|
1449
|
+
sinkinit: {
|
|
1450
|
+
title: 'Base pointer assigned at first use',
|
|
1451
|
+
summary: 'each leading base-pointer assignment moves down to the statement that first uses it',
|
|
1452
|
+
detail:
|
|
1453
|
+
'A base hoist assigns every base local at the top of the body, keeping it live across everything ' +
|
|
1454
|
+
'above its first use. This moves each assignment of the leading run down to the first top-level ' +
|
|
1455
|
+
'statement mentioning it, never into a nested scope. It also follows `nearbase` and the `livebase` ' +
|
|
1456
|
+
'hoists.',
|
|
1457
|
+
compilerBehavior:
|
|
1458
|
+
'On `synthetic:basehome` the top assignment costs a callee-saved push and pop that the first-use ' +
|
|
1459
|
+
'assignment avoids.',
|
|
1460
|
+
offeredWhen: {
|
|
1461
|
+
when: 'The body starts with base-pointer assignments whose first use is further down.',
|
|
1462
|
+
decidedBy: { symbol: 'sinkInitsToFirstUse', file: l3('sinkinit') },
|
|
1463
|
+
},
|
|
1464
|
+
example: {
|
|
1465
|
+
compiler: 'agbcc',
|
|
1466
|
+
unit: CALLS + 'extern u8 gTbl[]; void example(void) { u8 *p; @ }',
|
|
1467
|
+
before: 'p = (u8 *)&gTbl; f(); g(); p[1] = 0;',
|
|
1468
|
+
after: 'f(); g(); p = (u8 *)&gTbl; p[1] = 0;',
|
|
1469
|
+
},
|
|
1470
|
+
implementedIn: l3('sinkinit'),
|
|
1471
|
+
seeAlso: ['livebase', 'nearbase', 'basefold'],
|
|
1472
|
+
},
|
|
1473
|
+
regcopy: {
|
|
1474
|
+
title: 'Register copies written out',
|
|
1475
|
+
summary: 'register copies the structurer merged away are written back as locals',
|
|
1476
|
+
detail:
|
|
1477
|
+
'Register allocation leaves source-visible copies that coalescing erases: a merge made as a copy ' +
|
|
1478
|
+
'plus an in-place update on one arm, a large constant staged in its own register, a return value ' +
|
|
1479
|
+
'built in another register. This writes them back, in the top-level statement list only. With no ' +
|
|
1480
|
+
'subject it applies the first two; a subject adds a return assignment.',
|
|
1481
|
+
offeredWhen: {
|
|
1482
|
+
when: 'A top-level `if` whose arms set one variable from one pure value, or a constant expression used as an operand.',
|
|
1483
|
+
decidedBy: { symbol: 'registerishSpellings', file: l3('regspell') },
|
|
1484
|
+
},
|
|
1485
|
+
subject: {
|
|
1486
|
+
meaning:
|
|
1487
|
+
'How the returned value is written. `regcopy-ret` assigns it to the variable the copy left unused ' +
|
|
1488
|
+
'and returns that; `regcopy-ret-fresh` assigns it to a new variable.',
|
|
1489
|
+
examples: ['regcopy-ret', 'regcopy-ret-fresh'],
|
|
1490
|
+
},
|
|
1491
|
+
example: {
|
|
1492
|
+
compiler: 'ido',
|
|
1493
|
+
unit: 's32 example(s32 *p, s32 i) { s32 j; s32 r; j = i; if (j >= 8) j -= 8; @ }',
|
|
1494
|
+
before: 'return p[j] + i;',
|
|
1495
|
+
after: 'r = p[j] + i; return r;',
|
|
1496
|
+
note: 'as the variation `regcopy-ret-fresh` spells it',
|
|
1497
|
+
},
|
|
1498
|
+
implementedIn: l3('regspell'),
|
|
1499
|
+
seeAlso: ['coalesce', 'merge-names'],
|
|
1500
|
+
},
|
|
1501
|
+
initfirst: {
|
|
1502
|
+
title: 'Loop start before its guard',
|
|
1503
|
+
summary: "a loop's starting assignment moves above the guard, and the guard reads the variable",
|
|
1504
|
+
detail:
|
|
1505
|
+
'`for (i = 0; i < n; i++)` compiles with the assignment before the zero-trip test, while ' +
|
|
1506
|
+
'`if (0 < n) { i = 0; do … }` compiles with it behind the branch, and both lift to the same code. It ' +
|
|
1507
|
+
"touches private locals only. Applied on top of every other candidate's source, alone and together " +
|
|
1508
|
+
'with `pollguard` and `pollread`.',
|
|
1509
|
+
offeredWhen: {
|
|
1510
|
+
when: 'An `if` whose arms both start with the same constant assignment, or whose then-arm assigns the value its condition compares.',
|
|
1511
|
+
decidedBy: { symbol: 'initFirstGuards', file: l3('initfirst') },
|
|
1512
|
+
},
|
|
1513
|
+
example: {
|
|
1514
|
+
compiler: 'agbcc',
|
|
1515
|
+
unit: CALLS + 'void example(s32 n) { s32 v; @ }',
|
|
1516
|
+
before: 'if (0 < n) { v = 0; do { use(v); v++; } while (v < n); }',
|
|
1517
|
+
after: 'v = 0; if (v < n) { do { use(v); v++; } while (v < n); }',
|
|
1518
|
+
},
|
|
1519
|
+
implementedIn: l3('initfirst'),
|
|
1520
|
+
seeAlso: ['loop-entry', 'defsite'],
|
|
1521
|
+
},
|
|
1522
|
+
pollguard: {
|
|
1523
|
+
title: 'Guard around an empty wait loop',
|
|
1524
|
+
summary: 'an empty bottom-tested loop gets back the guard the compiler folded into its test',
|
|
1525
|
+
detail:
|
|
1526
|
+
'For an empty body the two forms compile to the same instructions and evaluate the condition the same ' +
|
|
1527
|
+
'number of times. The difference is a register-allocation ripple: the extra source read raises the ' +
|
|
1528
|
+
"use counts of the condition. Applied on top of every other candidate's source.",
|
|
1529
|
+
compilerBehavior:
|
|
1530
|
+
'gcc merges the guard into the bottom test late, after flow counted its reads, which reorders the ' +
|
|
1531
|
+
"allocator's priorities for the whole function.",
|
|
1532
|
+
offeredWhen: {
|
|
1533
|
+
when: 'The function has an empty-bodied `do … while`.',
|
|
1534
|
+
decidedBy: { symbol: 'pollGuards', file: l3('pollguard') },
|
|
1535
|
+
},
|
|
1536
|
+
example: {
|
|
1537
|
+
compiler: 'agbcc',
|
|
1538
|
+
unit: 'void example(volatile u32 *dma, u8 *dst, s32 n) { s32 i; for (i = 0; i < n; i++) { dma[0] = (u32)dst + i; dma[2] = 0x80000020; @ } }',
|
|
1539
|
+
before: 'do { } while (dma[2] & 0x80000000);',
|
|
1540
|
+
after: 'if (dma[2] & 0x80000000) { do { } while (dma[2] & 0x80000000); }',
|
|
1541
|
+
},
|
|
1542
|
+
implementedIn: l3('pollguard'),
|
|
1543
|
+
seeAlso: ['pollread', 'livebase'],
|
|
1544
|
+
},
|
|
1545
|
+
pollread: {
|
|
1546
|
+
title: 'Wait loop reads in its condition',
|
|
1547
|
+
summary: "a poll's value is read inside the loop condition instead of into a variable",
|
|
1548
|
+
detail:
|
|
1549
|
+
'The structurer names a value re-read at each iteration, with a read before the loop and one per ' +
|
|
1550
|
+
'pass. The source may have read it inside the condition of an empty loop. Both read once more than ' +
|
|
1551
|
+
"the number of iterations. Applied on top of every other candidate's source.",
|
|
1552
|
+
compilerBehavior:
|
|
1553
|
+
'The named spelling materializes an extra register and instruction that the in-condition spelling does not.',
|
|
1554
|
+
offeredWhen: {
|
|
1555
|
+
when: "A loop whose only statement re-reads the local its condition tests, into the function's own non-volatile local.",
|
|
1556
|
+
decidedBy: { symbol: 'pollReads', file: l3('pollguard') },
|
|
1557
|
+
},
|
|
1558
|
+
example: {
|
|
1559
|
+
compiler: 'agbcc',
|
|
1560
|
+
unit: '#define BUSY 0x80000000\nvoid example(u32 *dma) { u32 v; @ }',
|
|
1561
|
+
before: 'v = dma[2]; while ((v & BUSY) != 0) { v = dma[2]; }',
|
|
1562
|
+
after: 'while ((dma[2] & BUSY) != 0) {}',
|
|
1563
|
+
},
|
|
1564
|
+
implementedIn: l3('pollguard'),
|
|
1565
|
+
seeAlso: ['pollguard'],
|
|
1566
|
+
},
|
|
1567
|
+
|
|
1568
|
+
// ── symbol map ──────────────────────────────────────────────────────────────────────────────
|
|
1569
|
+
'raw-globals': {
|
|
1570
|
+
title: 'Raw addresses, not map names',
|
|
1571
|
+
summary: "the symbol map's shaped spellings are dropped in favour of raw addresses",
|
|
1572
|
+
detail:
|
|
1573
|
+
'Naming a global changes codegen, and which side matches is per function. The raw candidate still ' +
|
|
1574
|
+
"names globals the literal pool or a relocation spells; it drops only the map's shaped spellings. At " +
|
|
1575
|
+
'equal score the named spelling wins, so this is published only where it scores better.',
|
|
1576
|
+
compilerBehavior: 'On agbcc a named global changes when the address is loaded.',
|
|
1577
|
+
offeredWhen: {
|
|
1578
|
+
when: 'A symbol map was supplied; a row without one never carries it.',
|
|
1579
|
+
decidedBy: { symbol: 'enumerateCandidates', file: RANK },
|
|
1580
|
+
},
|
|
1581
|
+
example: {
|
|
1582
|
+
compiler: 'agbcc',
|
|
1583
|
+
unit: 'extern struct { u8 pad[4]; u8 field; } gCounter; s32 example(void) { s32 x; @ return x; }',
|
|
1584
|
+
before: 'x = gCounter.field;',
|
|
1585
|
+
after: 'x = ((u8 *)0x3003468)[4];',
|
|
1586
|
+
},
|
|
1587
|
+
implementedIn: RANK,
|
|
1588
|
+
seeAlso: ['reread-globals', 'no-bitfield', 'no-ptr-elem'],
|
|
1589
|
+
},
|
|
1590
|
+
};
|