@asmlift/core 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/backend/cfamily.ts +39 -11
- package/src/contracts.ts +60 -11
- package/src/frontend/ssa.ts +1 -1
- package/src/frontend/thumb.ts +2 -2
- package/src/ir/alias.ts +24 -0
- package/src/ir/core.ts +8 -0
- package/src/ir/opcodes.ts +43 -7
- 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 +4 -4
- package/src/l3/ast.ts +65 -21
- package/src/l3/basecse.ts +48 -28
- package/src/l3/coalesce.ts +9 -9
- 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 +28 -25
- 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/typing.ts +3 -3
- package/src/l3/unmerge.ts +483 -59
- package/src/l3/unreduce.ts +13 -13
- 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/pattern/engine.ts +4 -4
- package/src/pipeline.ts +17 -5
- 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 +342 -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 +24 -2
- package/src/raise/pre-recovery.ts +90 -25
- package/src/raise/retsink.ts +381 -15
- package/src/raise/shortcircuit.ts +595 -34
- package/src/raise/structs.ts +4 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +4 -4
- package/src/{rank-axes.ts → rank-variations.ts} +319 -189
- package/src/rank.ts +1148 -803
- 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/structure.ts +1415 -200
- package/src/structure/switch-recover.ts +100 -7
- package/src/symbols.ts +127 -6
- package/src/target.ts +155 -35
- package/src/trace.ts +1 -1
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// L3 respell variation: a pointer local the source ADVANCED between two accesses, rather than two
|
|
2
|
+
// addresses the compiler derived from one.
|
|
3
|
+
//
|
|
4
|
+
// `ldr r3,=X; strh [r3]; adds r3,#2; strh [r3]` — the machine held an address in a register, used
|
|
5
|
+
// it, moved it, used it again. `raise/const.ts` folds the lift's `add(const X, const 2)` into the
|
|
6
|
+
// literal `X + 2`, because on Thumb that pair is also how a compiler materialises a 32-bit literal
|
|
7
|
+
// it cannot encode in one instruction, and it records the distinction it is erasing as
|
|
8
|
+
// `index.baseAdvanced` (l3/ast.ts's third evidence field). This pass reads it:
|
|
9
|
+
//
|
|
10
|
+
// *(u16 *)0x04000048 = a; *(u16 *)0x0400004A = b;
|
|
11
|
+
// → u16 *p = (u16 *)0x04000048; *p = a; p = p + 1; *p = b;
|
|
12
|
+
//
|
|
13
|
+
// WHY IT IS A CANDIDATE. Against the INDEXED spelling of the same minted local the advance buys
|
|
14
|
+
// nothing: agbcc folds `p = p + 1; *p` straight back into `strh [r3, #2]`, byte for byte the
|
|
15
|
+
// subscript's own object. What makes it visible is the CONJUNCTION with a `volatile` pointee,
|
|
16
|
+
// which bars that fold and leaves the `add` the target records. Both halves are compiled against
|
|
17
|
+
// `kleod:StreamCmd_SetWindowRegs`'s target object; the four corners are in test/advance.test.ts's
|
|
18
|
+
// header. So this pass emits a spelling and `compareScored` referees; nothing here claims the
|
|
19
|
+
// source wrote it.
|
|
20
|
+
//
|
|
21
|
+
// SOUNDNESS IS ADDRESS EQUALITY. `p` is freshly minted and assigned by nothing else, so at each
|
|
22
|
+
// member's access it holds `A0 + Σ steps so far` — that member's own absolute address — PROVIDED
|
|
23
|
+
// every advance sits between the accesses it separates on every path, and PROVIDED every node this
|
|
24
|
+
// pass re-spells as `*p` is one of those accesses. A top-level statement list has no back edge and
|
|
25
|
+
// runs its statements in order at most once each, so placing each advance at the top level
|
|
26
|
+
// immediately above its member's statement, with the members at STRICTLY INCREASING top-level
|
|
27
|
+
// indices, carries the first half.
|
|
28
|
+
//
|
|
29
|
+
// THE SECOND HALF IS `rewrite`, AND IT MATCHES BY ADDRESS, NOT BY IDENTITY (`:rewrite` below): it
|
|
30
|
+
// replaces EVERY `index` node whose `cellAddress` is a chain member's, wherever it sits. So a
|
|
31
|
+
// second access at a member's address — a twin at the top level, or one inside an arm or a loop
|
|
32
|
+
// body — is re-spelled `*p` at a point where `p` does not hold that address. The two rules that
|
|
33
|
+
// refuse those shapes (`member-second-site`, `member-nested-site`, and their head twins) are
|
|
34
|
+
// therefore SOUND, not narrowing, and the shape is pinned by test/advance.test.ts's `an access at
|
|
35
|
+
// a chain address inside a loop is not re-spelled`.
|
|
36
|
+
//
|
|
37
|
+
// SCOPE (decline over approximate) is `ADVANCE_HEAD_GATES` and `ADVANCE_MEMBER_GATES` below — as
|
|
38
|
+
// tables rather than an `||` chain, so `sound` costs a `guardedBy` and every rule is ablated
|
|
39
|
+
// against the real pass by test/advance.test.ts's battery, which records WHAT THE ABLATED PASS
|
|
40
|
+
// EMITS and checks `sound` against it. NOT by `bench gates`: `pnpm bench gates --pass advance`
|
|
41
|
+
// answers `no censusable pass "advance"`, and structurally must, because this pass is reached
|
|
42
|
+
// through a static import binding in rank.ts rather than through a mutable caller-side record
|
|
43
|
+
// (run/gate-census.ts's header, which measures the `TypeError` a module-namespace write raises).
|
|
44
|
+
// The firing census below was therefore taken by hand, with the recipe it states.
|
|
45
|
+
//
|
|
46
|
+
// FIVE OF THE TWELVE ARE NARROWING rather than soundness and each says which it is. Three are
|
|
47
|
+
// judgements about what the asm shows (`head-already-advanced`, `member-negative-step`,
|
|
48
|
+
// `member-no-evidence` — the last is what makes this a reading rather than a guess); one,
|
|
49
|
+
// `member-signedness`, buys the minted local ONE pointee type where the backend would otherwise
|
|
50
|
+
// spell a correct reinterpret cast; one, `member-element-grid`, is a LOUD refusal — ablated it
|
|
51
|
+
// emits `p0 = p0 + 0.5;`, which is not C. Dropping `member-no-evidence` alone leaves the emitted
|
|
52
|
+
// step `undefined / width` = `NaN`, refused downstream only by the two arithmetic rules'
|
|
53
|
+
// comparisons against it (`NaN % w !== 0`, `NaN !== addr`); the battery's `noncompile` verdicts
|
|
54
|
+
// for both are what hold that.
|
|
55
|
+
//
|
|
56
|
+
// HOW OFTEN EACH FIRES, over the whole corpus — `bench sweep --fan`, both arms, 2,126 records,
|
|
57
|
+
// instrumented on `firstRejection` (2026-09-12), which is HAND INSTRUMENTATION and reproduced by
|
|
58
|
+
// wrapping both tables in `tallying()` (l3/gates.ts) at this pass's one call site in rank.ts,
|
|
59
|
+
// passing `.gates` to `advancedBases`, and printing `.refusals()` when the sweep ends. The numbers
|
|
60
|
+
// count CALLS, and enumeration calls this pass about eleven times per record, once per structure setting's
|
|
61
|
+
// tree:
|
|
62
|
+
// 23,322 calls · 112 found a chain · 23,210 declined
|
|
63
|
+
// head-second-site 872 · member-no-evidence 664 · head-nested-site 256 · head-already-advanced 144
|
|
64
|
+
// every other member rule: 0
|
|
65
|
+
// So the eight remaining member rules are pinned by the battery and by NOTHING IN THE CORPUS —
|
|
66
|
+
// where the corpus refuses a chain, it refuses it at the head. Chain lengths found: 96 of two
|
|
67
|
+
// members and 16 of four, no others.
|
|
68
|
+
//
|
|
69
|
+
// WHAT THIS PASS DOES NOT DO, both measured rather than assumed:
|
|
70
|
+
// • A function with TWO disjoint chains gets one candidate, spelling the FIRST BY POSITION — not
|
|
71
|
+
// the longest, and the second chain is unreachable by any variation. ZERO of the 112 chain-bearing
|
|
72
|
+
// calls above held a second chain sharing no address with the first (the instrument kept
|
|
73
|
+
// scanning), so the second local this would need has no inhabitant to price it.
|
|
74
|
+
// • The init is `prepend`ed and there is no sunk twin; see the note at `placeBaseLocals` below.
|
|
75
|
+
// • IT IS MAP-LESS ONLY. Every member is reached through `cellAddress`, which answers null once
|
|
76
|
+
// a symbol map promotes the pool word to `®_WININ` — so with a map this pass enumerates
|
|
77
|
+
// nothing, and every `/advance` candidate the corpus carries is a `/raw-globals` one. That is
|
|
78
|
+
// what caps the variation at the six rows `bench sweep --fan --base origin/main` names
|
|
79
|
+
// (apps/benchmark/dataset/synthetic.ts, at `volwalk`), and it is the question to ask of it the
|
|
80
|
+
// day the symbol-map direction lands: this capability survives only if `cellAddress` learns
|
|
81
|
+
// the promoted form.
|
|
82
|
+
import { type IrType, scalarTypeForAccess } from '../ir/types';
|
|
83
|
+
import { cellAddress } from './address';
|
|
84
|
+
import { type Expr, type SFn, type Stmt, mapExprChildren, mapStmtExprs, stmtChildren, stmtExprs } from './ast';
|
|
85
|
+
import { type Gate, firstRejection } from './gates';
|
|
86
|
+
import type { BaseInit } from './hoist';
|
|
87
|
+
import { nameAllocator, placeBaseLocals } from './hoist';
|
|
88
|
+
|
|
89
|
+
/** One const-addressed access, with the top-level statement it was reached at. */
|
|
90
|
+
export interface Site {
|
|
91
|
+
stmt: number;
|
|
92
|
+
addr: number;
|
|
93
|
+
width: number;
|
|
94
|
+
signed: boolean;
|
|
95
|
+
advanced?: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** One candidate member, judged against the chain so far. `twin`/`nested` are the two ways some
|
|
99
|
+
* OTHER node in the tree names this site's address — the facts `rewrite`'s by-address match makes
|
|
100
|
+
* load-bearing. */
|
|
101
|
+
export interface MemberCtx {
|
|
102
|
+
prev: Site;
|
|
103
|
+
site: Site;
|
|
104
|
+
twin: boolean;
|
|
105
|
+
nested: boolean;
|
|
106
|
+
}
|
|
107
|
+
export type HeadCtx = Omit<MemberCtx, 'prev'>;
|
|
108
|
+
|
|
109
|
+
/** The head's own admission. The two address rules are the same PREDICATE as the member table's
|
|
110
|
+
* and deliberately not the same rule objects (see gates.ts on why a second consumer owns its
|
|
111
|
+
* own): a head that is re-spelled at a second site is wrong for the same reason a member is. */
|
|
112
|
+
export const ADVANCE_HEAD_GATES: readonly Gate<HeadCtx>[] = [
|
|
113
|
+
{
|
|
114
|
+
id: 'head-second-site',
|
|
115
|
+
why: 'the rewrite finds accesses by address, so another access to the same address would read `p` before it is set',
|
|
116
|
+
sound: true,
|
|
117
|
+
guardedBy: 'advance.test.ts: a chain address reached at a second site declines',
|
|
118
|
+
rejects: (c) => c.twin,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'head-nested-site',
|
|
122
|
+
why: 'the same address inside an arm or a loop body is re-spelled at a point `p` may not hold',
|
|
123
|
+
sound: true,
|
|
124
|
+
guardedBy: 'advance.test.ts: an access at a chain address inside a loop is not re-spelled',
|
|
125
|
+
rejects: (c) => c.nested,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'head-already-advanced',
|
|
129
|
+
why: 'an access the machine reached by stepping from an earlier one continues that chain, and starting a chain there would load an address the machine never loaded',
|
|
130
|
+
sound: false,
|
|
131
|
+
guardedBy: 'advance.test.ts: a chain may not START at an advanced site',
|
|
132
|
+
rejects: (c) => c.site.advanced !== undefined,
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
/** Each successor, against the member before it. FIRST rejection wins, so a refusal is
|
|
137
|
+
* attributable to one rule. */
|
|
138
|
+
export const ADVANCE_MEMBER_GATES: readonly Gate<MemberCtx>[] = [
|
|
139
|
+
{
|
|
140
|
+
id: 'member-no-evidence',
|
|
141
|
+
why: 'where the lift recorded no step, the pair is the compiler deriving two addresses from one literal',
|
|
142
|
+
sound: false,
|
|
143
|
+
guardedBy: 'advance.test.ts: the same pair with no evidence declines',
|
|
144
|
+
rejects: (c) => c.site.advanced === undefined,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'member-second-site',
|
|
148
|
+
why: 'the rewrite finds accesses by address, so another access to the same address would read `p` at the wrong value',
|
|
149
|
+
sound: true,
|
|
150
|
+
guardedBy: 'advance.test.ts: a chain address reached at a second site declines',
|
|
151
|
+
rejects: (c) => c.twin,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'member-nested-site',
|
|
155
|
+
why: 'the same address inside an arm or a loop body is re-spelled at a point `p` may not hold',
|
|
156
|
+
sound: true,
|
|
157
|
+
guardedBy: 'advance.test.ts: an access at a chain address inside a loop is not re-spelled',
|
|
158
|
+
rejects: (c) => c.nested,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'member-statement-order',
|
|
162
|
+
why: 'the advance must sit between the two accesses it separates, so the indices must increase',
|
|
163
|
+
sound: true,
|
|
164
|
+
guardedBy: 'advance.test.ts: two accesses in ONE statement are not a chain',
|
|
165
|
+
rejects: (c) => c.site.stmt <= c.prev.stmt,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'member-width',
|
|
169
|
+
why: 'the new pointer has one pointee width, and `*p` at another width names other bytes',
|
|
170
|
+
sound: true,
|
|
171
|
+
guardedBy: 'advance.test.ts: members of different widths decline',
|
|
172
|
+
rejects: (c) => c.site.width !== c.prev.width,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'member-signedness',
|
|
176
|
+
why: 'the new pointer has one pointee type, and a second access of another type would need a cast the source did not write',
|
|
177
|
+
sound: false,
|
|
178
|
+
guardedBy: 'advance.test.ts: members of different signedness decline',
|
|
179
|
+
rejects: (c) => c.site.signed !== c.prev.signed,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: 'member-element-grid',
|
|
183
|
+
why: 'a step that is not a whole number of elements would be written `p = p + step / width` with a fraction, which is not C',
|
|
184
|
+
sound: false,
|
|
185
|
+
guardedBy: 'advance.test.ts: a step off the element grid declines',
|
|
186
|
+
rejects: (c) => c.site.advanced! % c.prev.width !== 0,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'member-step-lands',
|
|
190
|
+
why: 'a step that does not land on this access is evidence about some other pair of addresses',
|
|
191
|
+
sound: true,
|
|
192
|
+
guardedBy: 'advance.test.ts: a step that does not land on the next access declines',
|
|
193
|
+
rejects: (c) => c.prev.addr + c.site.advanced! !== c.site.addr,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: 'member-negative-step',
|
|
197
|
+
why: 'a backward step (`p = p + -1`) is correct C, but this variation writes only forward steps',
|
|
198
|
+
sound: false,
|
|
199
|
+
guardedBy: 'advance.test.ts: a NEGATIVE step declines',
|
|
200
|
+
rejects: (c) => c.site.advanced! <= 0,
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
/** Every const-addressed `index` node in the body, split into the ones reached EXACTLY ONCE per
|
|
205
|
+
* execution of a top-level statement — the only places an advance statement can be put — and the
|
|
206
|
+
* ADDRESSES of every other one, which the chain rule refuses outright.
|
|
207
|
+
*
|
|
208
|
+
* A loop's OWN expression joins its body on the second side: a `while` condition runs once per
|
|
209
|
+
* iteration, so an advance above the loop and an access in its test are not the same count. A
|
|
210
|
+
* top-level `if`'s condition stays on the first side, because the `if` statement itself runs once
|
|
211
|
+
* whatever its arms do. */
|
|
212
|
+
function collectSites(body: readonly Stmt[]): { sites: Site[]; nestedAddrs: Set<number> } {
|
|
213
|
+
const sites: Site[] = [];
|
|
214
|
+
const nestedAddrs = new Set<number>();
|
|
215
|
+
const visit = (e: Expr, stmt: number, nested: boolean): void => {
|
|
216
|
+
if (e.k === 'index') {
|
|
217
|
+
const addr = cellAddress(e);
|
|
218
|
+
if (addr !== null) {
|
|
219
|
+
if (nested) {
|
|
220
|
+
nestedAddrs.add(addr);
|
|
221
|
+
} else {
|
|
222
|
+
sites.push({ stmt, addr, width: e.width, signed: e.signed, advanced: e.baseAdvanced });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
mapExprChildren(e, (c) => {
|
|
227
|
+
visit(c, stmt, nested);
|
|
228
|
+
return c;
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
const walk = (stmts: readonly Stmt[], stmt: number, nested: boolean): void => {
|
|
232
|
+
for (const s of stmts) {
|
|
233
|
+
const repeats = s.k === 'while' || s.k === 'dowhile' || s.k === 'for';
|
|
234
|
+
for (const e of stmtExprs(s)) {
|
|
235
|
+
visit(e, stmt, nested || repeats);
|
|
236
|
+
}
|
|
237
|
+
walk(stmtChildren(s), stmt, true);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
body.forEach((s, i) => walk([s], i, false));
|
|
241
|
+
return { sites, nestedAddrs };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The one chain this pass spells, or null.
|
|
245
|
+
*
|
|
246
|
+
* A NON-MEMBER SITE BETWEEN TWO MEMBERS DOES NOT END THE CHAIN. `p` is freshly minted, so an
|
|
247
|
+
* access that does not touch it cannot move it — and the clientele is MMIO setup code, where one
|
|
248
|
+
* `REG_BLDCNT = y;` between two window writes is the ordinary case. So the head is chosen by the
|
|
249
|
+
* head table rather than by position, and the walk scans every later site rather than stopping at
|
|
250
|
+
* the first one a gate refuses. Ambiguity is resolved greedily in statement order: where two later
|
|
251
|
+
* sites would both extend the chain, the earlier one does.
|
|
252
|
+
*
|
|
253
|
+
* THE TOLERANCE HAS NO CORPUS INHABITANT. MMIO writes with an unrelated const-addressed access
|
|
254
|
+
* between two members appear nowhere in the corpus, so this is a rule the file can state
|
|
255
|
+
* truthfully rather than reach a sweep can show. */
|
|
256
|
+
function chainOf(sites: readonly Site[], nestedAddrs: ReadonlySet<number>, gates: AdvanceGates): Site[] | null {
|
|
257
|
+
const head = gates.head ?? ADVANCE_HEAD_GATES;
|
|
258
|
+
const member = gates.member ?? ADVANCE_MEMBER_GATES;
|
|
259
|
+
const occurrences = new Map<number, number>();
|
|
260
|
+
for (const s of sites) {
|
|
261
|
+
occurrences.set(s.addr, (occurrences.get(s.addr) ?? 0) + 1);
|
|
262
|
+
}
|
|
263
|
+
const ctx = (site: Site): HeadCtx => ({
|
|
264
|
+
site,
|
|
265
|
+
twin: (occurrences.get(site.addr) ?? 0) > 1,
|
|
266
|
+
nested: nestedAddrs.has(site.addr),
|
|
267
|
+
});
|
|
268
|
+
for (let i = 0; i < sites.length; i++) {
|
|
269
|
+
if (firstRejection(head, ctx(sites[i])) !== null) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const chain = [sites[i]];
|
|
273
|
+
for (let j = i + 1; j < sites.length; j++) {
|
|
274
|
+
if (firstRejection(member, { prev: chain[chain.length - 1], ...ctx(sites[j]) }) === null) {
|
|
275
|
+
chain.push(sites[j]);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (chain.length >= 2) {
|
|
279
|
+
return chain;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** The two tables, ablatable — `gates.ts`'s reason: a test drops one entry and re-runs the REAL
|
|
286
|
+
* predicate on real input, with no test-only branch in the shipped path. Nothing in `src/` passes
|
|
287
|
+
* this; a shipped ablation of a `sound: true` rule emits wrong addresses, which is what
|
|
288
|
+
* `ablateHeuristic` refuses. */
|
|
289
|
+
export interface AdvanceGates {
|
|
290
|
+
head?: readonly Gate<HeadCtx>[];
|
|
291
|
+
member?: readonly Gate<MemberCtx>[];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Re-spell one advanced chain as a pointer local moved in place, or decline (null). */
|
|
295
|
+
export function advancedBases(sfn: SFn, gates: AdvanceGates = {}): SFn | null {
|
|
296
|
+
const { sites, nestedAddrs } = collectSites(sfn.body);
|
|
297
|
+
const chain = chainOf(sites, nestedAddrs, gates);
|
|
298
|
+
if (chain === null) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
const name = nameAllocator(sfn)();
|
|
302
|
+
const elem: IrType = scalarTypeForAccess(chain[0].width, chain[0].signed);
|
|
303
|
+
const ptr: IrType = { kind: 'ptr', to: elem };
|
|
304
|
+
const members = new Set(chain.map((m) => m.addr));
|
|
305
|
+
// The access itself: every chain member reads `*p`, because `p` has been advanced to exactly its
|
|
306
|
+
// address. The evidence fields go with the old base — they described how the ADDRESS was
|
|
307
|
+
// computed, and this spelling is the answer to that question rather than another instance of it.
|
|
308
|
+
//
|
|
309
|
+
// BY ADDRESS, NOT BY IDENTITY, and the header's soundness argument turns on it: a node this
|
|
310
|
+
// finds at a member's address that is NOT the member — a twin, or one inside an arm or a loop —
|
|
311
|
+
// is re-spelled too, which is why the gates that refuse those shapes are `sound: true`.
|
|
312
|
+
const rewrite = (e: Expr): Expr => {
|
|
313
|
+
const m = mapExprChildren(e, rewrite);
|
|
314
|
+
const addr = m.k === 'index' ? cellAddress(m) : null;
|
|
315
|
+
if (m.k === 'index' && addr !== null && members.has(addr)) {
|
|
316
|
+
return { k: 'index', base: { k: 'var', name }, idx: { k: 'const', value: 0 }, width: m.width, signed: m.signed };
|
|
317
|
+
}
|
|
318
|
+
return m;
|
|
319
|
+
};
|
|
320
|
+
// The emitted distance is the GATED quantity — the step `member-step-lands` tied to this pair of
|
|
321
|
+
// addresses and `member-element-grid` divided — rather than the address difference, which is the
|
|
322
|
+
// same number only because those two rules hold. Deriving it separately is how a later ablation
|
|
323
|
+
// of one of them emits a fractional advance nothing checked.
|
|
324
|
+
//
|
|
325
|
+
// WHICH MAKES THE ARITHMETIC HERE TOTAL ONLY BECAUSE OF THE TABLE, and both ways out are LOUD
|
|
326
|
+
// rather than silent — measured, and pinned by the battery's `noncompile` verdicts rather than
|
|
327
|
+
// guarded here: with `member-element-grid` dropped this emits `p0 = p0 + 0.5;`, and with
|
|
328
|
+
// `member-no-evidence` dropped `advanced` is `undefined` and this emits `p0 = p0 + NaN;`.
|
|
329
|
+
// Neither is C, so a candidate carrying one is dropped at compile with its message rather than
|
|
330
|
+
// scored — which is why the two rules are `sound: false` and why no `Number.isInteger` refusal
|
|
331
|
+
// stands here: adding one would turn those two ablations into a DECLINE and delete the evidence
|
|
332
|
+
// the battery reads. `member-no-evidence` is ablatable by `ablateHeuristic`, so a round that
|
|
333
|
+
// ships that ablation as a ranked candidate ships noncompiling sources; that is its price.
|
|
334
|
+
const advanceAt = new Map<number, number>();
|
|
335
|
+
for (let i = 1; i < chain.length; i++) {
|
|
336
|
+
advanceAt.set(chain[i].stmt, chain[i].advanced! / chain[i].width);
|
|
337
|
+
}
|
|
338
|
+
const body: Stmt[] = [];
|
|
339
|
+
sfn.body.forEach((s, i) => {
|
|
340
|
+
const step = advanceAt.get(i);
|
|
341
|
+
if (step !== undefined) {
|
|
342
|
+
body.push({
|
|
343
|
+
k: 'assign',
|
|
344
|
+
name,
|
|
345
|
+
value: { k: 'bin', op: '+', l: { k: 'var', name }, r: { k: 'const', value: step } },
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
body.push(mapStmtExprs(s, rewrite));
|
|
349
|
+
});
|
|
350
|
+
const init: BaseInit = {
|
|
351
|
+
k: 'assign',
|
|
352
|
+
name,
|
|
353
|
+
value: { k: 'cast', to: ptr, e: { k: 'const', value: chain[0].addr } },
|
|
354
|
+
};
|
|
355
|
+
const locals = [...sfn.locals, { name, type: ptr as SFn['locals'][number]['type'] }];
|
|
356
|
+
// `prepend` for `l3/nearbase.ts`'s reason and a second one this pass owns: the init MATERIALISES
|
|
357
|
+
// the register the chain advances, and the target's own instruction order is what says where the
|
|
358
|
+
// pool word was loaded. Putting it in first-use order instead moves it below whatever else the
|
|
359
|
+
// function loads first, which on `kleod:StreamCmd_SetWindowRegs` swaps the two pool words and
|
|
360
|
+
// costs the match; test/advance.test.ts's `the base init leads` pins the emitted order.
|
|
361
|
+
//
|
|
362
|
+
// AND NO `/advance/sinkinit` ALTERNATIVE, unlike `/nearbase`, which ships one for exactly this decision —
|
|
363
|
+
// not because the decision is better determined here (the generator cannot see the target either
|
|
364
|
+
// way) but because the twin CANNOT EXIST. `sinkInitsToFirstUse` sinks an init only when
|
|
365
|
+
// `localMentions` counts ONE assignment to its local ("or the move would cross the other write",
|
|
366
|
+
// l3/hoist.ts), and an advance IS a second assignment to this one — so the sink declines on every
|
|
367
|
+
// tree this pass produces, by construction rather than by row: the sink returns null on the
|
|
368
|
+
// advanced tree, and registering `/advance/sinkinit` adds no candidate to
|
|
369
|
+
// `kleod:StreamCmd_SetWindowRegs:agbcc`'s fan. The `prepend` decision above is therefore the only
|
|
370
|
+
// placement this variation HAS, which is a stronger reason to record the compile behind it.
|
|
371
|
+
const { body: placed } = placeBaseLocals({ ...sfn, locals, body }, [init], 'prepend');
|
|
372
|
+
return { ...sfn, locals, body: placed };
|
|
373
|
+
}
|
package/src/l3/argbase.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: materialize the deref BASES of a call's arguments into locals, before the
|
|
2
2
|
// call.
|
|
3
3
|
//
|
|
4
4
|
// When a call's arguments are each a deref through a different fixed address, the compiler loads
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
// order names the bases first (`vu8 *p = ®_VCOUNT_L; u8 *e = gEntityArray; f(*p, e[8])`), which
|
|
17
17
|
// is what a decomp author writes and what this pass reproduces.
|
|
18
18
|
//
|
|
19
|
-
// A
|
|
19
|
+
// A VARIATION, not a committed rewrite: it is emitted as an ADDITIONAL candidate (rank.ts `/argbase`) and the
|
|
20
20
|
// differ referees, so the inline spelling is always still there to win. That is what bounds the
|
|
21
|
-
// risk — a
|
|
21
|
+
// risk — a rewrite that replaced the default could lose a match, this one cannot.
|
|
22
22
|
//
|
|
23
23
|
// SEMANTICS ARE PRESERVED BY CONSTRUCTION, which matters because on a NONMATCH row the
|
|
24
24
|
// best-scoring candidate is what the user is shown. Only a PURE leaf base is eligible — a global's
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// KNOWN LIMITATION: the hoisted local is a plain `T *` — `IrType` models no cv-qualifier at all,
|
|
30
30
|
// so naming a VOLATILE cell through it drops the qualifier that macros.ts goes out of its way to
|
|
31
31
|
// carry. Pre-existing and not introduced here (every pointer local in the tower has it), but the
|
|
32
|
-
// two features meet on exactly the MMIO shape this
|
|
32
|
+
// two features meet on exactly the MMIO shape this variation targets, so it is written down rather
|
|
33
33
|
// than left to be rediscovered.
|
|
34
34
|
//
|
|
35
35
|
// GATE: at least TWO arguments of the same call must qualify, with DISTINCT bases. The reordering
|
package/src/l3/ast.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// asmlift L3 — the language-NEUTRAL structured AST. A LanguageBackend lowers this to a
|
|
2
2
|
// concrete language (C / Pascal / C++) and prints it. "Return a value" and binary ops
|
|
3
3
|
// are neutral nodes here; each backend owns its own spelling.
|
|
4
|
-
import type
|
|
4
|
+
import { type IrType, typeEquals } from '../ir/types';
|
|
5
5
|
|
|
6
6
|
export type Expr =
|
|
7
7
|
| { k: 'var'; name: string }
|
|
@@ -74,7 +74,7 @@ export type Expr =
|
|
|
74
74
|
// • a local (`var`) — nothing to reassociate into; already right, and no reader fires.
|
|
75
75
|
// • a plus tree — the fold pulls the displacement into the tree, costing an `add` and a
|
|
76
76
|
// register. The repair is to home the base in a local so it becomes a
|
|
77
|
-
// `var`, which is the ADDRESS-HOME
|
|
77
|
+
// `var`, which is the ADDRESS-HOME variation and lives one level down at L2
|
|
78
78
|
// (`structure/analysis.ts`'s `sharedBaseClasses`) because materializing
|
|
79
79
|
// a value is a structuring decision, not a spelling.
|
|
80
80
|
// • a leaf const/addr — the fold bakes the displacement into the literal, changing the `.word`.
|
|
@@ -96,6 +96,16 @@ export type Expr =
|
|
|
96
96
|
// reason — both spellings denote the same cell — and it is per SYMBOL, so every access of one
|
|
97
97
|
// name carries the same answer, which is right for agbcc because one CSEd pool load serves
|
|
98
98
|
// them all.
|
|
99
|
+
//
|
|
100
|
+
// `baseAdvanced` is the THIRD, and it answers about the ADDRESS COMPUTATION: this access's
|
|
101
|
+
// address was reached by ADDING this many bytes to a register that already held — and had just
|
|
102
|
+
// been used as — another address (`adds r3, #2` between two stores), rather than by a second
|
|
103
|
+
// pool word or a memory-operand displacement. `raise/const.ts` records it on the literal its
|
|
104
|
+
// fold produces, because the fold is what makes the three shapes indistinguishable; the
|
|
105
|
+
// structure seam copies it here. `l3/advance.ts` is what reads it, to offer a pointer local
|
|
106
|
+
// advanced in place. `exprEquals` ignores it for `operandOff`'s reason. Absence is never proof:
|
|
107
|
+
// a target whose frontend does not lift the advance stamps nothing, and the value is a byte
|
|
108
|
+
// count that can be NEGATIVE, so readers test `!== undefined`.
|
|
99
109
|
| {
|
|
100
110
|
k: 'index';
|
|
101
111
|
base: Expr;
|
|
@@ -103,8 +113,35 @@ export type Expr =
|
|
|
103
113
|
width: number;
|
|
104
114
|
signed: boolean;
|
|
105
115
|
lead?: Expr[];
|
|
116
|
+
/** The ELEMENT type the base's own DECLARATION gives it, for the one base whose stride the
|
|
117
|
+
* C type walk cannot reconstruct: a map-declared array MEMBER (`gPtr->arr`), which
|
|
118
|
+
* `exprCType` types `undefined` because the `field` node hangs off an untyped `var`. Without
|
|
119
|
+
* it the C backend legalizes the base through a reinterpret cast (`((u8 *)gPtr->arr)[i]`),
|
|
120
|
+
* which is the CAST form's object again and defeats the whole point of naming the member.
|
|
121
|
+
*
|
|
122
|
+
* IT IS A PRODUCER INVARIANT: the backend cannot vet a stated type. `derefStrideOk` tests
|
|
123
|
+
* the STATED type against the access width, never against the base, so it is true by
|
|
124
|
+
* construction for anything a producer could state AND for a statement gone stale. What
|
|
125
|
+
* the consumer does guard is PRECEDENCE — this field is consulted only where `exprCType`
|
|
126
|
+
* answers nothing (cfamily.ts `legalizedIndexBase`), so wherever the walk can read the base
|
|
127
|
+
* it corrects a stale statement instead of being overridden by it. The one producer,
|
|
128
|
+
* structure/structure.ts `pointeeElement`, sets it from the same `elemSize`/`elemSigned` it
|
|
129
|
+
* has just passed `spellsAccessType` on — and that predicate IS
|
|
130
|
+
* `typeEquals(T.int(width*8, elemSigned), scalarTypeForAccess(width, signed))`, so
|
|
131
|
+
* `derefStrideOk` over the stated pointer is true by construction at every width (4 by
|
|
132
|
+
* `width === 4`, 1 and 2 by `to.signed === signed`). The obligation is on the PRODUCER:
|
|
133
|
+
* state the type the access's own width and signedness agree with.
|
|
134
|
+
*
|
|
135
|
+
* THE ALTERNATIVE REJECTED: teach `exprCType` the pointee layout, the way
|
|
136
|
+
* `sym.noteGlobal` types the bare-array spelling —
|
|
137
|
+
* the printer already renders `u8 grid[6][8]` for this member from this same layout. It was
|
|
138
|
+
* rejected because `SFn.globals` is also the ADDRESSABLE-BASE list, so typing the symbol
|
|
139
|
+
* there admits it as a `/livebase` base (measured on the probe: 8 extra base locals). Typing
|
|
140
|
+
* the one node costs no candidates. */
|
|
141
|
+
baseElem?: IrType;
|
|
106
142
|
operandOff?: number;
|
|
107
143
|
baseOrdered?: true;
|
|
144
|
+
baseAdvanced?: number;
|
|
108
145
|
}
|
|
109
146
|
// A named struct-field access `base->name` (raise/structs.ts recovered `base` as a struct
|
|
110
147
|
// pointer, so the byte offset resolves to a named field instead of a scaled array index).
|
|
@@ -145,7 +182,7 @@ export type Expr =
|
|
|
145
182
|
//
|
|
146
183
|
// The COMPARISONS stay collapsed, and that asymmetry is the rule applying rather than an omission:
|
|
147
184
|
// which side a compare was spelled from genuinely underdetermines — a signed spelling that
|
|
148
|
-
// byte-matched was proved non-negative by the compiler — so it is refereed as
|
|
185
|
+
// byte-matched was proved non-negative by the compiler — so it is refereed as a variation, while a
|
|
149
186
|
// division helper is a pure function of the expression's C type with no such proof available.
|
|
150
187
|
export type BinOp =
|
|
151
188
|
| '+'
|
|
@@ -245,7 +282,7 @@ export interface SFn {
|
|
|
245
282
|
/** Recovered locals, declared at function top. Two INDEPENDENT volatility facts, mirroring
|
|
246
283
|
* symbols.ts's cell-vs-pointee split: `volatile` = the local OBJECT is volatile (the
|
|
247
284
|
* address-escaped frame scratch; dce.ts treats reads of it as observable), `pointeeVolatile`
|
|
248
|
-
* = the local is a pointer TO volatile data (the l3/volatileptr.ts
|
|
285
|
+
* = the local is a pointer TO volatile data (the l3/volatileptr.ts variation; a declaration
|
|
249
286
|
* spelling only — nothing about the local itself is observable).
|
|
250
287
|
*
|
|
251
288
|
* `frame` is present on a local the structurer recovered from an `laddr` — the asm
|
|
@@ -254,7 +291,7 @@ export interface SFn {
|
|
|
254
291
|
* SUB-WORD frame object: `strh/ldrh/strb/ldrb` have no `[sp,#imm]` form, so a compiler must
|
|
255
292
|
* copy `sp` first, while a word spill goes straight to `[sp,#imm]` and is recovered as an
|
|
256
293
|
* SSA value with no local of its own. So `frame` is NOT the set of every value the machine
|
|
257
|
-
* slotted. `loads`/`stores` are the yardstick a qualifier
|
|
294
|
+
* slotted. `loads`/`stores` are the yardstick a qualifier variation must match before it may
|
|
258
295
|
* declare every access to the object observable: the readability passes between here and L3
|
|
259
296
|
* may drop a store or render one machine load as two reads, and `volatile` over an access
|
|
260
297
|
* set asmlift did not preserve is a source that contradicts itself. ABSENT where the counts
|
|
@@ -312,7 +349,7 @@ export interface SFn {
|
|
|
312
349
|
* exists to keep it out of. So the tree is neutral in its NODES — every node still spells the
|
|
313
350
|
* same thing in C, C++ and Pascal — and not in its emission policy, which this field is.
|
|
314
351
|
* `ascending` = the earlier-declared spilled local takes the LOWER `[sp,#k]`. Set by the
|
|
315
|
-
* structurer from `StructureOptions.spillSlotOrder`, itself a
|
|
352
|
+
* structurer from `StructureOptions.spillSlotOrder`, itself a compiler behavior declared in
|
|
316
353
|
* `TargetDescription.compilerBehaviors`. ABSENT means the direction is unknown for this target
|
|
317
354
|
* and `l3/slotorder.ts` is the identity — never "ascending by default". */
|
|
318
355
|
slotOrder?: 'ascending' | 'descending';
|
|
@@ -396,7 +433,7 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
396
433
|
const bb = b as typeof a;
|
|
397
434
|
// `volatile` is part of the SPELLING, compared for the same reason `lead` and `dot` are: a
|
|
398
435
|
// CSE or dedup that treats these as equal keeps one node and drops the other, silently
|
|
399
|
-
//
|
|
436
|
+
// rewriting a volatile access as a plain one.
|
|
400
437
|
return (
|
|
401
438
|
JSON.stringify(a.to) === JSON.stringify(bb.to) &&
|
|
402
439
|
(a.volatile ?? false) === (bb.volatile ?? false) &&
|
|
@@ -411,9 +448,9 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
411
448
|
const bb = b as typeof a;
|
|
412
449
|
// `lead` is part of the ADDRESS (`g[0][i]` and `g[1][i]` are different elements), so it
|
|
413
450
|
// must be compared — an omission here would let CSE/dedup collapse two distinct accesses.
|
|
414
|
-
// `operandOff` deliberately
|
|
415
|
-
//
|
|
416
|
-
// collapses them respells nothing.
|
|
451
|
+
// The EVIDENCE fields (`operandOff`, `baseOrdered`, `baseAdvanced`) deliberately are NOT:
|
|
452
|
+
// two accesses agreeing on everything else denote the same cell and print the same subscript
|
|
453
|
+
// however the machine spelled the offset, so a CSE that collapses them respells nothing.
|
|
417
454
|
const lead = a.lead ?? [];
|
|
418
455
|
const bLead = bb.lead ?? [];
|
|
419
456
|
return (
|
|
@@ -421,6 +458,13 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
421
458
|
a.signed === bb.signed &&
|
|
422
459
|
lead.length === bLead.length &&
|
|
423
460
|
lead.every((v, i) => exprEquals(v, bLead[i])) &&
|
|
461
|
+
// `baseElem` is part of the SPELLING for the same reason `lead` is: it decides whether the
|
|
462
|
+
// base takes the reinterpret cast, so two otherwise-equal nodes disagreeing about it print
|
|
463
|
+
// two different expressions. (Its one producer derives it from the base and the width, so
|
|
464
|
+
// nodes that agree on those agree here too — this cannot reject a real CSE.)
|
|
465
|
+
(a.baseElem === undefined
|
|
466
|
+
? bb.baseElem === undefined
|
|
467
|
+
: bb.baseElem !== undefined && typeEquals(a.baseElem, bb.baseElem)) &&
|
|
424
468
|
exprEquals(a.base, bb.base) &&
|
|
425
469
|
exprEquals(a.idx, bb.idx)
|
|
426
470
|
);
|
|
@@ -428,7 +472,7 @@ export function exprEquals(a: Expr, b: Expr): boolean {
|
|
|
428
472
|
case 'field': {
|
|
429
473
|
const bb = b as typeof a;
|
|
430
474
|
// `dot` is part of the SPELLING, and for the same reason `lead` is compared above: a CSE or
|
|
431
|
-
// dedup that treats these as equal keeps one node and discards the other, silently
|
|
475
|
+
// dedup that treats these as equal keeps one node and discards the other, silently rewriting
|
|
432
476
|
// `p->field_4` as `p.field_4` (or the reverse). Both compile only for the base type each
|
|
433
477
|
// belongs to, so collapsing them is how a valid access becomes an invalid one — or worse, a
|
|
434
478
|
// valid one against a different object.
|
|
@@ -533,7 +577,7 @@ export function stmtExprs(s: Stmt): Expr[] {
|
|
|
533
577
|
* heads included, nested statements recursively. The rewrite dual of stmtExprs/stmtChildren, for
|
|
534
578
|
* the PURE 1:1 case: one expression in, one expression out, every statement kept.
|
|
535
579
|
*
|
|
536
|
-
* A
|
|
580
|
+
* A PASS THAT HAND-ROLLS ITS OWN MAPPER IS NOT A MISSED MIGRATION. Several do, because their
|
|
537
581
|
* contract is not this one — a rewrite that may DECLINE, one that INSERTS statements, one that
|
|
538
582
|
* rewrites assign TARGETS as well as expressions, one that turns a statement into a list. Check
|
|
539
583
|
* the contract before pointing one of them here. */
|
|
@@ -583,13 +627,13 @@ export function mapStmtExprs(s: Stmt, f: (e: Expr) => Expr): Stmt {
|
|
|
583
627
|
* one admits and the other refuses can be re-indexed but never qualified, so the paired
|
|
584
628
|
* `/indexed/volatile` spelling is unreachable at exactly the hardware addresses it is for.
|
|
585
629
|
*
|
|
586
|
-
* Two
|
|
630
|
+
* Two respell variations reading the same initializers are deliberately NOT here: l3/inlinebase.ts
|
|
587
631
|
* substitutes the address at each use, l3/nearbase.ts clusters neighbours by distance, and both
|
|
588
632
|
* need the VALUE, which is the evaluator above. Declining a shift-encoded base there costs a
|
|
589
|
-
*
|
|
633
|
+
* variation that does not fire, and the population is small: over klonoa's 531 lifting functions,
|
|
590
634
|
* one inlinebase-shaped local with no symbol map and none with it; a folded nearbase would form
|
|
591
635
|
* a new cluster in 4 functions mapless and 1 with the map. Both counts were zero over the agbcc
|
|
592
|
-
* benchmark rows that lifted when the
|
|
636
|
+
* benchmark rows that lifted when the variations landed — a MEASUREMENT over a corpus that grows, so
|
|
593
637
|
* re-take it rather than quoting it: a new row falsifies the number, not the argument. */
|
|
594
638
|
export function rematerializableAddress(e: Expr): boolean {
|
|
595
639
|
let nonZero = false;
|
|
@@ -626,8 +670,8 @@ export function exprHasEffect(e: Expr): boolean {
|
|
|
626
670
|
* about a QUALIFIER, and a pass that asks the first where it means the second reorders device
|
|
627
671
|
* accesses while its gate reports clean.
|
|
628
672
|
*
|
|
629
|
-
* Three spellings assert one thing, and all three are here because a
|
|
630
|
-
* would miss the two a later
|
|
673
|
+
* Three spellings assert one thing, and all three are here because a pass that knew only the cast
|
|
674
|
+
* would miss the two a later variation writes: a `volatile` cast (where a raw address carries it), a
|
|
631
675
|
* read through a pointer local declared to point at volatile data (l3/volatileptr.ts), and a read
|
|
632
676
|
* of a `volatile` local object (l3/volatileval.ts). A bare cast counts even with no deref under it
|
|
633
677
|
* — the qualifier is on the ACCESS the cast exists to spell, and every caller so far is asking
|
|
@@ -801,14 +845,14 @@ export function* walkExprs(body: Stmt[]): Generator<Expr> {
|
|
|
801
845
|
// so this must never be used to negate a general integer expression — only an `if`/loop test or an
|
|
802
846
|
// operand of one of the connectives above.
|
|
803
847
|
//
|
|
804
|
-
// SCOPE of rule 2: it fires wherever a branch-sense
|
|
848
|
+
// SCOPE of rule 2: it fires wherever a branch-sense variation negates the condition, which is both `if`
|
|
805
849
|
// classes — `preserveDivergentBranchSense` on divergent ifs, `negateJoinedBranchSense` on
|
|
806
850
|
// reconverging ones — and on the joined class it is a DEFAULT emission, not a differ-only
|
|
807
|
-
// alternative, so a source `&&` can come out as its `||` dual with no
|
|
851
|
+
// alternative, so a source `&&` can come out as its `||` dual with no variation asked for
|
|
808
852
|
// (`synthetic:ifand_far`, where the branch range put the fold on the other arm). Neither
|
|
809
|
-
//
|
|
853
|
+
// variation reaches a LOOP test, so a connective that ended up as one has no dual candidate at all —
|
|
810
854
|
// the differ never sees the other form, and on such a row this rule changes how the code READS and
|
|
811
|
-
// nothing else. Widening a branch-sense
|
|
855
|
+
// nothing else. Widening a branch-sense variation to loop tests is what would make it a matching variation
|
|
812
856
|
// there, and that is a separate change.
|
|
813
857
|
export const NEGATE_REL: Partial<Record<BinOp, BinOp>> = {
|
|
814
858
|
'<': '>=',
|