@asmlift/core 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +25 -7
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
// asmlift — A NAME FOR AN ADDRESS THE MACHINE BUILT BY ARITHMETIC, read off the same symbol map
|
|
2
|
+
// the pool-word promotion reads.
|
|
3
|
+
//
|
|
4
|
+
// WHAT IS MISSING WITHOUT THIS. frontend/thumb.ts promotes a POOL-LOADED word to `gaddr sym` by
|
|
5
|
+
// asking the address-keyed map what lives at that address, and its own note records the boundary:
|
|
6
|
+
// "Only pool-loaded words promote (an address built by arithmetic never reaches here)". A
|
|
7
|
+
// compiler that has several neighbouring cells to touch does not load a pool word per cell — it
|
|
8
|
+
// loads one and walks:
|
|
9
|
+
//
|
|
10
|
+
// ldr r1, .L4 @ .word 0x40000ba -> gaddr REG_DMA0CNT_H
|
|
11
|
+
// strh r2, [r1]
|
|
12
|
+
// adds r1, #12
|
|
13
|
+
// strh r3, [r1] @ 0x40000c6 — REG_DMA1CNT_H, and the map says so
|
|
14
|
+
//
|
|
15
|
+
// so the map is never asked about the second cell, and the second store spells an offset walk off
|
|
16
|
+
// the first name. That is not merely a longer spelling. `structure.ts` classifies a symbol whose
|
|
17
|
+
// address is ever an `add`/`sub` operand as AGGREGATE, which drops it from `scalarGlobals`, and
|
|
18
|
+
// the bare-name spelling and the `volatile` qualifier both hang off that set — so one walked store
|
|
19
|
+
// de-names the symbol at EVERY site in the function, including the off-0 ones, and silently casts
|
|
20
|
+
// the `volatile` away. Observable in the object: with the cast form agbcc CSEs a read the target
|
|
21
|
+
// re-reads.
|
|
22
|
+
//
|
|
23
|
+
// It is also a REPRESENTATION gap and not merely a missed call. `SymbolInfo` carries no address
|
|
24
|
+
// and `structure()` receives the map NAME-keyed, so once the frontend has emitted `gaddr S`
|
|
25
|
+
// nothing downstream can compute `addr(S) + K` at all. That puts the pass anywhere BEFORE
|
|
26
|
+
// `structure()`, and it sits at the top of that window for one reason: `raise/arrays.ts`,
|
|
27
|
+
// `raise/struct-arrays.ts` and `raise/memberarrays.ts` match the same `add(gaddr, K)` shape this
|
|
28
|
+
// pass consumes, and `interior-offset` below cedes the in-object case back to them — so the
|
|
29
|
+
// question "is this address an object of its own?" is answered first and what it refuses is handed
|
|
30
|
+
// on untouched. The passes on the OTHER side of the question are `l3/nearbase.ts` and
|
|
31
|
+
// `l3/basecse.ts`: they BUILD the one-base-many-cells form this one un-spells — `nearbase`'s own
|
|
32
|
+
// example is `add #72` / `add #74` off one pool word — which is why the walk stays reachable from
|
|
33
|
+
// `/raw-globals`, where no map name exists to replace it.
|
|
34
|
+
//
|
|
35
|
+
// THE CORPUS DOES NOT REFEREE THE SEAT: run instead from `raiseRecovered`'s
|
|
36
|
+
// `beforeRecover` hook, after every recognizer, and `pnpm bench gates --pass offsetnames` reports
|
|
37
|
+
// the identical counts on all six rows that refuse or name anything, with
|
|
38
|
+
// `kleod:DeleteAllSaveData:agbcc` the same winner at the same 0/81. The argument is about which
|
|
39
|
+
// pass sees a shape first, not about a row.
|
|
40
|
+
//
|
|
41
|
+
// WHAT IT DOES. Over the lifted L1 fn: resolve every `add`/`sub` whose operand tree bottoms out
|
|
42
|
+
// at a `gaddr` plus constants, and where the map holds a symbol EXACTLY at the resulting address,
|
|
43
|
+
// rewrite that op into `gaddr` for it. No opcode, no representation and no level is introduced —
|
|
44
|
+
// `gaddr` and `add` both already exist, and everything downstream is the named-global machinery
|
|
45
|
+
// the pool path already feeds.
|
|
46
|
+
//
|
|
47
|
+
// NAMING IS NOT A FIDELITY LOSS HERE, and that is compiled rather than assumed: the same four
|
|
48
|
+
// `REG_DMA*CNT_H` stores spelled as four independent named volatile MMIO stores compile back to
|
|
49
|
+
// `ldr r1 / strh / adds r1,#12 / strh / …` — agbcc re-derives the walk itself from the four
|
|
50
|
+
// literal addresses. Whether the compiler will re-derive it for an `extern`-shaped name it has to
|
|
51
|
+
// relocate is a SCORE question the ranker referees, not a correctness one; the address this pass
|
|
52
|
+
// spells is the address the machine computed, by the map's own arithmetic.
|
|
53
|
+
//
|
|
54
|
+
// SO WHY A DEFAULT AND NOT A VARIATION. The usual test is whether the asm UNDERDETERMINES the
|
|
55
|
+
// source, and here it does: the `adds r1,#12` chain is what agbcc emits for the walk AND for four
|
|
56
|
+
// independent named stores. What settles it the other way is that the two spellings are not peers.
|
|
57
|
+
// The named one is address-identical by construction and restores the `volatile` the cast form
|
|
58
|
+
// erases (see the AGGREGATE note above); the walk is reachable from `/raw-globals`, which is the
|
|
59
|
+
// arm that drops every map name at once. The price is measured and real — a default rewrite
|
|
60
|
+
// removes the walk spelling from the fan at every admitted site, and `sub_0804E708`'s falls 8 -> 7
|
|
61
|
+
// against `origin/main` — and it buys 0 lost rows. A per-site variation would be the third answer,
|
|
62
|
+
// and nothing in the corpus asks for it yet.
|
|
63
|
+
//
|
|
64
|
+
// WHY THERE IS NO SYNTHETIC ROW, measured rather than conceded. A walk only happens where the
|
|
65
|
+
// compiler saw LITERAL addresses — spelled as `extern`s the pool words are relocations, agbcc
|
|
66
|
+
// cannot see two cells are adjacent, and it loads one word per cell — and a literal address is
|
|
67
|
+
// exactly what `/raw-globals` spells with no map at all. So a row built on the walk alone always
|
|
68
|
+
// carries a map-less candidate with the same bytes. Two reductions were built and both scored the
|
|
69
|
+
// broken candidate exactly as they scored the right one: three MMIO stores twelve bytes apart is
|
|
70
|
+
// 0/13 with this pass and 0/13 with it ablated, and so is the version that re-reads the walked-to
|
|
71
|
+
// cell, whose `/raw-globals` sibling wins at 0/13 either way. The capability is row-visible only
|
|
72
|
+
// where the function ALSO needs something the map provides, which is what
|
|
73
|
+
// `kleod:DeleteAllSaveData:agbcc` is — ablate the pass there and it goes MATCH -> 20/82 while its
|
|
74
|
+
// best `/raw-globals` candidate is 72/97. The unit tests below carry the rest.
|
|
75
|
+
//
|
|
76
|
+
// WHAT REFUSES is `OFFSET_NAME_GATES` below, and every rejection leaves the arithmetic exactly as
|
|
77
|
+
// the frontend emitted it. That is a spelling whose BYTES are right under any declaration, and it
|
|
78
|
+
// is not a neutral one: the `add` it leaves behind is what makes `structure.ts` classify the base
|
|
79
|
+
// AGGREGATE, so a refusal here still de-names the symbol at its off-0 sites and drops the
|
|
80
|
+
// `volatile` (`gVolA = 0;` becomes `*(u16 *)&gVolA = 0;` the moment one refused walk stands beside
|
|
81
|
+
// it). The per-SITE classification that would answer it lives at `scalarGlobals`, three levels
|
|
82
|
+
// away and booked in `docs/level-tower.md`; a refusal here is the safe answer to the question this
|
|
83
|
+
// pass asks and not a cost-free one.
|
|
84
|
+
//
|
|
85
|
+
// WHICH GATE BOUNDS THE CORPUS PATH — `pnpm bench gates --pass offsetnames`, plus one run per real
|
|
86
|
+
// project (`--only <project>:`), counting a refusal per EVALUATION rather than per site (a row
|
|
87
|
+
// lifts more than once, which that command's own footer states):
|
|
88
|
+
//
|
|
89
|
+
// interior-offset 25 pokeemerald 13, synthetic `sbscope` 6, marioparty3 4, kleod 2
|
|
90
|
+
// access-behind-merge 1 kleod `ButtonConfigurationScreenInit`
|
|
91
|
+
// base-unsized 1 snowboardkids2 `func_80014440_15040`'s `gDefaultFontPalette+2`
|
|
92
|
+
// everything else 0
|
|
93
|
+
//
|
|
94
|
+
// Two of the three heuristics decide the whole path — `no-symbol-at-offset` fires nowhere. Of the
|
|
95
|
+
// SOUND rules exactly one refuses anything here, once: `access-behind-merge`, on the single site
|
|
96
|
+
// in `ButtonConfigurationScreenInit` whose address is merged from two arms. That row scores
|
|
97
|
+
// 107/212 whether the site is named or refused, and its fan moves 440 -> 446 against `origin/main`
|
|
98
|
+
// rather than the other way, because what a refusal leaves is arithmetic and the respell
|
|
99
|
+
// variations have something to apply to again. The rest — a code address at either end, an address
|
|
100
|
+
// the map spells two ways at either end, a width the map does not state, a width that disagrees
|
|
101
|
+
// with the access, a store onto a `const` name — refuse NOTHING here and are pinned by
|
|
102
|
+
// `offset-names.test.ts` alone.
|
|
103
|
+
import { constDisplacementOf } from '../ir/alias';
|
|
104
|
+
import { type Fn, type Op, type Value, defOpMap } from '../ir/core';
|
|
105
|
+
import { verify } from '../ir/verify';
|
|
106
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
107
|
+
import { dce } from '../pattern/engine';
|
|
108
|
+
import { type SymbolInfo, type SymbolMap, accessSignedness, lookupSymbol } from '../symbols';
|
|
109
|
+
import { type StampBehaviors, stampDeclaredVolatileUnder } from './declared-volatile';
|
|
110
|
+
|
|
111
|
+
/** One `add`/`sub` whose address the gates judge. Built only for an op that already resolves to a
|
|
112
|
+
* named base plus a NON-ZERO constant — a zero offset names the base itself, which the pool path
|
|
113
|
+
* spelled already, so there is nothing to judge. */
|
|
114
|
+
export interface OffsetAddress {
|
|
115
|
+
/** the base symbol's own entry, read at the FIRST address the map carries the name at */
|
|
116
|
+
readonly base: SymbolInfo;
|
|
117
|
+
/** the map carries the base name at more than one address, so `target` stands on an arbitrary
|
|
118
|
+
* pick — the one thing `base-address-ambiguous` is there to refuse */
|
|
119
|
+
readonly ambiguous: boolean;
|
|
120
|
+
/** the base `gaddr` carries the frontend's `code` attr (a function address) */
|
|
121
|
+
readonly baseIsCode: boolean;
|
|
122
|
+
/** the constant the machine added to the base address (signed) */
|
|
123
|
+
readonly offset: number;
|
|
124
|
+
/** what the map holds EXACTLY at `addr(base) + offset` */
|
|
125
|
+
readonly target: SymbolInfo | null;
|
|
126
|
+
/** the map holds several entries at that address and they do not all spell the same thing, so
|
|
127
|
+
* `target` is an arbitrary pick — the mirror of `ambiguous`, on the other end of the walk */
|
|
128
|
+
readonly targetAmbiguous: boolean;
|
|
129
|
+
/** some use of this value is the base operand of a store */
|
|
130
|
+
readonly written: boolean;
|
|
131
|
+
/** some use of this value is a successor's block argument, which is where the two access sets
|
|
132
|
+
* below stop being a complete answer — see `access-behind-merge` */
|
|
133
|
+
readonly crossesMerge: boolean;
|
|
134
|
+
/** the width of every OFF-0 access through this address, and the load signedness of the narrow
|
|
135
|
+
* ones — the same facts `rank-declare.ts`'s `bareGlobalAccessFacts` reads for a bare name, taken
|
|
136
|
+
* at this one address. An access at a non-zero offset is spelled through a cast that carries its
|
|
137
|
+
* own width, so it states nothing about the name's declaration and is not collected. Empty where
|
|
138
|
+
* the value is only an address: the next link of the walk, a call argument. */
|
|
139
|
+
readonly accessWidths: ReadonlySet<number>;
|
|
140
|
+
readonly accessSigns: ReadonlySet<boolean>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The access the walked-to name's own declaration produces: the width `gSym = v` writes, and the
|
|
144
|
+
* signedness `x = gSym` loads with. `null` where the map states no width — a name-only entry, or a
|
|
145
|
+
* struct whose interior the field spelling owns. A 4-byte unit has no signedness in the
|
|
146
|
+
* instruction, which is the normalization the access side makes too. */
|
|
147
|
+
function accessUnit(info: SymbolInfo): { width: number; signed: boolean } | null {
|
|
148
|
+
const [width, signed] =
|
|
149
|
+
info.shape === 'scalar'
|
|
150
|
+
? [info.size, info.signed === true]
|
|
151
|
+
: info.shape === 'array'
|
|
152
|
+
? [info.elemSize, info.elemSigned === true]
|
|
153
|
+
: info.shape === 'pointer'
|
|
154
|
+
? [4, false]
|
|
155
|
+
: [undefined, false];
|
|
156
|
+
return width === undefined ? null : { width, signed: accessSignedness(width, signed) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Everything the gates below read off a `SymbolInfo` beyond its identity: the name they spell,
|
|
160
|
+
* the kind `target-is-code` refuses, the `const` qualifier `const-target-store` refuses, and the
|
|
161
|
+
* access the declaration produces. Two entries at one address agreeing on all four make the pick
|
|
162
|
+
* between them immaterial. */
|
|
163
|
+
function sameAuthority(a: SymbolInfo, b: SymbolInfo): boolean {
|
|
164
|
+
const ua = accessUnit(a);
|
|
165
|
+
const ub = accessUnit(b);
|
|
166
|
+
return (
|
|
167
|
+
a.name === b.name &&
|
|
168
|
+
a.kind === b.kind &&
|
|
169
|
+
(a.const === true) === (b.const === true) &&
|
|
170
|
+
ua?.width === ub?.width &&
|
|
171
|
+
ua?.signed === ub?.signed
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The refusals. FIRST rejection is what `offsetNameRefusals` reports, so the order is the
|
|
176
|
+
* attribution order: what the map can say about the BASE, then what sits at the offset, then the
|
|
177
|
+
* ACCESS made through it, and last the store. The rules that are not `sound` are about OWNERSHIP
|
|
178
|
+
* and ABSENCE rather than correctness — an interior address is the field and element machinery's
|
|
179
|
+
* to spell, and where no symbol sits there the arithmetic is simply the only name available.
|
|
180
|
+
*
|
|
181
|
+
* THE ADDRESS IS NOT THE WHOLE QUESTION, and `target-unsized` and `access-unlike-target` are what
|
|
182
|
+
* say so. Naming an address the map holds is address-exact by construction; what a bare name ALSO
|
|
183
|
+
* decides is the instruction, because `gSym = v` writes whatever `gSym` was declared as. A
|
|
184
|
+
* halfword store walked onto a word-wide neighbour compiles to `str` where the target says `strh`
|
|
185
|
+
* — the same four bytes of address, four bytes written instead of two — and a name the map has no
|
|
186
|
+
* width for reaches declaration synthesis with no width authority at all (rank-declare.ts's
|
|
187
|
+
* `bareGlobalAccessFacts` keys off the pool-loaded `gaddr` defs of a lift this pass has not run
|
|
188
|
+
* on), so it falls back to `extern u32`. Both refuse here, where the map's own width is in hand. */
|
|
189
|
+
export const OFFSET_NAME_GATES: readonly Gate<OffsetAddress>[] = [
|
|
190
|
+
{
|
|
191
|
+
id: 'base-address-ambiguous',
|
|
192
|
+
why: 'the map carries the base name at more than one address, so the offset names nothing definite',
|
|
193
|
+
sound: true,
|
|
194
|
+
guardedBy: 'offset-names.test.ts: without `base-address-ambiguous` a doubly-mapped name picks one of its addresses',
|
|
195
|
+
rejects: (c) => c.ambiguous,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 'base-is-code',
|
|
199
|
+
why: 'an offset into a function is not an object C can name',
|
|
200
|
+
sound: true,
|
|
201
|
+
guardedBy: 'offset-names.test.ts: without `base-is-code` an offset into a function becomes a data name',
|
|
202
|
+
rejects: (c) => c.baseIsCode || c.base.kind === 'code',
|
|
203
|
+
},
|
|
204
|
+
// NOT sound, for the same reason `interior-offset` is not: the address it refuses is one the map
|
|
205
|
+
// holds a name at exactly, so the spelling it costs is address-identical and the access rules
|
|
206
|
+
// below check its width. What it buys is OWNERSHIP — an unsized base may span the cell, and
|
|
207
|
+
// naming the cell by a neighbour then re-attributes an access to an object the base's own
|
|
208
|
+
// spelling covers.
|
|
209
|
+
{
|
|
210
|
+
id: 'base-unsized',
|
|
211
|
+
why: 'the base declares no size, so nothing here can tell its own interior from a neighbour',
|
|
212
|
+
sound: false,
|
|
213
|
+
rejects: (c) => c.base.size === undefined,
|
|
214
|
+
},
|
|
215
|
+
// The `why` claims ownership rather than a handover, because the handover is not what happens:
|
|
216
|
+
// `pokeemerald:TrySetCantSelectMoveBattleScript`'s interior sites emit
|
|
217
|
+
// `((struct Elem7 *)((u32)&gBattleBufferB + 2))[i].field_0` — the element and field machinery
|
|
218
|
+
// indexes off the base, and the interior offset itself stays raw arithmetic.
|
|
219
|
+
{
|
|
220
|
+
id: 'interior-offset',
|
|
221
|
+
why: 'the address lands inside the base object, so the name at it labels a part of something the base already spells',
|
|
222
|
+
sound: false,
|
|
223
|
+
rejects: (c) => c.offset > 0 && c.offset < (c.base.size ?? 0),
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: 'no-symbol-at-offset',
|
|
227
|
+
why: 'no symbol sits exactly at the computed address, so the arithmetic is the only honest spelling',
|
|
228
|
+
sound: false,
|
|
229
|
+
rejects: (c) => c.target === null,
|
|
230
|
+
},
|
|
231
|
+
// The mirror of `base-address-ambiguous`, and it is not the same question asked twice: that one
|
|
232
|
+
// is one NAME the map places at two addresses, this one is one ADDRESS the map gives several
|
|
233
|
+
// names — aliases, or typed views of one RAM region, which `SymbolMap`'s `SymbolInfo[]` exists
|
|
234
|
+
// to carry. `lookupSymbol` answers with the first of them, so the width the two access rules
|
|
235
|
+
// check and the `const` the store rule checks would otherwise be read off an arbitrary pick.
|
|
236
|
+
{
|
|
237
|
+
id: 'target-address-ambiguous',
|
|
238
|
+
why: 'the map gives the walked-to address several names that do not agree, so which one this spells is arbitrary',
|
|
239
|
+
sound: true,
|
|
240
|
+
guardedBy:
|
|
241
|
+
'offset-names.test.ts: without `target-address-ambiguous` the walk spells one of two names at the address',
|
|
242
|
+
rejects: (c) => c.targetAmbiguous,
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: 'target-is-code',
|
|
246
|
+
why: 'a walked-to function address is a relocation this spelling cannot reproduce',
|
|
247
|
+
sound: true,
|
|
248
|
+
guardedBy: 'offset-names.test.ts: without `target-is-code` the walk names a function',
|
|
249
|
+
rejects: (c) => c.target?.kind === 'code',
|
|
250
|
+
},
|
|
251
|
+
// FIRST of the access rules, because it is the reason the ones below it can have nothing to
|
|
252
|
+
// judge. An address merged from two predecessors reaches its store through the successor's
|
|
253
|
+
// block parameter, and `offsetSites` counts accesses against the site's OWN value — so without
|
|
254
|
+
// this the rules below are asked about an empty set and pass, and the merge the structurer then
|
|
255
|
+
// collapses hands the bare name straight to the access they exist to check.
|
|
256
|
+
{
|
|
257
|
+
id: 'access-behind-merge',
|
|
258
|
+
why: 'a use of this address is a block argument, so the accesses made through it are not this census to count',
|
|
259
|
+
sound: true,
|
|
260
|
+
guardedBy: 'offset-names.test.ts: without `access-behind-merge` a word store behind a merge names a halfword cell',
|
|
261
|
+
rejects: (c) => c.crossesMerge,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: 'target-unsized',
|
|
265
|
+
why: 'the map states no width for the walked-to name, so a declaration synthesized for it would guess one',
|
|
266
|
+
sound: true,
|
|
267
|
+
guardedBy:
|
|
268
|
+
'offset-names.test.ts: without `target-unsized` a name-only neighbour is stored through at the guessed width',
|
|
269
|
+
rejects: (c) => c.target !== null && c.accessWidths.size > 0 && accessUnit(c.target) === null,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: 'access-unlike-target',
|
|
273
|
+
why: 'the access through this address is not the one the walked-to name declares, so the bare spelling is a different instruction',
|
|
274
|
+
sound: true,
|
|
275
|
+
guardedBy:
|
|
276
|
+
'offset-names.test.ts: without `access-unlike-target` a halfword store onto a word-wide name becomes `str`',
|
|
277
|
+
rejects: (c) => {
|
|
278
|
+
const unit = c.target === null ? null : accessUnit(c.target);
|
|
279
|
+
return (
|
|
280
|
+
unit !== null &&
|
|
281
|
+
([...c.accessWidths].some((w) => w !== unit.width) || [...c.accessSigns].some((s) => s !== unit.signed))
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
// How hard the constraint violation fails is per compiler, so the rule states the constraint
|
|
286
|
+
// rather than a verdict: the same `gRomTable[i] = 1;` is `warning: assignment of read-only
|
|
287
|
+
// location` and exit 0 on agbcc, and `cfe: Error: Change value for constant variable.` and exit 1
|
|
288
|
+
// on IDO 7.1.
|
|
289
|
+
{
|
|
290
|
+
id: 'const-target-store',
|
|
291
|
+
why: 'a store through a const-declared name is a constraint violation, and these compilers range from a warning to a hard error',
|
|
292
|
+
sound: true,
|
|
293
|
+
guardedBy: 'offset-names.test.ts: without `const-target-store` the store walks onto a const-declared name',
|
|
294
|
+
rejects: (c) => c.written && c.target?.const === true,
|
|
295
|
+
},
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
/** Every name the map carries, with the FIRST address it sits at and whether it sits at more than
|
|
299
|
+
* one. The reverse of the address-keyed map, which is the direction `addr(S) + K` needs and the
|
|
300
|
+
* only direction `SymbolMap` does not already have.
|
|
301
|
+
*
|
|
302
|
+
* `twice` is about two ADDRESSES, not two entries: `SymbolMap` holds a `SymbolInfo[]` per address
|
|
303
|
+
* precisely because one address legitimately carries several (aliases, typed views of one RAM
|
|
304
|
+
* region), and a name repeated inside one of those lists still says where it is. */
|
|
305
|
+
function addressesByName(symbols: SymbolMap): Map<string, { addr: number; info: SymbolInfo; twice: boolean }> {
|
|
306
|
+
const out = new Map<string, { addr: number; info: SymbolInfo; twice: boolean }>();
|
|
307
|
+
for (const [addr, infos] of symbols) {
|
|
308
|
+
for (const info of infos) {
|
|
309
|
+
const prev = out.get(info.name);
|
|
310
|
+
if (prev === undefined) {
|
|
311
|
+
out.set(info.name, { addr, info, twice: false });
|
|
312
|
+
} else if (prev.addr !== addr) {
|
|
313
|
+
out.set(info.name, { ...prev, twice: true });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** The named base an address value stands on, following `add`/`sub` chains through constants —
|
|
321
|
+
* the `adds r1,#12` walk is a chain of them, and each link is its own candidate. The chain is
|
|
322
|
+
* ir/alias.ts `constDisplacementOf`, the reading raise/structs.ts shares; it stops at anything
|
|
323
|
+
* else, block parameters included (they have no def, so a loop-carried pointer induction never
|
|
324
|
+
* resolves). */
|
|
325
|
+
function namedBase(v: Value, defOf: Map<Value, Op>): { sym: string; code: boolean; offset: number } | null {
|
|
326
|
+
const disp = constDisplacementOf(defOf, v);
|
|
327
|
+
const d = defOf.get(disp?.root ?? v);
|
|
328
|
+
return d?.opcode === 'gaddr'
|
|
329
|
+
? { sym: d.attrs.sym as string, code: d.attrs.code === true, offset: disp?.k ?? 0 }
|
|
330
|
+
: null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Every `add`/`sub` in `fn` that resolves to a named base plus a non-zero constant, with the
|
|
334
|
+
* context the gates judge. Shared by the rewrite and by `offsetNameRefusals` so the two cannot
|
|
335
|
+
* disagree about which sites exist. */
|
|
336
|
+
function offsetSites(fn: Fn, symbols: SymbolMap): { op: Op; sym: string; addr: OffsetAddress }[] {
|
|
337
|
+
const defOf = defOpMap(fn);
|
|
338
|
+
const byName = addressesByName(symbols);
|
|
339
|
+
const written = new Set<Value>();
|
|
340
|
+
// Through the address arithmetic, not just at it: `gRom[i] = v` stores through an `add` of the
|
|
341
|
+
// walked address and a scaled index, so the value the gates judge is one level up from the
|
|
342
|
+
// store's own base operand. Without the walk the const rule fires or not depending on whether an
|
|
343
|
+
// index happens to sit in between.
|
|
344
|
+
//
|
|
345
|
+
// A CONSTANT displacement ends the closure, because it is the link that names its own cell: on
|
|
346
|
+
// `&g + 12 + 12` the store goes through the second link, and descending past it would report
|
|
347
|
+
// the first as written too — a `const` two cells back would then refuse a site nothing stores
|
|
348
|
+
// through, and `offsetNameRefusals` would publish that as this site's reason.
|
|
349
|
+
const markWritten = (v: Value): void => {
|
|
350
|
+
if (written.has(v)) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
written.add(v);
|
|
354
|
+
const d = defOf.get(v);
|
|
355
|
+
if (d?.opcode !== 'add' && d?.opcode !== 'sub') {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (d.operands.some((o) => defOf.get(o)?.opcode === 'const')) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
d.operands.forEach(markWritten);
|
|
362
|
+
};
|
|
363
|
+
const access = new Map<Value, { widths: Set<number>; signs: Set<boolean> }>();
|
|
364
|
+
const accessOf = (v: Value) => access.get(v) ?? access.set(v, { widths: new Set(), signs: new Set() }).get(v)!;
|
|
365
|
+
const blockArgs = new Set<Value>();
|
|
366
|
+
for (const b of fn.blocks) {
|
|
367
|
+
for (const op of b.ops) {
|
|
368
|
+
for (const s of op.successors) {
|
|
369
|
+
s.args.forEach((a) => blockArgs.add(a));
|
|
370
|
+
}
|
|
371
|
+
if (op.opcode === 'store' || op.opcode === 'astore') {
|
|
372
|
+
markWritten(op.operands[0]);
|
|
373
|
+
}
|
|
374
|
+
if ((op.opcode === 'load' || op.opcode === 'store') && (op.attrs.off as number) === 0) {
|
|
375
|
+
const a = accessOf(op.operands[0]);
|
|
376
|
+
const w = op.attrs.width as number;
|
|
377
|
+
a.widths.add(w);
|
|
378
|
+
if (op.opcode === 'load') {
|
|
379
|
+
a.signs.add(accessSignedness(w, op.attrs.signed as boolean | undefined));
|
|
380
|
+
}
|
|
381
|
+
} else if (op.opcode === 'aload' || op.opcode === 'astore') {
|
|
382
|
+
const a = accessOf(op.operands[0]);
|
|
383
|
+
const w = op.attrs.elemSize as number;
|
|
384
|
+
a.widths.add(w);
|
|
385
|
+
if (op.opcode === 'aload') {
|
|
386
|
+
a.signs.add(accessSignedness(w, op.attrs.signed as boolean | undefined));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const out: { op: Op; sym: string; addr: OffsetAddress }[] = [];
|
|
392
|
+
for (const b of fn.blocks) {
|
|
393
|
+
for (const op of b.ops) {
|
|
394
|
+
if (op.opcode !== 'add' && op.opcode !== 'sub') {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const r = op.results[0];
|
|
398
|
+
if (r === undefined) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const base = namedBase(r, defOf);
|
|
402
|
+
if (base === null || base.offset === 0) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const at = byName.get(base.sym);
|
|
406
|
+
if (at === undefined) {
|
|
407
|
+
continue; // the base name reached the IR from somewhere other than this map
|
|
408
|
+
}
|
|
409
|
+
const entries = symbols.get(at.addr + base.offset) ?? [];
|
|
410
|
+
out.push({
|
|
411
|
+
op,
|
|
412
|
+
sym: base.sym,
|
|
413
|
+
addr: {
|
|
414
|
+
base: at.info,
|
|
415
|
+
ambiguous: at.twice,
|
|
416
|
+
baseIsCode: base.code,
|
|
417
|
+
offset: base.offset,
|
|
418
|
+
target: lookupSymbol(symbols, at.addr + base.offset),
|
|
419
|
+
targetAmbiguous: entries.length > 1 && entries.some((i) => !sameAuthority(i, entries[0])),
|
|
420
|
+
written: written.has(r),
|
|
421
|
+
crossesMerge: blockArgs.has(r),
|
|
422
|
+
accessWidths: access.get(r)?.widths ?? new Set(),
|
|
423
|
+
accessSigns: access.get(r)?.signs ?? new Set(),
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Rewrite every admitted site into the `gaddr` the map names, in place. Returns THE NAMES it
|
|
432
|
+
* spelled, in block order — what a caller has to be told, because a name this pass produced does
|
|
433
|
+
* not rest on the map alone the way a pool-loaded one does: it rests on `addr(base) + K` landing
|
|
434
|
+
* on the map entry it names. `DecompileResult.walkedNames` and the trace's `stage:offsetnames`
|
|
435
|
+
* carry it out; nothing else can tell from a result that a walk was named. */
|
|
436
|
+
export function nameOffsetAddresses(
|
|
437
|
+
fn: Fn,
|
|
438
|
+
symbols: SymbolMap,
|
|
439
|
+
behaviors: StampBehaviors,
|
|
440
|
+
gates: readonly Gate<OffsetAddress>[] = OFFSET_NAME_GATES,
|
|
441
|
+
): string[] {
|
|
442
|
+
// Every site is judged against the PRE-REWRITE defs, so a chain's later links resolve through
|
|
443
|
+
// the base they were lifted from and the answer does not depend on the order links are visited.
|
|
444
|
+
const admitted = offsetSites(fn, symbols).filter((s) => firstRejection(gates, s.addr) === null);
|
|
445
|
+
for (const s of admitted) {
|
|
446
|
+
s.op.opcode = 'gaddr';
|
|
447
|
+
s.op.operands = [];
|
|
448
|
+
s.op.attrs = { sym: s.addr.target!.name };
|
|
449
|
+
}
|
|
450
|
+
if (admitted.length > 0) {
|
|
451
|
+
// An access now names another object, so whether it reads one the map declares volatile is
|
|
452
|
+
// asked again before anything is deleted.
|
|
453
|
+
stampDeclaredVolatileUnder(fn, symbols, behaviors);
|
|
454
|
+
dce(fn); // the constants and the walked-off base the rewrite just orphaned
|
|
455
|
+
verify(fn);
|
|
456
|
+
}
|
|
457
|
+
return admitted.map((s) => s.addr.target!.name);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Which rule refused each offset site this function builds — one entry per SITE, in block order,
|
|
461
|
+
* as `[<base>+<offset>, reason]` with `null` where the site was named. The attribution
|
|
462
|
+
* `firstRejection` exists for. NOT on the shipped path: a caller instrumenting a refusal asks
|
|
463
|
+
* here instead of re-deriving the predicates.
|
|
464
|
+
*
|
|
465
|
+
* A LIST and not a map keyed by address: two `add` ops can resolve to one address — the arms of
|
|
466
|
+
* a merge do exactly that — and a map reports the pair as one entry, which under-reports the
|
|
467
|
+
* refusals the caller came to count. */
|
|
468
|
+
export function offsetNameRefusals(
|
|
469
|
+
fn: Fn,
|
|
470
|
+
symbols: SymbolMap,
|
|
471
|
+
gates: readonly Gate<OffsetAddress>[] = OFFSET_NAME_GATES,
|
|
472
|
+
): readonly (readonly [string, string | null])[] {
|
|
473
|
+
return offsetSites(fn, symbols).map(
|
|
474
|
+
(s) =>
|
|
475
|
+
[`${s.sym}${s.addr.offset < 0 ? '-' : '+'}${Math.abs(s.addr.offset)}`, firstRejection(gates, s.addr)] as const,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** THE CALLER-SIDE SEAM. The three drivers — pipeline's `runTower`, rank's `enumerateCandidates`,
|
|
480
|
+
* report's `traceTower` — call the pass THROUGH this record rather than through the binding above,
|
|
481
|
+
* which is what lets `pnpm bench gates --pass offsetnames` put a wrapped table in front of a real
|
|
482
|
+
* enumeration: a module-namespace binding is read-only and cannot be swapped, so a tabled pass
|
|
483
|
+
* whose only reachable name is its import is not censusable at all
|
|
484
|
+
* (`apps/benchmark/src/run/gate-census.ts`, WHAT PUTS A PASS IN THE REGISTRY).
|
|
485
|
+
*
|
|
486
|
+
* A record and not a pass LIST: the ordering question a list answers is already answered above,
|
|
487
|
+
* this pass having one seat. What a list also carries is the invariant that every driver picks a
|
|
488
|
+
* pass up, which three hand-written calls do not — so `offset-names.test.ts` supplies it by
|
|
489
|
+
* wrapping this record and asserting all three reach it. */
|
|
490
|
+
export const OFFSET_NAME_PASS: {
|
|
491
|
+
run: (fn: Fn, symbols: SymbolMap, behaviors: StampBehaviors) => string[];
|
|
492
|
+
} = {
|
|
493
|
+
run: (fn, symbols, behaviors) => nameOffsetAddresses(fn, symbols, behaviors),
|
|
494
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// asmlift — two argument registers that are one 64-bit PARAMETER.
|
|
2
|
+
//
|
|
3
|
+
// The ABI hands a 64-bit argument over in a register pair, so the frontend reads it as two
|
|
4
|
+
// live-ins and builds a `concat` from them. That `concat` is the evidence, and this pass turns it
|
|
5
|
+
// into the signature: one parameter of width 64 in place of the two words.
|
|
6
|
+
//
|
|
7
|
+
// WHY THE EVIDENCE IS THE `concat` AND NOT THE ADJACENCY. Two adjacent argument registers are two
|
|
8
|
+
// arguments far more often than they are one, so a pass keyed on position would rewrite the
|
|
9
|
+
// signature of most functions in the corpus. A `concat` is built at an ATOMIC site only — an ABI
|
|
10
|
+
// pair, or a carry-chained add — so it is a claim the machine made rather than one this pass
|
|
11
|
+
// invents, and the pair of registers it names is the pair the machine used.
|
|
12
|
+
//
|
|
13
|
+
// AND WHY EACH HALF MUST BE USED NOWHERE ELSE. A parameter also read on its own is not half of a
|
|
14
|
+
// 64-bit value; it is a word this function uses as a word, which is what a 64-bit argument passed
|
|
15
|
+
// to a helper and separately tested would look like. Fusing it would delete a use.
|
|
16
|
+
import { Fn, Op, Value } from '../ir/core';
|
|
17
|
+
import { intWidth } from '../ir/types';
|
|
18
|
+
import { type FnProto, declaredWidth } from '../proto';
|
|
19
|
+
import { RaiseUnsupportedError } from './errors';
|
|
20
|
+
|
|
21
|
+
/** Count every operand and edge-argument occurrence of each value. Occurrences, not sites: a value
|
|
22
|
+
* passed to `f(x, x)` is used twice, and a rule that said "used once" about it would be wrong. */
|
|
23
|
+
function useCounts(fn: Fn): Map<Value, number> {
|
|
24
|
+
const n = new Map<Value, number>();
|
|
25
|
+
const bump = (v: Value) => n.set(v, (n.get(v) ?? 0) + 1);
|
|
26
|
+
for (const b of fn.blocks) {
|
|
27
|
+
for (const op of b.ops) {
|
|
28
|
+
op.operands.forEach(bump);
|
|
29
|
+
for (const s of op.successors) {
|
|
30
|
+
s.args.forEach(bump);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return n;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Whether `op` is the `concat` of two entry parameters that ARRIVED as one 64-bit argument: the two
|
|
38
|
+
* consecutive argument slots, in slot order, each read by nothing else (`uses` counts occurrences).
|
|
39
|
+
*
|
|
40
|
+
* SLOT ORDER, because a pair built in any other order is a different value. `s64 f(s64 a, s64 b){
|
|
41
|
+
* return rot32(a) * b; }` hands `__muldi3` a's words swapped (`add r1,r5; add r0,r4` from r0:r1),
|
|
42
|
+
* so its `concat(r1, r0)` is not `a`, and a fusion that took it would print `a * b`. Consecutive,
|
|
43
|
+
* because the frontends mint every slot below the highest one read (`mintArgSlotHoles`), so two
|
|
44
|
+
* neighbouring parameters are two neighbouring slots — including r3 and the first stack word,
|
|
45
|
+
* which is where agbcc splits a 64-bit argument. */
|
|
46
|
+
export function isArgumentPair(entry: readonly Value[], op: Op, uses: (v: Value) => number): boolean {
|
|
47
|
+
if (op.opcode !== 'concat') {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const [lo, hi] = op.operands;
|
|
51
|
+
const at = entry.indexOf(lo);
|
|
52
|
+
return at >= 0 && entry[at + 1] === hi && uses(lo) === 1 && uses(hi) === 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Fuse each entry-parameter pair that a `concat` names into one 64-bit parameter. Returns whether
|
|
56
|
+
* anything changed. */
|
|
57
|
+
export function fuseParamPairs(fn: Fn): boolean {
|
|
58
|
+
const entry = fn.blocks[0];
|
|
59
|
+
if (entry.params.length < 2) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const uses = useCounts(fn);
|
|
63
|
+
let changed = false;
|
|
64
|
+
for (const b of fn.blocks) {
|
|
65
|
+
for (let i = 0; i < b.ops.length; i++) {
|
|
66
|
+
const op: Op = b.ops[i];
|
|
67
|
+
if (!isArgumentPair(entry.params, op, (v) => uses.get(v) ?? 0)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// The pair takes its first slot's position, which is its position in the ABI order the
|
|
71
|
+
// frontend already sorted these into — so the signature keeps the argument order the
|
|
72
|
+
// machine passed them in.
|
|
73
|
+
const whole = op.results[0];
|
|
74
|
+
entry.params.splice(entry.params.indexOf(op.operands[0]), 2, whole);
|
|
75
|
+
// The `concat`'s RESULT becomes the parameter — the same Value, so every existing use
|
|
76
|
+
// already points at it and nothing has to be rewritten. The op itself goes: a block
|
|
77
|
+
// parameter has no defining op, and leaving one would define the value twice.
|
|
78
|
+
b.ops.splice(i, 1);
|
|
79
|
+
i--;
|
|
80
|
+
changed = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return changed;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A FUNCTION DECLARED WITH AN INTEGER PARAMETER WIDER THAN A REGISTER, whose lift reads that
|
|
87
|
+
* parameter's words apart, refuses — on every frontend, since this reads only the entry block
|
|
88
|
+
* `fuseParamPairs` leaves. The return's twin is pipeline.ts `refuseNarrowReturnsOfWideDeclaration`.
|
|
89
|
+
*
|
|
90
|
+
* The pair is fused only where the lift built its `concat` (a helper call, a carry chain); a body
|
|
91
|
+
* that tests or combines the two words itself (`return a != 0;` is `or` of the halves on every
|
|
92
|
+
* compiler) leaves two word parameters, and the lift prints `s32 f(s32 a0, s32 a1)` against the
|
|
93
|
+
* caller's own `int f(long long a)` — a candidate the compiler rejects as a redeclaration. It is
|
|
94
|
+
* correct C under the ABI, and the wrong function for the declaration in hand. Counting is
|
|
95
|
+
* enough: every fused parameter is wider than a register and only `fuseParamPairs` makes one, so
|
|
96
|
+
* fewer of them than the declaration names means a declared one was left in halves. A
|
|
97
|
+
* declaration the caller does not state (a context's own entry is left out,
|
|
98
|
+
* proto-context.ts `withContextPrototypes`) asks nothing here. */
|
|
99
|
+
export function refuseSplitWideParams(fn: Fn, self: FnProto | undefined): void {
|
|
100
|
+
const declared = Array.isArray(self?.params) ? self.params : [];
|
|
101
|
+
const wide = declared.filter((t) => (declaredWidth(t) ?? 0) > 32);
|
|
102
|
+
if (wide.length === 0) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const fused = fn.blocks[0].params.filter((p) => (intWidth(p.type) ?? 0) > 32).length;
|
|
106
|
+
if (fused < wide.length) {
|
|
107
|
+
const at = declared.findIndex((t) => (declaredWidth(t) ?? 0) > 32);
|
|
108
|
+
throw new RaiseUnsupportedError(
|
|
109
|
+
`cannot lift '${fn.name}': its declaration takes parameter ${at + 1} as '${declared[at]}', and this lift ` +
|
|
110
|
+
'reads the two halves of a 64-bit value as two word parameters (a 64-bit parameter is one only where the ' +
|
|
111
|
+
'lift pairs its halves)',
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|