@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
package/src/l3/unreduce.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: UN-REDUCE a loop-carried accumulator — delete `v = INIT; … v = v + K;`
|
|
2
2
|
// and spell each read as the closed form `INIT[start := counter]`.
|
|
3
3
|
//
|
|
4
4
|
// v0 = (a0 << 6) + a1; while (v1 <= 31) {
|
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
// left. Refused function-wide, because a stashed pointer outlives the statement that made it.
|
|
92
92
|
// • MOVED-EFFECT — a call or a marker would run once per read instead of once. Refused.
|
|
93
93
|
// • MOVED-VOLATILE — a `volatile` access is one the source pinned precisely so it would not be
|
|
94
|
-
// duplicated or moved. Refused. No corpus row reaches it today (nothing on the
|
|
95
|
-
// this
|
|
94
|
+
// duplicated or moved. Refused. No corpus row reaches it today (nothing on the default spelling
|
|
95
|
+
// this variation rides carries a qualifier on a READ), so it is guarded by its unit test alone.
|
|
96
96
|
// • MOVED-READ-ALIASABLE — an ordinary memory read moved down the region sees whatever the
|
|
97
97
|
// region wrote. asmlift can only rule that out for writes it can NAME, so a moved read is
|
|
98
98
|
// admitted on one configuration: every write the region evaluates goes to a compile-time
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
// is FALSE. "A write to a hardware register is not a write to any object a C program declares" is
|
|
112
112
|
// true, and it does not finish the argument: a DMA controller READS a control word and then WRITES
|
|
113
113
|
// ordinary memory on the program's behalf. On the GBA, storing `0x84000020` to `DMA3CNT`
|
|
114
|
-
// (0x040000DC) starts a 32-word transfer into `[DMA3DAD]` — and every row this
|
|
114
|
+
// (0x040000DC) starts a 32-word transfer into `[DMA3DAD]` — and every row this variation reaches
|
|
115
115
|
// drives exactly that register. Modelled and executed, the admitted candidate turns a clean walk
|
|
116
116
|
// over a destination table into wild writes: the first transfer clobbers the table the init reads,
|
|
117
117
|
// and every later iteration recomputes its destination from the garbage.
|
|
@@ -147,12 +147,12 @@
|
|
|
147
147
|
// `firstRejection` call site over both — 750 synthetic trees and all 252 real-tier rows, the real
|
|
148
148
|
// tier being the SYMBOL-MAPPED configuration since every row carries its authored map —
|
|
149
149
|
// `unitsDisagree` is true on NO row in either, and `/unreduce` fires on no real-tier row at all,
|
|
150
|
-
// so no zero-flip gate reaches this
|
|
150
|
+
// so no zero-flip gate reaches this variation. Its one known inhabitant is outside the benchmark:
|
|
151
151
|
// klonoa's `UpdateHUDTimePanel`, where WITH a symbol map the accumulator lifts `u16 *` against an
|
|
152
152
|
// integer init and the gate refuses it, and with RAW ADDRESSES the same asm lifts all-integer and
|
|
153
153
|
// the candidate is correct and survives (`50335396 + (v15 << 6)`, 64 bytes an iteration, which is
|
|
154
154
|
// the ROM's own `adds r1, #0x40`). Same assembly, same loop, same stride — a raw-address sweep is
|
|
155
|
-
// BLIND to the defect and reports the
|
|
155
|
+
// BLIND to the defect and reports the variation as correct. The checkout sweep behind that datum
|
|
156
156
|
// covered klonoa's 467 functions in BOTH configurations; a second checkout was swept raw-only and
|
|
157
157
|
// found nothing, and since raw-only is the blind arm that null result carries no weight. Only the
|
|
158
158
|
// klonoa half of the sweep is evidence, and it is quoted here without the other.
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
// was actually MISSING from that table (`volatile-counter`) is in it now, which is the part that
|
|
187
187
|
// could not wait.
|
|
188
188
|
//
|
|
189
|
-
// Nothing qualifying ⇒ decline (null), never a duplicate of the
|
|
189
|
+
// Nothing qualifying ⇒ decline (null), never a duplicate of the default.
|
|
190
190
|
import { type IrType } from '../ir/types';
|
|
191
191
|
import { cellAddress, inRange, rootConst } from './address';
|
|
192
192
|
import {
|
|
@@ -264,7 +264,7 @@ export const UNREDUCE_GATES: readonly Gate<AccCtx>[] = [
|
|
|
264
264
|
},
|
|
265
265
|
{
|
|
266
266
|
id: 'acc-pinned',
|
|
267
|
-
why: 'a declaration that carries
|
|
267
|
+
why: 'a declaration that carries a fact from the assembly cannot be deleted without dropping the fact',
|
|
268
268
|
sound: true,
|
|
269
269
|
guardedBy: 'unreduce.test.ts: a pinned accumulator declines, on every pin a local can carry',
|
|
270
270
|
rejects: (c) => c.pinned,
|
|
@@ -344,14 +344,14 @@ export const UNREDUCE_GATES: readonly Gate<AccCtx>[] = [
|
|
|
344
344
|
},
|
|
345
345
|
{
|
|
346
346
|
id: 'nonzero-start',
|
|
347
|
-
why: 'a counter starting at a nonzero constant leaves a bias term this
|
|
347
|
+
why: 'a counter starting at a nonzero constant leaves a bias term this rewrite does not write',
|
|
348
348
|
sound: true,
|
|
349
349
|
guardedBy: 'unreduce.test.ts: a counter-free init declines unless its start is the constant 0',
|
|
350
350
|
rejects: (c) => c.declined === 'nonzero-start',
|
|
351
351
|
},
|
|
352
352
|
{
|
|
353
353
|
id: 'step-ratio',
|
|
354
|
-
why: 'a counter stepping by more than one leaves
|
|
354
|
+
why: 'a counter stepping by more than one leaves a ratio between the two steps, which is not a shift',
|
|
355
355
|
sound: true,
|
|
356
356
|
guardedBy: 'unreduce.test.ts: a counter-free init declines when the counter does not step by one',
|
|
357
357
|
rejects: (c) => c.declined === 'step-ratio',
|
|
@@ -426,7 +426,7 @@ function arithScale(t: IrType | undefined): number | null {
|
|
|
426
426
|
/** Does this local's DECLARATION pin it against deletion? Every flag `SFn.locals` can carry, because
|
|
427
427
|
* each is a fact about the ASM that only the declaration states: two qualifiers (deleting a
|
|
428
428
|
* `volatile u16 *` local re-spells `*p = 0` as a raw cast with no qualifier on it — l3/inlinebase.ts
|
|
429
|
-
* carries it onto the minted cast instead, and this
|
|
429
|
+
* carries it onto the minted cast instead, and this variation has no local left to carry anything), a
|
|
430
430
|
* frame home, an `undef` whose whole content is the assignment that is MISSING, and the SPILL HOMES.
|
|
431
431
|
*
|
|
432
432
|
* WHY `slots` PINS, which is not the obvious reading. Deleting a slot-carrying local does not
|
|
@@ -537,7 +537,7 @@ function relate(init: Expr, start: Expr, ctr: string, k: Expr, d: number): Relat
|
|
|
537
537
|
return null; // anything else between the root and the counter, and the stride is not `k`
|
|
538
538
|
};
|
|
539
539
|
// THE SUBSTITUTIONAL FORM FIRST, unchanged: it is the shipped spelling every corpus inhabitant
|
|
540
|
-
// of this
|
|
540
|
+
// of this variation rides, and trying it first makes the branch below strictly additive — it is
|
|
541
541
|
// reached only where the old rule already declined, so it can admit candidates but never
|
|
542
542
|
// re-spell one.
|
|
543
543
|
//
|
|
@@ -796,7 +796,7 @@ export interface UnreduceResult {
|
|
|
796
796
|
|
|
797
797
|
/** The `/unreduce` candidate, or null when no accumulator qualifies. `window` is the target's
|
|
798
798
|
* declared device-register range (TargetDescription.capabilities.deviceRegisters) — absent, the
|
|
799
|
-
*
|
|
799
|
+
* variation still fires on a closed form that reads no memory. `triggers` is
|
|
800
800
|
* `capabilities.deviceMemoryWriters`; absent, EVERY device store is treated as one. */
|
|
801
801
|
export function unreduceAccumulators(
|
|
802
802
|
sfn: SFn,
|
package/src/l3/volatileptr.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: declare a pointer local that holds a NUMERIC address as pointing to
|
|
2
2
|
// volatile data (`volatile u16 *p = (u16 *)0x3000010;`).
|
|
3
3
|
//
|
|
4
4
|
// A numeric address has no declaration anywhere — the project maps a symbol's volatility, but a
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// optimizer's pseudos and lands the register allocator on different homes (on the row this was
|
|
9
9
|
// built for, the counter is copied out of r0 so the loaded value can have it — the target's
|
|
10
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
|
|
11
|
+
// form as rank.ts `/volatile`, per-local subsets as its results named by subject — and the differ
|
|
12
12
|
// referees.
|
|
13
13
|
//
|
|
14
14
|
// SEMANTICS ARE PRESERVED BY CONSTRUCTION: `volatile` only RESTRICTS what a compiler may do
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// `p = q` taints `p`; conservatively, `p` assigned ANY expression mentioning a tainted name).
|
|
26
26
|
// The symbol map owns a declared global's volatility, and a mixed-feed local would read the
|
|
27
27
|
// mapped global through a volatile view the map never granted. No qualifying local ⇒ decline
|
|
28
|
-
// (null), so the
|
|
28
|
+
// (null), so the variation never emits a duplicate of the default.
|
|
29
29
|
import { addrConst, cellAddress, inRange } from './address';
|
|
30
30
|
import { type Expr, type SFn, type Stmt, mapExprChildren, rematerializableAddress, walkExprs } from './ast';
|
|
31
31
|
|
|
@@ -72,7 +72,7 @@ function collectAssigns(stmts: Stmt[], out: { name: string; value: Expr }[]): vo
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
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
|
|
75
|
+
* volatility tie-break, so it is counted here beside the variation that mints the claims.
|
|
76
76
|
*
|
|
77
77
|
* Two spellings assert one: a `volatile` pointer cast over a numeric address (what
|
|
78
78
|
* l3/inlinebase.ts leaves at each use and what l3/volstore.ts mints at a device store), and a
|
|
@@ -119,7 +119,7 @@ export function deviceVolatileClaims(sfn: SFn, window?: readonly [number, number
|
|
|
119
119
|
return n;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
/** The locals the
|
|
122
|
+
/** The locals the variation would qualify — the per-local SUBSET enumeration's input (below):
|
|
123
123
|
* which pointers the original declared volatile is per-pointer knowledge the asm does not
|
|
124
124
|
* carry (an MMIO block and a plain RAM table can sit side by side, and qualifying the table
|
|
125
125
|
* blocks the read collapse its region wants), so each non-empty subset is its own candidate
|
|
@@ -172,10 +172,10 @@ function eligibility(sfn: SFn): (l: SFn['locals'][number]) => boolean {
|
|
|
172
172
|
!tainted.has(l.name);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
/** The proper non-empty SUBSETS of the qualifying locals (within `within`, when given) as
|
|
176
|
-
*
|
|
175
|
+
/** The proper non-empty SUBSETS of the qualifying locals (within `within`, when given) as the
|
|
176
|
+
* results of a multi-result variation — one candidate per subset, its members named as the subject. Empty above
|
|
177
177
|
* three qualifiers: the arm is capped at 6 extra spellings, and the all-qualifiers form is the
|
|
178
|
-
* plain
|
|
178
|
+
* plain variation's own candidate. */
|
|
179
179
|
export function volatileSubsetCandidates(sfn: SFn, within?: ReadonlySet<string>): { merged: string; sfn: SFn }[] {
|
|
180
180
|
const elig = volatileEligibleLocals(sfn).filter((n) => within === undefined || within.has(n));
|
|
181
181
|
if (elig.length < 2 || elig.length > 3) {
|
|
@@ -193,9 +193,9 @@ export function volatileSubsetCandidates(sfn: SFn, within?: ReadonlySet<string>)
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/** The `/volatile` candidate, or null when no local qualifies. Read-only: returns a fresh SFn
|
|
196
|
-
* sharing the (unmodified) body. `only` narrows the
|
|
197
|
-
*
|
|
198
|
-
* created hoists), so the
|
|
196
|
+
* sharing the (unmodified) body. `only` narrows the variation to the named locals — a /volatile
|
|
197
|
+
* composition (rank.ts) qualifies just the locals its first variation centres on (kept walk bases,
|
|
198
|
+
* created hoists), so the composition never degenerates into a general /volatile composition over
|
|
199
199
|
* the function's other locals — and the subset enumeration re-uses the same door. */
|
|
200
200
|
export function volatilePtrLocals(sfn: SFn, only?: ReadonlySet<string>): SFn | null {
|
|
201
201
|
const eligible = eligibility(sfn);
|
package/src/l3/volatileval.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: declare a stack-homed scalar local `volatile` (`volatile u16 sp0;`).
|
|
2
2
|
//
|
|
3
3
|
// `volatile` on a scalar VALUE local FORCES the value into memory: agbcc's allocator is
|
|
4
4
|
// otherwise free to keep it in a callee-saved register across a call, and no other qualifier
|
|
5
5
|
// or type spelling takes that freedom away. Whether the source spelled it is not derivable from
|
|
6
6
|
// the asm — a slot-homed value can equally come from an address-taken local or from plain
|
|
7
7
|
// register pressure — so both spellings are emitted and the differ referees, exactly as the
|
|
8
|
-
// sibling pointee
|
|
8
|
+
// sibling pointee variation (volatileptr.ts) does for a numeric-address pointer.
|
|
9
9
|
//
|
|
10
10
|
// SEMANTICS ARE PRESERVED BY CONSTRUCTION, as they are there: `volatile` only RESTRICTS what a
|
|
11
11
|
// compiler may do with the accesses, so every execution of the qualified spelling is an
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// ENVELOPE — narrower than "a source-level volatile scalar": an `laddr`-recovered frame object,
|
|
16
16
|
// which under Thumb is a SUB-WORD one (see the `frame` note on SFn.locals). A `volatile s32`
|
|
17
17
|
// local spills straight to `[sp,#imm]` and is recovered as an ordinary value with no local of
|
|
18
|
-
// its own, so this
|
|
18
|
+
// its own, so this variation cannot reach it. The flag is the set of slots asmlift PROVED, not the
|
|
19
19
|
// set of values a source could have qualified.
|
|
20
20
|
//
|
|
21
21
|
// GATE — VOL_SLOT_GATES holds the rules; the argument behind each is here, where there is room:
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
// something about the pointee the tree never claimed.
|
|
28
28
|
// • `volatile` already set is the frontend's stamp for an object whose address was PUBLISHED
|
|
29
29
|
// to memory (frontend/thumb.ts stamps it there, on `published`, not on any escape) — the
|
|
30
|
-
// candidate would duplicate the
|
|
30
|
+
// candidate would duplicate the default.
|
|
31
31
|
// • An address-TAKEN local already has a memory home in every spelling, so there is no home
|
|
32
32
|
// left for the qualifier to move: EReader_Reset's slot read and written through a pointer
|
|
33
33
|
// local compiles to IDENTICAL assembly with the qualifier and without (agbcc 2.9-arm-000512,
|
|
@@ -38,14 +38,14 @@
|
|
|
38
38
|
// condition the tree alone cannot answer. `volatile` asserts that every access written is an
|
|
39
39
|
// access performed; the passes between the asm and here break that in both directions and
|
|
40
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
|
|
41
|
+
// by the ABSENCE of the flag this variation adds — so a source that stores the slot twice
|
|
42
42
|
// arrives with one store. And the structurer emits one C read per USE rather than per
|
|
43
43
|
// machine load, so one `ldrh` feeding two uses arrives as two reads. Either way the
|
|
44
44
|
// qualified spelling would declare an access set asmlift did not preserve, so both DECLINE.
|
|
45
45
|
//
|
|
46
|
-
// No qualifying local ⇒ decline (null), so the
|
|
46
|
+
// No qualifying local ⇒ decline (null), so the variation never emits a duplicate of the default.
|
|
47
47
|
//
|
|
48
|
-
// ALL ELIGIBLE SLOTS OR NONE, where the sibling pointee
|
|
48
|
+
// ALL ELIGIBLE SLOTS OR NONE, where the sibling pointee variation enumerates per-local SUBSETS on the
|
|
49
49
|
// argument that volatility is per-pointer knowledge. It is per-slot knowledge here too; the
|
|
50
50
|
// subsets are simply uninhabited — across the 311 agbcc benchmark rows no function reaches this
|
|
51
51
|
// gate with two eligible slots. How many reach it at all depends on the sweep's configuration, so
|
|
@@ -67,7 +67,7 @@ interface SlotCtx {
|
|
|
67
67
|
export const VOL_SLOT_GATES: readonly Gate<SlotCtx>[] = [
|
|
68
68
|
{
|
|
69
69
|
id: 'no-frame',
|
|
70
|
-
why: 'the
|
|
70
|
+
why: 'without a stack slot in the assembly there is no memory for the qualifier to force the local into',
|
|
71
71
|
sound: false,
|
|
72
72
|
rejects: (c) => !c.hasFrame,
|
|
73
73
|
},
|
|
@@ -80,19 +80,19 @@ export const VOL_SLOT_GATES: readonly Gate<SlotCtx>[] = [
|
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
82
|
id: 'already-volatile',
|
|
83
|
-
why: 'the
|
|
83
|
+
why: 'the default already declares it, so the candidate would duplicate it',
|
|
84
84
|
sound: false,
|
|
85
85
|
rejects: (c) => c.alreadyVolatile,
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
88
|
id: 'addr-taken',
|
|
89
|
-
why: 'an address-taken local already
|
|
89
|
+
why: 'an address-taken local already lives in memory, so the qualifier has nothing left to force',
|
|
90
90
|
sound: false,
|
|
91
91
|
rejects: (c) => c.addrTaken > 0,
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
94
|
id: 'access-set',
|
|
95
|
-
why: 'the qualifier asserts every access written is performed, so the
|
|
95
|
+
why: 'the qualifier asserts every access written is performed, so the lifted code must write exactly the accesses the machine performed',
|
|
96
96
|
sound: true,
|
|
97
97
|
guardedBy: 'volatileval.test.ts: a store the tree no longer carries declines',
|
|
98
98
|
rejects: (c) => !c.accessSetKept,
|
package/src/l3/volstore.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: pin a STORE at a fixed DEVICE-REGISTER address `volatile`
|
|
2
2
|
// (`*(volatile s32 *)0x40000d4 = x` rather than `*(s32 *)0x40000d4 = x`).
|
|
3
3
|
//
|
|
4
4
|
// A numeric address has no declaration anywhere, so whether the original source wrote through a
|
|
5
5
|
// `volatile` lvalue is not derivable from the asm — the same gap l3/volatileptr.ts's header
|
|
6
|
-
// argues. That
|
|
6
|
+
// argues. That variation answers it for a pointer LOCAL holding the address; this one answers it
|
|
7
7
|
// where there is no local at all, which is the spelling a `#define REG(x) *(vu32 *)(x)` macro
|
|
8
8
|
// produces and the shape structure.ts leaves when the address re-materializes at each use.
|
|
9
9
|
//
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
// that says WHICH half is missing — "this target declares no device page" and "this address is
|
|
37
37
|
// IWRAM" are different facts about a row, and over the corpus they are 283 and 711 of the 1011
|
|
38
38
|
// refusals against `outside-window`'s 17. Nothing qualifying ⇒ decline (null), never a duplicate
|
|
39
|
-
// of the
|
|
39
|
+
// of the default.
|
|
40
40
|
//
|
|
41
41
|
// THE WINDOW IS A REACH GATE, PRICED — not a soundness one, which is why it is `sound: false`.
|
|
42
42
|
// A `volatile` qualifier only restricts the compiler, so widening the range can never make a
|
|
43
43
|
// candidate WRONG; what it would make is a claim about ordinary memory that the target denies
|
|
44
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
|
|
45
|
+
// the differ can only referee by luck. Measured by running the variation twice, once with the declared
|
|
46
46
|
// range and once with one admitting every constant address: 8 trees over 7 rows carry a
|
|
47
47
|
// const-address store the window excludes, and the fan moves on two of them —
|
|
48
48
|
// `synthetic:readarm` 6 candidates → 8 (the extra one TIES its match at 0) and
|
|
@@ -53,31 +53,31 @@
|
|
|
53
53
|
// WHAT THE WINDOW IS NOT: A CLAIM THAT NOTHING ELSE MAY QUALIFY ORDINARY MEMORY. `/volatile`
|
|
54
54
|
// (l3/volatileptr.ts) does exactly that, and a sweep over 834 corpus trees finds it qualifying an
|
|
55
55
|
// address outside this window on 21 (tree, local, address) pairs, 16 of them on agbcc — including
|
|
56
|
-
// `kleod:WritePaletteColor:agbcc
|
|
56
|
+
// `kleod:WritePaletteColor:agbcc` (retired 2026-09-13), then a published byte-exact MATCH whose winning source contained
|
|
57
57
|
// `*(volatile s32 *)50351492 = v2 + 5;` at 0x03004D84, which is IWRAM — the same minted-cast form
|
|
58
|
-
// this
|
|
58
|
+
// this variation produces, at an address this variation's window refuses.
|
|
59
59
|
//
|
|
60
60
|
// THE TWO ARE SEPARATED BY THEIR DERIVATION AND NOT BY WHAT THEY EMIT, which is the trap in
|
|
61
61
|
// reading that spelling as a contradiction. `/inlinebase` alone mints no qualifier at all —
|
|
62
62
|
// enumerated on that row, `unsigned/inlinebase` carries zero `volatile` casts and
|
|
63
63
|
// `unsigned/inlinebase/volatile` carries three. The qualifier comes from `/volatile`, which put it
|
|
64
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
|
|
65
|
+
// then carries that codegen fact onto each cast it leaves behind. This variation runs where no such
|
|
66
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
|
|
67
|
+
// declared, a number supports nothing. The window is where THIS variation's evidence runs out, not
|
|
68
68
|
// where the target's permission does.
|
|
69
69
|
//
|
|
70
70
|
// (So the two are not foldable on the window, and the fold is not free: adopting it for `/volatile`
|
|
71
71
|
// would delete the WritePaletteColor spelling. `deviceVolatileClaims` in volatileptr.ts already
|
|
72
72
|
// unifies the COUNT side, which is the half where one answer really is enough. Nor do they
|
|
73
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
|
|
74
|
+
// homes and its raw-constant stores together, and over 834 corpus trees both variations fire on ONE —
|
|
75
75
|
// `kleod:SetupBG3WindowOverlay:agbcc`, which neither decompiler scores — and under two DIFFERENT
|
|
76
76
|
// published classifications: asmlift `noncompile` (agbcc rejects its call to `m4aSoundVSyncOff`),
|
|
77
77
|
// m2c `declined`. A pairing whose whole reach is a row asmlift cannot compile is one a row has yet
|
|
78
78
|
// to demand.
|
|
79
79
|
//
|
|
80
|
-
// A
|
|
80
|
+
// A variation that MINTS the locals is a different question with a different answer, and `rank.ts`
|
|
81
81
|
// pairs this pass with one: `/regionbase` homes the regions holding two or more direct uses of a
|
|
82
82
|
// device base and leaves every other spelling of the same address inline, so both qualifiers have
|
|
83
83
|
// something to claim in one function. `synthetic:dmascope` is that row.)
|
|
@@ -85,11 +85,11 @@
|
|
|
85
85
|
// SCOPE: STORES only. A device READ is a different question with a different answer — the idiom
|
|
86
86
|
// fold's DCE drops a use-less device load outright (synthetic:dmaback), so a read that survives to
|
|
87
87
|
// L3 is one whose value the function consumes, and whether THAT may be CSEd is the question
|
|
88
|
-
// `/reread-globals` referees as a
|
|
88
|
+
// `/reread-globals` referees as a structure variation. The price of pinning one is real and is
|
|
89
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
|
|
90
|
+
// that read and the row scores 15. This variation does not reach it — ucmp's stores go through a
|
|
91
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
|
|
92
|
+
// both configurations and with any window. No row demands the read spelling, and a variation with no
|
|
93
93
|
// inhabitant is what "earn the level" forbids.
|
|
94
94
|
import { type IrType, T, scalarTypeForAccess } from '../ir/types';
|
|
95
95
|
import { cellAddress, inRange } from './address';
|
|
@@ -114,7 +114,7 @@ interface AccessCtx {
|
|
|
114
114
|
export const VOL_STORE_GATES: readonly Gate<AccessCtx>[] = [
|
|
115
115
|
{
|
|
116
116
|
id: 'no-window',
|
|
117
|
-
why: 'a target that declares no device range has no address this
|
|
117
|
+
why: 'a target that declares no device range has no address this variation may call volatile',
|
|
118
118
|
sound: false,
|
|
119
119
|
rejects: (c) => !c.hasWindow,
|
|
120
120
|
},
|
|
@@ -127,9 +127,9 @@ export const VOL_STORE_GATES: readonly Gate<AccessCtx>[] = [
|
|
|
127
127
|
{
|
|
128
128
|
// NOT "the target denies this address may be volatile" — `/volatile` qualifies IWRAM on eleven
|
|
129
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
|
|
130
|
+
// paragraph. This gate is about what THIS variation has evidence for, which is the address alone.
|
|
131
131
|
id: 'outside-window',
|
|
132
|
-
why: 'the address is the only evidence this
|
|
132
|
+
why: 'the address is the only evidence this variation has, and outside the window it supports nothing',
|
|
133
133
|
sound: false,
|
|
134
134
|
rejects: (c) => !c.inWindow,
|
|
135
135
|
},
|
package/src/l3/zerosub.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: spell a negate of a SHARED subtraction as `0 - x`.
|
|
2
2
|
//
|
|
3
3
|
// The two C spellings of a negation are not interchangeable in front of gcc 2.9's folder. `-x` is
|
|
4
4
|
// built through build_unary_op and FOLDED, so fold-const.c's "Convert - (a - b) to (b - a)"
|
|
@@ -23,20 +23,20 @@
|
|
|
23
23
|
// local as well — and the differ referees. Semantics are preserved by construction: `-x` and
|
|
24
24
|
// `0 - x` are the same C expression for every integer type.
|
|
25
25
|
//
|
|
26
|
-
// A
|
|
26
|
+
// A RESPELL VARIATION rather than a fourth value-home structure variation (docs/level-tower.md's third fork). The fold
|
|
27
27
|
// rule fires at all only because asmlift INLINED a value the source bound to a local, and naming
|
|
28
28
|
// that local reaches the same match by the other route: on `pokeemerald:GetAnchorCoord:agbcc`,
|
|
29
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
|
|
30
|
+
// `-` scores 1. asmlift cannot spell it — all three home variations decline on pedigree (`/addr-home`
|
|
31
31
|
// wants an address, `/expr-home` a loop, `/derived-home` a memory read) and this is a bare pure
|
|
32
|
-
// value with three consumers.
|
|
32
|
+
// value with three consumers. A structure variation admitting any such value would reach the use shapes a
|
|
33
33
|
// substitution cannot, and would double the fan wherever it admits; this costs one candidate per
|
|
34
|
-
// distinct tree carrying the shape. Take the
|
|
34
|
+
// distinct tree carrying the shape. Take the structure variation when a row demands a shape this cannot reach.
|
|
35
35
|
//
|
|
36
36
|
// SCOPE (decline over approximate). Only a `bin('-')` operand, and only a SHARED one. Neither
|
|
37
37
|
// restriction is caution: over any other operand shape the fold rule does not apply and the two
|
|
38
38
|
// spellings compile identically (verified for `-(a + b)`, `-(a >> 3)`, `-(a * 3)`, `-a`), so
|
|
39
|
-
// firing there could only duplicate the
|
|
39
|
+
// firing there could only duplicate the default. An EFFECTFUL subtraction is out of scope too —
|
|
40
40
|
// two textually equal calls are two calls, not one shared value, so the premise fails.
|
|
41
41
|
import { type Expr, type SFn, exprEquals, exprHasEffect, mapExprChildren, mapStmtExprs, walkExprs } from './ast';
|
|
42
42
|
|
package/src/pattern/engine.ts
CHANGED
|
@@ -67,7 +67,7 @@ export interface RewritePattern {
|
|
|
67
67
|
// `applies` is DATA, consumed generically by patternApplies — NOT an `arch ==` branch.
|
|
68
68
|
// `isa` pins the ISA; `compilers` pins which COMPILERS emit this idiom (the same shift-sequence
|
|
69
69
|
// for `/2` is produced by agbcc AND gcc, so a compiler LIST, not a single arch, is the honest
|
|
70
|
-
// predicate); `capabilities` is a hardware predicate. An absent
|
|
70
|
+
// predicate); `capabilities` is a hardware predicate. An absent field means "don't constrain on it".
|
|
71
71
|
applies: { isa?: string; compilers?: string[]; capabilities?: Partial<{ hwDivide: boolean; hwFloat: boolean }> };
|
|
72
72
|
match: MatchNode; // rooted at the op result to replace
|
|
73
73
|
// NOTE: a RELATIONAL guard (a `where` clause constraining the bound immediates, e.g. "two shift
|
|
@@ -93,10 +93,10 @@ export interface RewritePattern {
|
|
|
93
93
|
unsequencedRightFirst?: [string, string];
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/** Does this pattern apply to `target`? Every DECLARED
|
|
96
|
+
/** Does this pattern apply to `target`? Every DECLARED field must match: the ISA (so an idiom can be
|
|
97
97
|
* pinned to one frontend), the compiler set (so an idiom fires only for the compilers that emit
|
|
98
98
|
* it — the reason MIPS+IDO and MIPS+GCC are distinguishable despite one frontend), and every
|
|
99
|
-
* declared capability. An omitted
|
|
99
|
+
* declared capability. An omitted field is unconstrained. */
|
|
100
100
|
export function patternApplies(
|
|
101
101
|
p: RewritePattern,
|
|
102
102
|
target: { id: string; compiler: string; capabilities: { hwDivide: boolean; hwFloat: boolean } },
|
|
@@ -172,7 +172,7 @@ export const SDIV_POW2_2: RewritePattern = {
|
|
|
172
172
|
// out as its own definition. Folding the triple back to one `smod`/`umod` gives recovery and the
|
|
173
173
|
// structurer the operator the source wrote, and re-emitting `%` reproduces the triple byte-exact.
|
|
174
174
|
//
|
|
175
|
-
// This is NOT the `capabilities.hwDivide`
|
|
175
|
+
// This is NOT the `capabilities.hwDivide` field: MIPS also divides in hardware and needs no fold at
|
|
176
176
|
// all, because `div` leaves the remainder in `hi` and the frontend reads it straight out. The
|
|
177
177
|
// narrower fact is a hardware divide that yields the QUOTIENT ONLY; `isa: 'ppc'` STANDS IN for it
|
|
178
178
|
// until a second ISA earns the capability, and `compilers` carries the measured half, that mwcc's
|
package/src/pipeline.ts
CHANGED
|
@@ -261,7 +261,7 @@ export function raiseRecovered(
|
|
|
261
261
|
self?: FnProto,
|
|
262
262
|
pre: PreRecoveryOptions = {},
|
|
263
263
|
): void {
|
|
264
|
-
runPreRecovery(
|
|
264
|
+
const lifted = runPreRecovery(
|
|
265
265
|
fn,
|
|
266
266
|
target,
|
|
267
267
|
(pass, result) => {
|
|
@@ -276,7 +276,15 @@ export function raiseRecovered(
|
|
|
276
276
|
verify(fn);
|
|
277
277
|
assertTypesRecovered(fn);
|
|
278
278
|
hooks.afterRecover?.();
|
|
279
|
-
|
|
279
|
+
// `lifted.mergeShapes` is the CFG as it ENTERED pre-recovery, and retsink's `pre-diamond` needs
|
|
280
|
+
// exactly that: `raise/shortcircuit.ts` manufactures two-armed diamonds out of condition trees the
|
|
281
|
+
// ROM never merged, and a diamond this pass reads at its own turn may be one of those.
|
|
282
|
+
if (
|
|
283
|
+
sinkReturns(fn, {
|
|
284
|
+
hoistsSingleSetArm: target.compilerBehaviors.hoistsSingleSetArm,
|
|
285
|
+
mergeShapes: lifted.mergeShapes,
|
|
286
|
+
})
|
|
287
|
+
) {
|
|
280
288
|
verify(fn);
|
|
281
289
|
hooks.afterRetsink?.();
|
|
282
290
|
}
|
|
@@ -333,7 +341,7 @@ function attributeOpaques<T>(fn: Fn, body: () => T): T {
|
|
|
333
341
|
if (!names.size || /unmodelled instruction/.test(e.message)) {
|
|
334
342
|
throw e;
|
|
335
343
|
}
|
|
336
|
-
// Through `gapReasonFor`, so the classifier sees its canonical text — a hand-written
|
|
344
|
+
// Through `gapReasonFor`, so the classifier sees its canonical text — a hand-written spelling
|
|
337
345
|
// misses the mnemonic-anchored classes and every attributed decline lands in the generic bucket.
|
|
338
346
|
const list = [...names].sort().map(gapReasonFor).join(', ');
|
|
339
347
|
throw new StructureError(`${e.message} — and the function carries ${list}, which is the more likely cause`);
|
|
@@ -341,8 +349,12 @@ function attributeOpaques<T>(fn: Fn, body: () => T): T {
|
|
|
341
349
|
}
|
|
342
350
|
|
|
343
351
|
/** Stage 4 — structure + its boundary contracts, always as a pair. */
|
|
344
|
-
export function structureChecked(
|
|
345
|
-
|
|
352
|
+
export function structureChecked(
|
|
353
|
+
fn: Fn,
|
|
354
|
+
opts: Parameters<typeof structure>[1],
|
|
355
|
+
hooks?: Parameters<typeof structure>[2],
|
|
356
|
+
): SFn {
|
|
357
|
+
const raw = attributeOpaques(fn, () => structure(fn, opts, hooks));
|
|
346
358
|
// The boundary contracts run on the pre-DCE tree: the readability pass must never be able to
|
|
347
359
|
// hide a structuring defect by dropping the dead statement that carries it. assertResolved
|
|
348
360
|
// catches an unresolved `?` value; assertDerefsTyped catches an ill-typed deref (e.g. a pointer
|
package/src/proto.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type { SymbolMap, SymbolTypeFacts } from './symbols';
|
|
|
16
16
|
* the extension is an inference off an encoding two different C sources produce. Where the asm
|
|
17
17
|
* carries no extension, this list is NOT consulted: pinning there would type every parameter of
|
|
18
18
|
* every row from the declaration, and a declared `u32` kills rank.ts's signed arm before the
|
|
19
|
-
* differ ever sees it. That half is
|
|
19
|
+
* differ ever sees it. That half is a variation question and is not answered here. */
|
|
20
20
|
export type ParamType = string;
|
|
21
21
|
|
|
22
22
|
/** What the headers know about one function. All fields optional: a partial table (only
|
|
@@ -143,7 +143,7 @@ export function validatePrototypes(value: unknown): string[] {
|
|
|
143
143
|
* determine one. A pointer is `void *` — address-identical to any object pointer, and asmlift
|
|
144
144
|
* makes every stride explicit — so nothing is guessed about what it points at. A richer spelling
|
|
145
145
|
* would also be INERT: `declaredWidth` answers 32 for every `*`, and a CALLEE's parameter types
|
|
146
|
-
* are read for the list's length alone (test/param-pointee-
|
|
146
|
+
* are read for the list's length alone (test/param-pointee-variation.test.ts). */
|
|
147
147
|
function typeSpelling(t: SymbolTypeFacts): ParamType | null {
|
|
148
148
|
if (t.pointer) {
|
|
149
149
|
return 'void *';
|