@asmlift/core 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +238 -164
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +341 -22
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +24 -6
- package/src/frontend/opaque.ts +31 -18
- package/src/frontend/ppc.ts +54 -7
- package/src/frontend/ssa.ts +632 -13
- package/src/frontend/thumb.ts +2786 -286
- package/src/ir/alias.ts +129 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +156 -27
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/argbase.ts +8 -2
- package/src/l3/ast.ts +464 -49
- package/src/l3/basecse.ts +709 -88
- package/src/l3/coalesce.ts +521 -66
- package/src/l3/dce.ts +54 -19
- package/src/l3/gates.ts +88 -0
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +113 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +649 -219
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +23 -4
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +236 -13
- package/src/pipeline.ts +206 -49
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +4 -3
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +44 -19
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +101 -16
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +215 -14
- package/src/raise/shortcircuit.ts +477 -79
- package/src/raise/struct-arrays.ts +21 -3
- package/src/raise/structs.ts +61 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1726 -251
- package/src/structure/analysis.ts +1516 -220
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2850 -533
- package/src/structure/switch-recover.ts +688 -147
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// L3 re-spelling lever: declare a pointer local that holds a NUMERIC address as pointing to
|
|
2
|
+
// volatile data (`volatile u16 *p = (u16 *)0x3000010;`).
|
|
3
|
+
//
|
|
4
|
+
// A numeric address has no declaration anywhere — the project maps a symbol's volatility, but a
|
|
5
|
+
// raw constant is exactly the case with no symbol — so whether the original source read it
|
|
6
|
+
// through a `volatile` pointer is not derivable from the asm. It is codegen-visible all the
|
|
7
|
+
// same: a volatile MEM is barred from motion and combination, which reorders the loop
|
|
8
|
+
// optimizer's pseudos and lands the register allocator on different homes (on the row this was
|
|
9
|
+
// built for, the counter is copied out of r0 so the loaded value can have it — the target's
|
|
10
|
+
// allocation). So the qualified spellings are emitted alongside the plain one — the all-locals
|
|
11
|
+
// form as rank.ts `/volatile`, per-local subsets as its `-name` variants — and the differ
|
|
12
|
+
// referees.
|
|
13
|
+
//
|
|
14
|
+
// SEMANTICS ARE PRESERVED BY CONSTRUCTION: `volatile` only RESTRICTS what a compiler may do
|
|
15
|
+
// with the accesses; every execution of the qualified spelling is an execution of the plain
|
|
16
|
+
// one. C89 also admits the assignment without a cast change — the qualifier is added on the
|
|
17
|
+
// pointee, and assignment may add pointee qualifiers.
|
|
18
|
+
//
|
|
19
|
+
// GATE: only a local of pointer type assigned a REMATERIALIZABLE address somewhere in the body
|
|
20
|
+
// (l3/ast.ts — any constant expression reading no variable and no memory, so a shift-encoded
|
|
21
|
+
// hardware base qualifies exactly like a pool word); a bare `0` is NULL, never an address. A value
|
|
22
|
+
// CONTAINING a global's address — `&gSym` at ANY depth: under a cast, inside interior-address
|
|
23
|
+
// arithmetic (`(u16 *)((u32)&gSym + 8)`) — VETOES the local, qualifying assignments on other paths
|
|
24
|
+
// notwithstanding, and the veto propagates through assignments to a FIXPOINT (`q` tainted,
|
|
25
|
+
// `p = q` taints `p`; conservatively, `p` assigned ANY expression mentioning a tainted name).
|
|
26
|
+
// The symbol map owns a declared global's volatility, and a mixed-feed local would read the
|
|
27
|
+
// mapped global through a volatile view the map never granted. No qualifying local ⇒ decline
|
|
28
|
+
// (null), so the lever never emits a duplicate of the primary.
|
|
29
|
+
import { addrConst, cellAddress, inRange } from './address';
|
|
30
|
+
import { type Expr, type SFn, type Stmt, mapExprChildren, rematerializableAddress, walkExprs } from './ast';
|
|
31
|
+
|
|
32
|
+
const exprHas = (e: Expr, pred: (x: Expr) => boolean): boolean => {
|
|
33
|
+
if (pred(e)) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
let hit = false;
|
|
37
|
+
mapExprChildren(e, (c) => {
|
|
38
|
+
hit ||= exprHas(c, pred);
|
|
39
|
+
return c;
|
|
40
|
+
});
|
|
41
|
+
return hit;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function collectAssigns(stmts: Stmt[], out: { name: string; value: Expr }[]): void {
|
|
45
|
+
for (const s of stmts) {
|
|
46
|
+
switch (s.k) {
|
|
47
|
+
case 'assign':
|
|
48
|
+
out.push({ name: s.name, value: s.value });
|
|
49
|
+
break;
|
|
50
|
+
case 'if':
|
|
51
|
+
collectAssigns(s.then, out);
|
|
52
|
+
collectAssigns(s.else, out);
|
|
53
|
+
break;
|
|
54
|
+
case 'while':
|
|
55
|
+
case 'dowhile':
|
|
56
|
+
collectAssigns(s.body, out);
|
|
57
|
+
break;
|
|
58
|
+
case 'for':
|
|
59
|
+
collectAssigns([s.init, s.inc], out);
|
|
60
|
+
collectAssigns(s.body, out);
|
|
61
|
+
break;
|
|
62
|
+
case 'switch':
|
|
63
|
+
for (const c of s.cases) {
|
|
64
|
+
collectAssigns(c.body, out);
|
|
65
|
+
}
|
|
66
|
+
collectAssigns(s.default ?? [], out);
|
|
67
|
+
break;
|
|
68
|
+
default:
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** How many of this tree's `volatile` claims land on a DEVICE REGISTER — the gate on rank.ts's
|
|
75
|
+
* volatility tie-break, so it is counted here beside the lever that mints the claims.
|
|
76
|
+
*
|
|
77
|
+
* Two spellings assert one: a `volatile` pointer cast over a numeric address (what
|
|
78
|
+
* l3/inlinebase.ts leaves at each use and what l3/volstore.ts mints at a device store), and a
|
|
79
|
+
* pointee-volatile pointer local, which asserts it at whichever numeric address feeds the local.
|
|
80
|
+
* A `volatile` SCALAR local (l3/volatileval.ts) asserts nothing about an address — it is a stack
|
|
81
|
+
* slot — so it is not counted.
|
|
82
|
+
*
|
|
83
|
+
* A claim under a SUBSCRIPT is placed by the whole cell and not by the cast's own operand, which
|
|
84
|
+
* is the disagreement l3/address.ts warns about: `((volatile s32 *)0x03FFFFF0)[8]` denotes
|
|
85
|
+
* BG0HOFS, and asking the base alone reports EWRAM and counts nothing. Either reading landing in
|
|
86
|
+
* the window counts it, so a runtime subscript over an in-window base still does.
|
|
87
|
+
*
|
|
88
|
+
* Nothing outside the window counts. A qualifier on ordinary memory is a claim about the source
|
|
89
|
+
* the differ cannot referee and the range cannot support, and preferring it corpus-wide buys one
|
|
90
|
+
* honest MMIO spelling at the price of many false ones. */
|
|
91
|
+
export function deviceVolatileClaims(sfn: SFn, window?: readonly [number, number]): number {
|
|
92
|
+
if (window === undefined) {
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
const inWindow = (e: Expr) => inRange(addrConst(e), window);
|
|
96
|
+
let n = 0;
|
|
97
|
+
const underSubscript = new Set<Expr>();
|
|
98
|
+
for (const e of walkExprs(sfn.body)) {
|
|
99
|
+
if (e.k === 'index' && e.base.k === 'cast') {
|
|
100
|
+
underSubscript.add(e.base);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const assigns: { name: string; value: Expr }[] = [];
|
|
104
|
+
collectAssigns(sfn.body, assigns);
|
|
105
|
+
for (const l of sfn.locals) {
|
|
106
|
+
if (l.type.kind === 'ptr' && (l.pointeeVolatile !== undefined || l.volatile !== undefined)) {
|
|
107
|
+
n += assigns.filter((a) => a.name === l.name && inWindow(a.value)).length;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const e of walkExprs(sfn.body)) {
|
|
111
|
+
if (e.k === 'index' && e.base.k === 'cast' && e.base.volatile === true) {
|
|
112
|
+
if (inRange(cellAddress(e), window) || inWindow(e.base.e)) {
|
|
113
|
+
n++;
|
|
114
|
+
}
|
|
115
|
+
} else if (e.k === 'cast' && e.volatile === true && !underSubscript.has(e) && inWindow(e.e)) {
|
|
116
|
+
n++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return n;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The locals the lever would qualify — the per-local SUBSET enumeration's input (below):
|
|
123
|
+
* which pointers the original declared volatile is per-pointer knowledge the asm does not
|
|
124
|
+
* carry (an MMIO block and a plain RAM table can sit side by side, and qualifying the table
|
|
125
|
+
* blocks the read collapse its region wants), so each non-empty subset is its own candidate
|
|
126
|
+
* when few enough locals qualify, and the differ referees. */
|
|
127
|
+
function volatileEligibleLocals(sfn: SFn): string[] {
|
|
128
|
+
return sfn.locals.filter(eligibility(sfn)).map((l) => l.name);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** the value-side VETO of the header's GATE: `&gSym` at any depth, under a cast or inside interior
|
|
132
|
+
* address arithmetic. Named because the taint seed and `volatileEligibleValue` are the same test. */
|
|
133
|
+
const feedsSymbolAddress = (e: Expr): boolean => exprHas(e, (x) => x.k === 'addr');
|
|
134
|
+
|
|
135
|
+
/** Whether a pointer local fed exactly this value, and nothing else, would qualify — the header's
|
|
136
|
+
* GATE at its one-assignment case, where the taint fixpoint is its own seed.
|
|
137
|
+
*
|
|
138
|
+
* Exported because a caller asking "would the spelling I am REPLACING have carried the qualifier?"
|
|
139
|
+
* has to ask this model rather than re-derive it. The const-vs-symbol answer is this file's: a
|
|
140
|
+
* numeric address has no declaration anywhere, a symbol's volatility is the map's, and a rule that
|
|
141
|
+
* hard-codes that split drifts the moment the veto moves (l3/homesplit.ts). */
|
|
142
|
+
export const volatileEligibleValue = (e: Expr): boolean => rematerializableAddress(e) && !feedsSymbolAddress(e);
|
|
143
|
+
|
|
144
|
+
/** the shared eligibility predicate (the GATE in the header) for one function's locals */
|
|
145
|
+
function eligibility(sfn: SFn): (l: SFn['locals'][number]) => boolean {
|
|
146
|
+
const assigns: { name: string; value: Expr }[] = [];
|
|
147
|
+
collectAssigns(sfn.body, assigns);
|
|
148
|
+
const numericFed = new Set<string>();
|
|
149
|
+
const tainted = new Set<string>();
|
|
150
|
+
for (const a of assigns) {
|
|
151
|
+
if (rematerializableAddress(a.value)) {
|
|
152
|
+
numericFed.add(a.name);
|
|
153
|
+
}
|
|
154
|
+
if (feedsSymbolAddress(a.value)) {
|
|
155
|
+
tainted.add(a.name);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (let grew = true; grew;) {
|
|
159
|
+
grew = false;
|
|
160
|
+
for (const a of assigns) {
|
|
161
|
+
if (!tainted.has(a.name) && exprHas(a.value, (x) => x.k === 'var' && tainted.has(x.name))) {
|
|
162
|
+
tainted.add(a.name);
|
|
163
|
+
grew = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return (l) =>
|
|
168
|
+
l.type.kind === 'ptr' &&
|
|
169
|
+
l.volatile === undefined &&
|
|
170
|
+
l.pointeeVolatile === undefined &&
|
|
171
|
+
numericFed.has(l.name) &&
|
|
172
|
+
!tainted.has(l.name);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The proper non-empty SUBSETS of the qualifying locals (within `within`, when given) as
|
|
176
|
+
* alternative outputs — one candidate per subset, labeled by its member names. Empty above
|
|
177
|
+
* three qualifiers: the arm is capped at 6 extra spellings, and the all-qualifiers form is the
|
|
178
|
+
* plain lever's own candidate. */
|
|
179
|
+
export function volatileSubsetCandidates(sfn: SFn, within?: ReadonlySet<string>): { merged: string; sfn: SFn }[] {
|
|
180
|
+
const elig = volatileEligibleLocals(sfn).filter((n) => within === undefined || within.has(n));
|
|
181
|
+
if (elig.length < 2 || elig.length > 3) {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
const out: { merged: string; sfn: SFn }[] = [];
|
|
185
|
+
for (let mask = 1; mask < (1 << elig.length) - 1; mask++) {
|
|
186
|
+
const subset = elig.filter((_, i) => (mask & (1 << i)) !== 0);
|
|
187
|
+
const r = volatilePtrLocals(sfn, new Set(subset));
|
|
188
|
+
if (r) {
|
|
189
|
+
out.push({ merged: subset.join('-'), sfn: r });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The `/volatile` candidate, or null when no local qualifies. Read-only: returns a fresh SFn
|
|
196
|
+
* sharing the (unmodified) body. `only` narrows the lever to the named locals — a /volatile
|
|
197
|
+
* PRODUCT (rank.ts) qualifies just the locals its first lever centres on (kept walk bases,
|
|
198
|
+
* created hoists), so the product never degenerates into a general /volatile composition over
|
|
199
|
+
* the function's other locals — and the subset enumeration re-uses the same door. */
|
|
200
|
+
export function volatilePtrLocals(sfn: SFn, only?: ReadonlySet<string>): SFn | null {
|
|
201
|
+
const eligible = eligibility(sfn);
|
|
202
|
+
const qualifies = (l: SFn['locals'][number]): boolean => eligible(l) && (only === undefined || only.has(l.name));
|
|
203
|
+
if (!sfn.locals.some(qualifies)) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return { ...sfn, locals: sfn.locals.map((l) => (qualifies(l) ? { ...l, pointeeVolatile: true } : l)) };
|
|
207
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// L3 re-spelling lever: declare a stack-homed scalar local `volatile` (`volatile u16 sp0;`).
|
|
2
|
+
//
|
|
3
|
+
// `volatile` on a scalar VALUE local FORCES the value into memory: agbcc's allocator is
|
|
4
|
+
// otherwise free to keep it in a callee-saved register across a call, and no other qualifier
|
|
5
|
+
// or type spelling takes that freedom away. Whether the source spelled it is not derivable from
|
|
6
|
+
// the asm — a slot-homed value can equally come from an address-taken local or from plain
|
|
7
|
+
// register pressure — so both spellings are emitted and the differ referees, exactly as the
|
|
8
|
+
// sibling pointee lever (volatileptr.ts) does for a numeric-address pointer.
|
|
9
|
+
//
|
|
10
|
+
// SEMANTICS ARE PRESERVED BY CONSTRUCTION, as they are there: `volatile` only RESTRICTS what a
|
|
11
|
+
// compiler may do with the accesses, so every execution of the qualified spelling is an
|
|
12
|
+
// execution of the plain one. The object is a function local whose address does not escape,
|
|
13
|
+
// so nothing outside the function can observe the difference at all.
|
|
14
|
+
//
|
|
15
|
+
// ENVELOPE — narrower than "a source-level volatile scalar": an `laddr`-recovered frame object,
|
|
16
|
+
// which under Thumb is a SUB-WORD one (see the `frame` note on SFn.locals). A `volatile s32`
|
|
17
|
+
// local spills straight to `[sp,#imm]` and is recovered as an ordinary value with no local of
|
|
18
|
+
// its own, so this lever cannot reach it. The flag is the set of slots asmlift PROVED, not the
|
|
19
|
+
// set of values a source could have qualified.
|
|
20
|
+
//
|
|
21
|
+
// GATE — VOL_SLOT_GATES holds the rules; the argument behind each is here, where there is room:
|
|
22
|
+
//
|
|
23
|
+
// • The frame record is what gives the qualifier a home to force. Qualifying a register-homed
|
|
24
|
+
// value would enumerate a spelling no source that produced this asm could have had.
|
|
25
|
+
// • SCALAR, because a pointer local declares as `volatile u16 * p` — one prefix, two meanings —
|
|
26
|
+
// and cfamily.ts spells that for `pointeeVolatile`. On a pointer the qualifier would say
|
|
27
|
+
// something about the pointee the tree never claimed.
|
|
28
|
+
// • `volatile` already set is the frontend's stamp for an object whose address was PUBLISHED
|
|
29
|
+
// to memory (frontend/thumb.ts stamps it there, on `published`, not on any escape) — the
|
|
30
|
+
// candidate would duplicate the primary.
|
|
31
|
+
// • An address-TAKEN local already has a memory home in every spelling, so there is no home
|
|
32
|
+
// left for the qualifier to move: EReader_Reset's slot read and written through a pointer
|
|
33
|
+
// local compiles to IDENTICAL assembly with the qualifier and without (agbcc 2.9-arm-000512,
|
|
34
|
+
// `-O2 -mthumb-interwork -Wimplicit -fhex-asm -fprologue-bugfix`). What it can still do
|
|
35
|
+
// there is stop reads collapsing, which frontend/thumb.ts measured turning a byte-exact
|
|
36
|
+
// candidate into a four-instruction nonmatch.
|
|
37
|
+
// • ACCESS-SET EQUALITY is what makes the qualified spelling honest, and it is the one
|
|
38
|
+
// condition the tree alone cannot answer. `volatile` asserts that every access written is an
|
|
39
|
+
// access performed; the passes between the asm and here break that in both directions and
|
|
40
|
+
// leave no trace. eliminateDeadStores drops a store to a local it can see is dead — licensed
|
|
41
|
+
// by the ABSENCE of the flag this lever adds — so a source that stores the slot twice
|
|
42
|
+
// arrives with one store. And the structurer emits one C read per USE rather than per
|
|
43
|
+
// machine load, so one `ldrh` feeding two uses arrives as two reads. Either way the
|
|
44
|
+
// qualified spelling would declare an access set asmlift did not preserve, so both DECLINE.
|
|
45
|
+
//
|
|
46
|
+
// No qualifying local ⇒ decline (null), so the lever never emits a duplicate of the primary.
|
|
47
|
+
//
|
|
48
|
+
// ALL ELIGIBLE SLOTS OR NONE, where the sibling pointee lever enumerates per-local SUBSETS on the
|
|
49
|
+
// argument that volatility is per-pointer knowledge. It is per-slot knowledge here too; the
|
|
50
|
+
// subsets are simply uninhabited — across the 311 agbcc benchmark rows no function reaches this
|
|
51
|
+
// gate with two eligible slots. How many reach it at all depends on the sweep's configuration, so
|
|
52
|
+
// the subset question is the one to quote: two rows with the callee arities declared, one without.
|
|
53
|
+
import type { SFn } from './ast';
|
|
54
|
+
import { type Gate, firstRejection } from './gates';
|
|
55
|
+
import { localMentions, readsOf } from './mentions';
|
|
56
|
+
|
|
57
|
+
/** One local as the gates read it. */
|
|
58
|
+
interface SlotCtx {
|
|
59
|
+
hasFrame: boolean;
|
|
60
|
+
isScalar: boolean;
|
|
61
|
+
alreadyVolatile: boolean;
|
|
62
|
+
addrTaken: number;
|
|
63
|
+
/** the tree's reads and writes are the machine's loads and stores */
|
|
64
|
+
accessSetKept: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const VOL_SLOT_GATES: readonly Gate<SlotCtx>[] = [
|
|
68
|
+
{
|
|
69
|
+
id: 'no-frame',
|
|
70
|
+
why: 'the frame record is the memory home the qualifier has to force',
|
|
71
|
+
sound: false,
|
|
72
|
+
rejects: (c) => !c.hasFrame,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'non-scalar',
|
|
76
|
+
why: 'on a pointer declarator the one `volatile` prefix binds to the pointee, not the object',
|
|
77
|
+
sound: true,
|
|
78
|
+
guardedBy: 'volatileval.test.ts: a non-scalar frame local never qualifies',
|
|
79
|
+
rejects: (c) => !c.isScalar,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'already-volatile',
|
|
83
|
+
why: 'the primary already declares it, so the candidate would duplicate it',
|
|
84
|
+
sound: false,
|
|
85
|
+
rejects: (c) => c.alreadyVolatile,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'addr-taken',
|
|
89
|
+
why: 'an address-taken local already has a memory home, so there is none left to force',
|
|
90
|
+
sound: false,
|
|
91
|
+
rejects: (c) => c.addrTaken > 0,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'access-set',
|
|
95
|
+
why: 'the qualifier asserts every access written is performed, so the tree’s must be the machine’s',
|
|
96
|
+
sound: true,
|
|
97
|
+
guardedBy: 'volatileval.test.ts: a store the tree no longer carries declines',
|
|
98
|
+
rejects: (c) => !c.accessSetKept,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
/** the shared eligibility predicate (VOL_SLOT_GATES) for one function's locals */
|
|
103
|
+
function eligibility(sfn: SFn): (l: SFn['locals'][number]) => boolean {
|
|
104
|
+
const uses = localMentions(sfn);
|
|
105
|
+
return (l) => {
|
|
106
|
+
const u = uses.get(l.name);
|
|
107
|
+
if (u === undefined) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return (
|
|
111
|
+
firstRejection(VOL_SLOT_GATES, {
|
|
112
|
+
hasFrame: l.frame !== undefined,
|
|
113
|
+
isScalar: l.type.kind === 'int',
|
|
114
|
+
alreadyVolatile: l.volatile !== undefined || l.pointeeVolatile !== undefined,
|
|
115
|
+
addrTaken: u.addrTaken,
|
|
116
|
+
accessSetKept: readsOf(u) === l.frame?.loads && u.assigns === l.frame?.stores,
|
|
117
|
+
}) === null
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The `/vol-slot` candidate, or null when no local qualifies. Read-only: returns a fresh SFn
|
|
123
|
+
* sharing the (unmodified) body. */
|
|
124
|
+
export function volatileValueLocals(sfn: SFn): SFn | null {
|
|
125
|
+
const qualifies = eligibility(sfn);
|
|
126
|
+
if (!sfn.locals.some(qualifies)) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return { ...sfn, locals: sfn.locals.map((l) => (qualifies(l) ? { ...l, volatile: true as const } : l)) };
|
|
130
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// L3 re-spelling lever: pin a STORE at a fixed DEVICE-REGISTER address `volatile`
|
|
2
|
+
// (`*(volatile s32 *)0x40000d4 = x` rather than `*(s32 *)0x40000d4 = x`).
|
|
3
|
+
//
|
|
4
|
+
// A numeric address has no declaration anywhere, so whether the original source wrote through a
|
|
5
|
+
// `volatile` lvalue is not derivable from the asm — the same gap l3/volatileptr.ts's header
|
|
6
|
+
// argues. That lever answers it for a pointer LOCAL holding the address; this one answers it
|
|
7
|
+
// where there is no local at all, which is the spelling a `#define REG(x) *(vu32 *)(x)` macro
|
|
8
|
+
// produces and the shape structure.ts leaves when the address re-materializes at each use.
|
|
9
|
+
//
|
|
10
|
+
// IT IS CODEGEN-VISIBLE, and the mechanism is one line of the compiler. agbcc's loop optimizer
|
|
11
|
+
// runs `load_mems` (gcc/loop.c:8877) between the invariant hoist and strength reduction; it
|
|
12
|
+
// PROMOTES a loop-invariant MEM into a register for the loop's duration and writes it back once
|
|
13
|
+
// after the exit, and it stands down on exactly two conditions (gcc/loop.c:8934):
|
|
14
|
+
//
|
|
15
|
+
// if (MEM_VOLATILE_P (mem) || invariant_p (XEXP (mem, 0)) != 1)
|
|
16
|
+
// loop_mems[i].optimize = 0;
|
|
17
|
+
//
|
|
18
|
+
// So an unpinned store to a fixed address inside a loop LEAVES THE LOOP — verified in both
|
|
19
|
+
// directions on synthetic:dmafill (unpinned: one store in the body, three after the `ble`, two
|
|
20
|
+
// pool words; pinned: four stores per iteration, four pool words). A decompiler that never spells
|
|
21
|
+
// the qualifier can therefore never reproduce the loop body of any function that drives a device
|
|
22
|
+
// register in a loop, whatever else it gets right.
|
|
23
|
+
//
|
|
24
|
+
// SEMANTICS ARE PRESERVED BY CONSTRUCTION: `volatile` only RESTRICTS what a compiler may do with
|
|
25
|
+
// an access; every execution of the qualified spelling is an execution of the plain one. The
|
|
26
|
+
// qualifier goes on the POINTEE of the deref cast, which is where C puts it and what
|
|
27
|
+
// backend/cfamily.ts already prints for the casts l3/inlinebase.ts mints.
|
|
28
|
+
//
|
|
29
|
+
// GATE (VOL_STORE_GATES, read once per access). THE ADMISSION RULE IS ONE: the access's WHOLE
|
|
30
|
+
// address must be a compile-time constant — a base that rematerializes to a numeric address plus a
|
|
31
|
+
// constant subscript, no `lead` — lying inside a device-register window the target declares
|
|
32
|
+
// (TargetDescription.capabilities.deviceRegisters); and the access must not already be qualified.
|
|
33
|
+
// The first three entries PARTITION the first half rather than adding to it: `inRange` is false
|
|
34
|
+
// when there is no window OR no constant OR the wrong constant, so ablating `no-window` or
|
|
35
|
+
// `non-const-address` changes no admission anywhere in the ctx space. What they buy is a refusal
|
|
36
|
+
// that says WHICH half is missing — "this target declares no device page" and "this address is
|
|
37
|
+
// IWRAM" are different facts about a row, and over the corpus they are 283 and 711 of the 1011
|
|
38
|
+
// refusals against `outside-window`'s 17. Nothing qualifying ⇒ decline (null), never a duplicate
|
|
39
|
+
// of the primary.
|
|
40
|
+
//
|
|
41
|
+
// THE WINDOW IS A REACH GATE, PRICED — not a soundness one, which is why it is `sound: false`.
|
|
42
|
+
// A `volatile` qualifier only restricts the compiler, so widening the range can never make a
|
|
43
|
+
// candidate WRONG; what it would make is a claim about ordinary memory that the target denies
|
|
44
|
+
// (IWRAM, EWRAM, palette, VRAM and OAM are memory a source does not qualify — target.ts) and that
|
|
45
|
+
// the differ can only referee by luck. Measured by running the lever twice, once with the declared
|
|
46
|
+
// range and once with one admitting every constant address: 8 trees over 7 rows carry a
|
|
47
|
+
// const-address store the window excludes, and the fan moves on two of them —
|
|
48
|
+
// `synthetic:readarm` 6 candidates → 8 (the extra one TIES its match at 0) and
|
|
49
|
+
// `synthetic:fieldbase` 14 → 20 (best extra 22, losing to 0). No row's score or outcome moves
|
|
50
|
+
// either way. So the range buys candidate discipline and the honesty of the claim, and no match
|
|
51
|
+
// rests on it.
|
|
52
|
+
//
|
|
53
|
+
// WHAT THE WINDOW IS NOT: A CLAIM THAT NOTHING ELSE MAY QUALIFY ORDINARY MEMORY. `/volatile`
|
|
54
|
+
// (l3/volatileptr.ts) does exactly that, and a sweep over 834 corpus trees finds it qualifying an
|
|
55
|
+
// address outside this window on 21 (tree, local, address) pairs, 16 of them on agbcc — including
|
|
56
|
+
// `kleod:WritePaletteColor:agbcc`, a published byte-exact MATCH whose winning source contains
|
|
57
|
+
// `*(volatile s32 *)50351492 = v2 + 5;` at 0x03004D84, which is IWRAM — the same minted-cast form
|
|
58
|
+
// this lever produces, at an address this lever's window refuses.
|
|
59
|
+
//
|
|
60
|
+
// THE TWO ARE SEPARATED BY THEIR DERIVATION AND NOT BY WHAT THEY EMIT, which is the trap in
|
|
61
|
+
// reading that spelling as a contradiction. `/inlinebase` alone mints no qualifier at all —
|
|
62
|
+
// enumerated on that row, `unsigned/inlinebase` carries zero `volatile` casts and
|
|
63
|
+
// `unsigned/inlinebase/volatile` carries three. The qualifier comes from `/volatile`, which put it
|
|
64
|
+
// on a pointer LOCAL the asm shows the compiler re-materializing rather than keeping; inlining
|
|
65
|
+
// then carries that codegen fact onto each cast it leaves behind. This lever runs where no such
|
|
66
|
+
// local ever existed, so its only input is the number — and outside a range the target has
|
|
67
|
+
// declared, a number supports nothing. The window is where THIS lever's evidence runs out, not
|
|
68
|
+
// where the target's permission does.
|
|
69
|
+
//
|
|
70
|
+
// (So the two are not foldable on the window, and the fold is not free: adopting it for `/volatile`
|
|
71
|
+
// would delete the WritePaletteColor spelling. `deviceVolatileClaims` in volatileptr.ts already
|
|
72
|
+
// unifies the COUNT side, which is the half where one answer really is enough. Nor do they
|
|
73
|
+
// COMPOSE over the tree's OWN locals: that pairing would qualify a function's existing pointer-local
|
|
74
|
+
// homes and its raw-constant stores together, and over 834 corpus trees both levers fire on ONE —
|
|
75
|
+
// `kleod:SetupBG3WindowOverlay:agbcc`, which neither decompiler scores — and under two DIFFERENT
|
|
76
|
+
// published classifications: asmlift `noncompile` (agbcc rejects its call to `m4aSoundVSyncOff`),
|
|
77
|
+
// m2c `declined`. A pairing whose whole reach is a row asmlift cannot compile is one a row has yet
|
|
78
|
+
// to demand.
|
|
79
|
+
//
|
|
80
|
+
// A lever that MINTS the locals is a different question with a different answer, and `rank.ts`
|
|
81
|
+
// pairs this pass with one: `/regionbase` homes the regions holding two or more direct uses of a
|
|
82
|
+
// device base and leaves every other spelling of the same address inline, so both qualifiers have
|
|
83
|
+
// something to claim in one function. `synthetic:dmascope` is that row.)
|
|
84
|
+
//
|
|
85
|
+
// SCOPE: STORES only. A device READ is a different question with a different answer — the idiom
|
|
86
|
+
// fold's DCE drops a use-less device load outright (synthetic:dmaback), so a read that survives to
|
|
87
|
+
// L3 is one whose value the function consumes, and whether THAT may be CSEd is the question
|
|
88
|
+
// `/reread-globals` referees as a structuring axis. The price of pinning one is real and is
|
|
89
|
+
// measured on `synthetic:ucmp:agbcc`, a byte-exact match whose loop test READS 0x3001048: qualify
|
|
90
|
+
// that read and the row scores 15. This lever does not reach it — ucmp's stores go through a
|
|
91
|
+
// runtime address (`*(u8 *)(v1 + 0x3002000)`), so it declines there on `non-const-address`, in
|
|
92
|
+
// both configurations and with any window. No row demands the read spelling, and a lever with no
|
|
93
|
+
// inhabitant is what "earn the level" forbids.
|
|
94
|
+
import { type IrType, T, scalarTypeForAccess } from '../ir/types';
|
|
95
|
+
import { cellAddress, inRange } from './address';
|
|
96
|
+
import { type Expr, type SFn, type Stmt, stmtChildren } from './ast';
|
|
97
|
+
import { type Gate, firstRejection } from './gates';
|
|
98
|
+
|
|
99
|
+
/** One STORE lvalue as the gates read it. */
|
|
100
|
+
interface AccessCtx {
|
|
101
|
+
/** the target declares a device-register range at all */
|
|
102
|
+
hasWindow: boolean;
|
|
103
|
+
/** the access's whole address as a compile-time constant, or null */
|
|
104
|
+
address: number | null;
|
|
105
|
+
/** that address lies inside the declared window */
|
|
106
|
+
inWindow: boolean;
|
|
107
|
+
/** the lvalue already carries a `volatile` qualifier */
|
|
108
|
+
qualified: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// One rule at three resolutions, most general first: `inRange` fails for all three reasons, so
|
|
112
|
+
// only `outside-window` and `already-qualified` change an admission. See the header's GATE
|
|
113
|
+
// paragraph — what the first two decide is the REASON a row is refused, not whether it is.
|
|
114
|
+
export const VOL_STORE_GATES: readonly Gate<AccessCtx>[] = [
|
|
115
|
+
{
|
|
116
|
+
id: 'no-window',
|
|
117
|
+
why: 'a target that declares no device range has no address this lever may call volatile',
|
|
118
|
+
sound: false,
|
|
119
|
+
rejects: (c) => !c.hasWindow,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: 'non-const-address',
|
|
123
|
+
why: 'a runtime address names no cell, so nothing here can say which object it reaches',
|
|
124
|
+
sound: false,
|
|
125
|
+
rejects: (c) => c.address === null,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
// NOT "the target denies this address may be volatile" — `/volatile` qualifies IWRAM on eleven
|
|
129
|
+
// agbcc rows, one of them a published match, and it is right to: see the header's last
|
|
130
|
+
// paragraph. This gate is about what THIS lever has evidence for, which is the address alone.
|
|
131
|
+
id: 'outside-window',
|
|
132
|
+
why: 'the address is the only evidence this lever has, and outside the window it supports nothing',
|
|
133
|
+
sound: false,
|
|
134
|
+
rejects: (c) => !c.inWindow,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'already-qualified',
|
|
138
|
+
why: 'a second qualifier would nest a cast over a spelling that already asserts volatility',
|
|
139
|
+
sound: false,
|
|
140
|
+
rejects: (c) => c.qualified,
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
/** Does this base already assert volatility — a `volatile` cast at any depth of the cast chain? */
|
|
145
|
+
const qualifiedBase = (e: Expr): boolean => e.k === 'cast' && (e.volatile === true || qualifiedBase(e.e));
|
|
146
|
+
|
|
147
|
+
/** The pointee the deref cast carries: the access's own scalar type. */
|
|
148
|
+
const pointee = (ix: Extract<Expr, { k: 'index' }>): IrType => scalarTypeForAccess(ix.width, ix.signed);
|
|
149
|
+
|
|
150
|
+
/** What the gates read about one indexed access: its cell address, and whether that address is
|
|
151
|
+
* evidence — a window to place it in, the placement itself, and whether the base already carries
|
|
152
|
+
* the qualifier. */
|
|
153
|
+
function accessCtx(lval: Extract<Expr, { k: 'index' }>, window: readonly [number, number] | undefined): AccessCtx {
|
|
154
|
+
const address = cellAddress(lval);
|
|
155
|
+
return {
|
|
156
|
+
hasWindow: window !== undefined,
|
|
157
|
+
address,
|
|
158
|
+
inWindow: inRange(address, window),
|
|
159
|
+
qualified: qualifiedBase(lval.base),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The store lvalue the gates admit, rewritten — or the input unchanged. */
|
|
164
|
+
function qualify(lval: Expr, window: readonly [number, number] | undefined): Expr {
|
|
165
|
+
if (lval.k !== 'index') {
|
|
166
|
+
return lval; // a `field` lvalue is a recovered struct view, whose declaration owns volatility
|
|
167
|
+
}
|
|
168
|
+
const ctx = accessCtx(lval, window);
|
|
169
|
+
if (firstRejection(VOL_STORE_GATES, ctx) !== null) {
|
|
170
|
+
return lval;
|
|
171
|
+
}
|
|
172
|
+
// An existing scalar pointer cast takes the qualifier in place; a bare const gets one minted,
|
|
173
|
+
// exactly the node backend/cfamily.ts's own deref legalization would have synthesized.
|
|
174
|
+
const base: Expr =
|
|
175
|
+
lval.base.k === 'cast' && lval.base.to.kind === 'ptr'
|
|
176
|
+
? { ...lval.base, volatile: true }
|
|
177
|
+
: { k: 'cast', to: T.ptr(pointee(lval)), volatile: true, e: lval.base };
|
|
178
|
+
return { ...lval, base };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** How many stores this tree would qualify — the enumeration gate, so a function with no device
|
|
182
|
+
* store costs one walk and no candidate.
|
|
183
|
+
*
|
|
184
|
+
* A COUNT, so the shared `stmtChildren` walk order (a switch's default at `defaultAt`, not last)
|
|
185
|
+
* cannot reach the answer. */
|
|
186
|
+
export function deviceStoreCount(sfn: SFn, window?: readonly [number, number]): number {
|
|
187
|
+
let n = 0;
|
|
188
|
+
const visit = (stmts: readonly Stmt[]): void => {
|
|
189
|
+
for (const s of stmts) {
|
|
190
|
+
if (s.k === 'store' && qualify(s.lval, window) !== s.lval) {
|
|
191
|
+
n++;
|
|
192
|
+
}
|
|
193
|
+
visit(stmtChildren(s));
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
visit(sfn.body);
|
|
197
|
+
return n;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The `/vol-store` candidate, or null when no store qualifies. Read-only: returns a fresh SFn
|
|
201
|
+
* whose body is rebuilt, leaving the input untouched. */
|
|
202
|
+
export function volatileDeviceStores(sfn: SFn, window?: readonly [number, number]): SFn | null {
|
|
203
|
+
if (deviceStoreCount(sfn, window) === 0) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const rewrite = (stmts: readonly Stmt[]): Stmt[] =>
|
|
207
|
+
stmts.map((s): Stmt => {
|
|
208
|
+
switch (s.k) {
|
|
209
|
+
case 'store':
|
|
210
|
+
return { ...s, lval: qualify(s.lval, window) };
|
|
211
|
+
case 'if':
|
|
212
|
+
return { ...s, then: rewrite(s.then), else: rewrite(s.else) };
|
|
213
|
+
case 'while':
|
|
214
|
+
case 'dowhile':
|
|
215
|
+
return { ...s, body: rewrite(s.body) };
|
|
216
|
+
case 'for':
|
|
217
|
+
return { ...s, init: rewrite([s.init])[0], inc: rewrite([s.inc])[0], body: rewrite(s.body) };
|
|
218
|
+
case 'switch':
|
|
219
|
+
return {
|
|
220
|
+
...s,
|
|
221
|
+
cases: s.cases.map((c) => ({ ...c, body: rewrite(c.body) })),
|
|
222
|
+
...(s.default ? { default: rewrite(s.default) } : {}),
|
|
223
|
+
};
|
|
224
|
+
default:
|
|
225
|
+
return s;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
return { ...sfn, body: rewrite(sfn.body) };
|
|
229
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// L3 re-spelling lever: spell a negate of a SHARED subtraction as `0 - x`.
|
|
2
|
+
//
|
|
3
|
+
// The two C spellings of a negation are not interchangeable in front of gcc 2.9's folder. `-x` is
|
|
4
|
+
// built through build_unary_op and FOLDED, so fold-const.c's "Convert - (a - b) to (b - a)"
|
|
5
|
+
// (gcc/fold-const.c:4821) rewrites a negated subtraction into the reversed one before CSE ever
|
|
6
|
+
// runs. `0 - x` reaches fold as a MINUS_EXPR with a zero left operand and comes back as a bare
|
|
7
|
+
// `build1 (NEGATE_EXPR, …)` (gcc/fold-const.c:5085) which is never re-folded, so the subtraction
|
|
8
|
+
// survives as itself.
|
|
9
|
+
//
|
|
10
|
+
// With ONE use that difference is invisible: RTL combine folds the negate back in and both
|
|
11
|
+
// spellings emit the same `sub r0, r1, r0`. With the subtraction SHARED — its value also feeding
|
|
12
|
+
// the compare that chose the branch — they diverge by more than the negate, because the reversed
|
|
13
|
+
// subtraction is a second computation whose operands must stay live: `-(a - b)` costs six
|
|
14
|
+
// instructions (one a register copy) where `0 - (a - b)` costs five. Both compiled with agbcc
|
|
15
|
+
// `-O2 -mthumb-interwork -Wimplicit -fhex-asm -fprologue-bugfix`.
|
|
16
|
+
//
|
|
17
|
+
// NOT an agbcc rule, so no target gate: compiled on the shared shape, IDO, KMC GCC and gcc
|
|
18
|
+
// 2.7.2/MIPS each emit a different function for the two spellings as well — IDO the other way
|
|
19
|
+
// round, where `-(a - b)` is the `negu` and `0 - (a - b)` the reversed `subu` — and only mwcc
|
|
20
|
+
// collapses them, where the duplicate scores identically and can neither win nor lose.
|
|
21
|
+
//
|
|
22
|
+
// So which one the source wrote is not recoverable — a `neg` is reachable from `-t` over a named
|
|
23
|
+
// local as well — and the differ referees. Semantics are preserved by construction: `-x` and
|
|
24
|
+
// `0 - x` are the same C expression for every integer type.
|
|
25
|
+
//
|
|
26
|
+
// A RE-SPELLING rather than a fourth value-home axis (docs/level-tower.md's third fork). The fold
|
|
27
|
+
// rule fires at all only because asmlift INLINED a value the source bound to a local, and naming
|
|
28
|
+
// that local reaches the same match by the other route: on `pokeemerald:GetAnchorCoord:agbcc`,
|
|
29
|
+
// `s32 t = a1 - a0;` scores 0 against the row's own target.o where the inlined body with a plain
|
|
30
|
+
// `-` scores 1. asmlift cannot spell it — all three home axes decline on pedigree (`/addr-home`
|
|
31
|
+
// wants an address, `/expr-home` a loop, `/derived-home` a memory read) and this is a bare pure
|
|
32
|
+
// value with three consumers. An axis admitting any such value would reach the use shapes a
|
|
33
|
+
// substitution cannot, and would double the fan wherever it admits; this costs one candidate per
|
|
34
|
+
// distinct tree carrying the shape. Take the axis when a row demands a shape this cannot reach.
|
|
35
|
+
//
|
|
36
|
+
// SCOPE (decline over approximate). Only a `bin('-')` operand, and only a SHARED one. Neither
|
|
37
|
+
// restriction is caution: over any other operand shape the fold rule does not apply and the two
|
|
38
|
+
// spellings compile identically (verified for `-(a + b)`, `-(a >> 3)`, `-(a * 3)`, `-a`), so
|
|
39
|
+
// firing there could only duplicate the primary. An EFFECTFUL subtraction is out of scope too —
|
|
40
|
+
// two textually equal calls are two calls, not one shared value, so the premise fails.
|
|
41
|
+
import { type Expr, type SFn, exprEquals, exprHasEffect, mapExprChildren, mapStmtExprs, walkExprs } from './ast';
|
|
42
|
+
|
|
43
|
+
export function zeroSubNegates(sfn: SFn): SFn | null {
|
|
44
|
+
const subs: Expr[] = [];
|
|
45
|
+
for (const e of walkExprs(sfn.body)) {
|
|
46
|
+
if (e.k === 'bin' && e.op === '-') {
|
|
47
|
+
subs.push(e);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const shared = (x: Expr): boolean => subs.filter((s) => exprEquals(s, x)).length > 1;
|
|
51
|
+
let changed = false;
|
|
52
|
+
const rewrite = (e: Expr): Expr => {
|
|
53
|
+
const m = mapExprChildren(e, rewrite);
|
|
54
|
+
if (m.k === 'un' && m.op === '-' && m.e.k === 'bin' && m.e.op === '-' && !exprHasEffect(m.e) && shared(m.e)) {
|
|
55
|
+
changed = true;
|
|
56
|
+
return { k: 'bin', op: '-', l: { k: 'const', value: 0 }, r: m.e };
|
|
57
|
+
}
|
|
58
|
+
return m;
|
|
59
|
+
};
|
|
60
|
+
const body = sfn.body.map((s) => mapStmtExprs(s, rewrite));
|
|
61
|
+
return changed ? { ...sfn, body } : null;
|
|
62
|
+
}
|