@asmlift/core 0.5.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 -167
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +194 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +54 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +140 -21
- 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 +2 -1
- package/src/l3/ast.ts +464 -57
- package/src/l3/basecse.ts +664 -76
- package/src/l3/coalesce.ts +429 -43
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +21 -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 +644 -218
- 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 +15 -0
- 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 +157 -56
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +33 -18
- 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 +97 -14
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +210 -10
- package/src/raise/shortcircuit.ts +474 -74
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +33 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1723 -272
- package/src/structure/analysis.ts +1392 -141
- 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 +2678 -526
- package/src/structure/switch-recover.ts +616 -144
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// L3 re-spelling lever: NEIGHBOR absolute addresses derive from one shared base local.
|
|
2
|
+
//
|
|
3
|
+
// A cluster of raw-address accesses a few bytes apart is one object's cells: the compiler holds
|
|
4
|
+
// the object's base in a register and derives each cell (`add #72` / `add #74` off one pool
|
|
5
|
+
// word, a halfword offset beyond the load range forcing the add, the in-range word staying
|
|
6
|
+
// `[rN, #112]`), where a per-cell spelling anchors one pool constant per address. This lever
|
|
7
|
+
// re-spells every deref base in a cluster as an offset from a `u8 *` base local holding the
|
|
8
|
+
// cluster's lowest address, and the differ referees:
|
|
9
|
+
//
|
|
10
|
+
// *(u16 *)0x0300104A → u8 *b = (u8 *)0x03001048; *(u16 *)(b + 2)
|
|
11
|
+
//
|
|
12
|
+
// SCOPE (decline over approximate): cluster MEMBERSHIP comes from CONST deref bases only (a
|
|
13
|
+
// struct-pointer cast base and everything inside a dot-form field subtree keep their spelling —
|
|
14
|
+
// their stride is the struct's, not a byte's); a cluster needs at least two DISTINCT addresses
|
|
15
|
+
// within the target's declared derivation reach of its lowest (TargetDescription nearBaseSpan —
|
|
16
|
+
// beyond it the derive costs more than the pool word it saves); every access the walk visits
|
|
17
|
+
// rewrites, so a cluster splits only across the field-subtree and struct-pointer-cast
|
|
18
|
+
// boundaries. A member basecse
|
|
19
|
+
// already hoisted arrives as a `var` base and is invisible here — the reused-base and
|
|
20
|
+
// neighbor-base spellings stay separate candidates. Once a cluster HAS formed, a bare const
|
|
21
|
+
// VALUE inside its window re-spells too, as `(s32)(b + off)` — the address of a cell handed to
|
|
22
|
+
// something (a DMA source register) is the same derived add in the original, and the two
|
|
23
|
+
// spellings are value-equal by construction, so the differ referees — including an integer that
|
|
24
|
+
// only coincidentally lands in the window, which is the stated cost of the lever (the `s32` cast
|
|
25
|
+
// assumes addresses below 2^31, true of every target that declares nearBaseSpan today). Declines
|
|
26
|
+
// (null) when no cluster forms.
|
|
27
|
+
import { baseConst } from './address';
|
|
28
|
+
import type { Expr, SFn } from './ast';
|
|
29
|
+
import { mapExprChildren, mapStmtExprs } from './ast';
|
|
30
|
+
import { type BaseInit, nameAllocator, placeBaseLocals } from './hoist';
|
|
31
|
+
|
|
32
|
+
/** `span` is the target's single-add-immediate derivation reach
|
|
33
|
+
* (TargetDescription.compilerBehaviors.nearBaseSpan) — a target that declares none never runs
|
|
34
|
+
* this lever. */
|
|
35
|
+
export function nearBaseClusters(sfn: SFn, span: number): SFn | null {
|
|
36
|
+
if (!Number.isFinite(span) || span < 0) {
|
|
37
|
+
return null; // a hostile span stalls the cluster window instead of shrinking it
|
|
38
|
+
}
|
|
39
|
+
// collect every DISTINCT const deref-base address
|
|
40
|
+
const addrs = new Set<number>();
|
|
41
|
+
const collect = (e: Expr): Expr => {
|
|
42
|
+
if (e.k === 'field') {
|
|
43
|
+
return e; // a dot-form subtree keeps its struct base — never collected, never rewritten
|
|
44
|
+
}
|
|
45
|
+
if (e.k === 'cast' && e.to.kind === 'ptr' && e.to.to.kind === 'struct') {
|
|
46
|
+
return e; // rewrite refuses these subtrees, so collecting under them would seed a cluster
|
|
47
|
+
}
|
|
48
|
+
const m = mapExprChildren(e, collect);
|
|
49
|
+
if (m.k === 'index') {
|
|
50
|
+
const c = baseConst(m.base);
|
|
51
|
+
if (c !== null) {
|
|
52
|
+
addrs.add(c);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return m;
|
|
56
|
+
};
|
|
57
|
+
for (const s of sfn.body) {
|
|
58
|
+
mapStmtExprs(s, collect);
|
|
59
|
+
}
|
|
60
|
+
// greedy clusters over the sorted addresses; only multi-member clusters rewrite
|
|
61
|
+
const sorted = [...addrs].sort((a, b) => a - b);
|
|
62
|
+
const baseOf = new Map<number, number>();
|
|
63
|
+
for (let i = 0; i < sorted.length;) {
|
|
64
|
+
const lo = sorted[i];
|
|
65
|
+
let j = i;
|
|
66
|
+
while (j < sorted.length && sorted[j] - lo <= span) {
|
|
67
|
+
j++;
|
|
68
|
+
}
|
|
69
|
+
if (j - i >= 2) {
|
|
70
|
+
for (let k = i; k < j; k++) {
|
|
71
|
+
baseOf.set(sorted[k], lo);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
i = j;
|
|
75
|
+
}
|
|
76
|
+
if (baseOf.size === 0) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const baseName = new Map<number, string>();
|
|
80
|
+
const fresh = nameAllocator(sfn); // the shared minting mechanism — collides with nothing in sfn
|
|
81
|
+
for (const lo of new Set(baseOf.values())) {
|
|
82
|
+
baseName.set(lo, fresh());
|
|
83
|
+
}
|
|
84
|
+
const derived = (lo: number, off: number): Expr =>
|
|
85
|
+
off === 0
|
|
86
|
+
? { k: 'var', name: baseName.get(lo)! }
|
|
87
|
+
: { k: 'bin', op: '+', l: { k: 'var', name: baseName.get(lo)! }, r: { k: 'const', value: off } };
|
|
88
|
+
// the cluster (if any) whose window covers a bare const value
|
|
89
|
+
const windows = [...new Set(baseOf.values())];
|
|
90
|
+
const coveringLo = (v: number): number | undefined => windows.find((lo) => v >= lo && v - lo <= span);
|
|
91
|
+
const rewrite = (e: Expr): Expr => {
|
|
92
|
+
if (e.k === 'field') {
|
|
93
|
+
return e;
|
|
94
|
+
}
|
|
95
|
+
if (e.k === 'index') {
|
|
96
|
+
const c = baseConst(e.base);
|
|
97
|
+
const lo = c !== null ? baseOf.get(c) : undefined;
|
|
98
|
+
if (c !== null && lo !== undefined) {
|
|
99
|
+
// the base is replaced wholesale — its inner const must not reach the value path below
|
|
100
|
+
return { ...e, base: derived(lo, c - lo), idx: rewrite(e.idx) };
|
|
101
|
+
}
|
|
102
|
+
return { ...e, base: rewrite(e.base), idx: rewrite(e.idx) };
|
|
103
|
+
}
|
|
104
|
+
if (e.k === 'cast' && e.to.kind === 'ptr' && e.to.to.kind === 'struct') {
|
|
105
|
+
return e; // the struct-arrays base keeps its spelling — same refusal as baseConst's
|
|
106
|
+
}
|
|
107
|
+
if (e.k === 'const') {
|
|
108
|
+
const lo = coveringLo(e.value);
|
|
109
|
+
if (lo !== undefined) {
|
|
110
|
+
return {
|
|
111
|
+
k: 'cast',
|
|
112
|
+
to: { kind: 'int', width: 32, signed: true },
|
|
113
|
+
e: derived(lo, e.value - lo),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return e;
|
|
117
|
+
}
|
|
118
|
+
return mapExprChildren(e, rewrite);
|
|
119
|
+
};
|
|
120
|
+
const inits: BaseInit[] = [...baseName.entries()].map(([lo, name]) => ({
|
|
121
|
+
k: 'assign',
|
|
122
|
+
name,
|
|
123
|
+
value: {
|
|
124
|
+
k: 'cast',
|
|
125
|
+
to: { kind: 'ptr', to: { kind: 'int', width: 8, signed: false } },
|
|
126
|
+
e: { k: 'const', value: lo },
|
|
127
|
+
},
|
|
128
|
+
}));
|
|
129
|
+
const locals = [
|
|
130
|
+
...sfn.locals,
|
|
131
|
+
...[...baseName.values()].map((name) => ({
|
|
132
|
+
name,
|
|
133
|
+
type: { kind: 'ptr', to: { kind: 'int', width: 8, signed: false } } as SFn['locals'][number]['type'],
|
|
134
|
+
})),
|
|
135
|
+
];
|
|
136
|
+
// The body rebuild is `l3/hoist.ts`'s, shared with the two other passes that place into the
|
|
137
|
+
// leading base-init run. The ORDERING is not: `prepend` returns before the first-use query, so
|
|
138
|
+
// this pass takes the rebuild and abstains from the policy (see `BaseInitPlacement`).
|
|
139
|
+
//
|
|
140
|
+
// The DEFAULT is `prepend` — the cluster bases go ABOVE a run already there rather than being
|
|
141
|
+
// merged into it in first-use order — and it rests on a row, not on a compiler fact. Placing
|
|
142
|
+
// them in first-use order instead turns `synthetic:dmafield` (won by
|
|
143
|
+
// `signed/livebase/volatile/nearbase/initfirst`) from a MATCH into diff:5, measured 2026-08-26.
|
|
144
|
+
// The reading that goes with it — a cluster base is reached at 2+ addresses by construction, so
|
|
145
|
+
// its pool word is not "first touched late" — explains why first-use order is not obviously
|
|
146
|
+
// right, not why prepending is; which order the source wrote is per-function knowledge the asm
|
|
147
|
+
// does not carry. So it is a DEFAULT and not a decision: `rank.ts` offers the sunk ordering
|
|
148
|
+
// beside it as `/nearbase/sinkinit`, and the differ settles which one a function wanted.
|
|
149
|
+
const rewritten = sfn.body.map((s) => mapStmtExprs(s, rewrite));
|
|
150
|
+
const { body } = placeBaseLocals({ ...sfn, locals, body: rewritten }, inits, 'prepend');
|
|
151
|
+
return { ...sfn, locals, body };
|
|
152
|
+
}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// L3 re-spelling: spell a leaf base's constant subscript as a struct MEMBER, so the offset stays
|
|
2
|
+
// in the instruction's displacement instead of folding into the address the compiler materializes.
|
|
3
|
+
//
|
|
4
|
+
// THE COMPILER FACT this exists for is the one `l3/basecse.ts`'s header states in the imperative
|
|
5
|
+
// and cannot act on: on a target that declares `compilerBehaviors.foldsConstAddrOffset`, a
|
|
6
|
+
// constant SUBSCRIPT folds into the literal the address materializes, while an aggregate MEMBER
|
|
7
|
+
// offset stays in the memory operand.
|
|
8
|
+
//
|
|
9
|
+
// ((u16 *)0x3003468)[7] → .word 0x3003476 + ldrh r0, [r0]
|
|
10
|
+
// ((struct S *)0x3003468)->m14 → .word 0x3003468 + ldrh r0, [r0, #0xe]
|
|
11
|
+
//
|
|
12
|
+
// Both denote the same cell. Which one the source wrote is not derivable from the C, but the ASM
|
|
13
|
+
// says which one the compiler was given: an offset that reached the load's own displacement got
|
|
14
|
+
// there because nothing folded it, and only a member (or a named base — that is `/basefold`'s
|
|
15
|
+
// spelling) leaves it there. `l3/ast.ts`'s `index.operandOff` carries that displacement down from
|
|
16
|
+
// the lift, because the fold at L3 (`idx = idxVal + off / width`, structure/structure.ts) makes
|
|
17
|
+
// the two indistinguishable afterwards.
|
|
18
|
+
//
|
|
19
|
+
// THE ROSTER ALREADY OFFERS THE OTHER SOURCE OF THE SAME SHAPE. `/basefold` reads the identical
|
|
20
|
+
// evidence and answers it with a NAMED BASE (`u16 *p = (u16 *)C; p[7]`), which also keeps the
|
|
21
|
+
// displacement. The member is the second source and was in no fan; the two are different C and
|
|
22
|
+
// different register pressure, so both ride and the differ referees.
|
|
23
|
+
//
|
|
24
|
+
// SCOPE — a LEAF base only (a bare `addr` or a bare `const`), which is `l3/basecse.ts`'s
|
|
25
|
+
// `isHoistableBase` population. A computed base (`((u16 *)v0)[8]`) is out: the address is already
|
|
26
|
+
// held somewhere, so nothing folded into a literal and the evidence says nothing — that shape is
|
|
27
|
+
// `/addr-home`'s (structure/analysis.ts). It is a GATE rather than a collection filter so the
|
|
28
|
+
// refusal is attributable: `firstRejection` names it.
|
|
29
|
+
//
|
|
30
|
+
// NO DEVICE-REGISTER REFUSAL, which is a priced decision and not an omission. A base inside the
|
|
31
|
+
// target's declared `deviceRegisters` window is admitted like any other, so this pass will offer
|
|
32
|
+
// `((struct Off0 *)®_DMA3SAD)->m8` — a struct declared over the register file, carrying no
|
|
33
|
+
// qualifier. Three measurements say a refusal there buys nothing for that price.
|
|
34
|
+
//
|
|
35
|
+
// REACH. Every device-window base reaching this table is already refused by `no-operand-off`
|
|
36
|
+
// (49 of them map-less, 2 map-ful, over all four checkouts — 1417 structured functions map-less,
|
|
37
|
+
// 398 map-ful): the DMA idiom writes offset 0 as well as offset 8, and offset 0 leaves no
|
|
38
|
+
// displacement. A window refusal removes ZERO further bases in either configuration.
|
|
39
|
+
//
|
|
40
|
+
// PREMISE. It would be refusing a dropped qualifier, and there is none to drop: `/volatile`
|
|
41
|
+
// wraps the base in a CAST that `non-leaf-base` refuses, so the two levers cannot compose and
|
|
42
|
+
// the tree this pass is handed is unqualified in both configurations. Nor is a tie-break lost —
|
|
43
|
+
// `deviceVolatileClaims` counts only qualifiers a tree already asserts, so an unqualified tree
|
|
44
|
+
// scores zero whichever way it is spelled.
|
|
45
|
+
//
|
|
46
|
+
// CONFIGURATION. A numeric window is read through `addrConst`, and a symbol map turns a device
|
|
47
|
+
// address into a named `addr` no range can see. So such a refusal fires map-less and not
|
|
48
|
+
// map-ful on the SAME function — and the real tier runs map-ful. A refusal a symbol map
|
|
49
|
+
// switches off is not a refusal.
|
|
50
|
+
//
|
|
51
|
+
// What referees it instead is the differ, and `offmember.test.ts` pins the price on real compiled
|
|
52
|
+
// asm: on a DMACNT spin that never touches DMASAD — the one shape `no-operand-off` does not cover
|
|
53
|
+
// — the member spelling is offered and LOSES, 52 candidates, best
|
|
54
|
+
// `unsigned/derived-home/livebase/volatile: 4` against `/offmember`'s 5. A row where it WINS over
|
|
55
|
+
// MMIO is what would earn a gate here, together with a window reading that survives a symbol map.
|
|
56
|
+
//
|
|
57
|
+
// NOT AN EXTENSION OF raise/struct-arrays.ts. That pass mints an element struct off the `mul`/`shl`
|
|
58
|
+
// STRIDE idiom in the machine code, and this shape has no scale at all — the subscript is a
|
|
59
|
+
// constant and the base is a leaf, so there is no stride to read. It is a SPELLING of a tree
|
|
60
|
+
// structuring already produced, which is what puts it in `l3/` beside the other re-spellings.
|
|
61
|
+
//
|
|
62
|
+
// SOUNDNESS. The member offset is `idx * width`, the access's own byte offset from the base — never
|
|
63
|
+
// `operandOff`, which is EVIDENCE about where the offset travelled and is only part of it when the
|
|
64
|
+
// address carried the rest. So the respelled node addresses the same cell by construction, and the
|
|
65
|
+
// one way it could not is a layout C cannot reproduce — a member C would seat somewhere other than
|
|
66
|
+
// where the asm read it. `unspellable-layout` refuses that, and it is the table's only sound gate.
|
|
67
|
+
//
|
|
68
|
+
// REFUSAL. `spellOperandMembers` returns `null` when no base was admitted, so a function with no
|
|
69
|
+
// eligible site contributes no candidate and costs nothing.
|
|
70
|
+
//
|
|
71
|
+
// WHAT THE DECLARATION GOVERNS, and it is narrower than it looks: the CONSTANT SUBSCRIPTS this
|
|
72
|
+
// pass grouped under one base expression, and nothing else. A sibling
|
|
73
|
+
// access through the same address that this pass has no member spelling for — a variable
|
|
74
|
+
// subscript, a `lead`-prefixed one — is left exactly as it was, keeping its own cast, and a
|
|
75
|
+
// struct-array element off the same numeric constant reaches the address through a different base
|
|
76
|
+
// expression and so a different key entirely. So one address really can leave here spelled two
|
|
77
|
+
// ways, and `offmember.test.ts` emits exactly this pair rather than describing it:
|
|
78
|
+
// `((struct Off0 *)50345232)->m16` beside `((s32 *)50345232)[a0]`.
|
|
79
|
+
//
|
|
80
|
+
// THAT IS UGLY AND IT IS NOT UNSOUND, and only the second half is a gate's business. Every
|
|
81
|
+
// access carries its own cast, so no access reads a byte it did not read before; the seating check
|
|
82
|
+
// below judges the accesses the declaration is built FROM, which are exactly the accesses
|
|
83
|
+
// respelled through it, so its argument is over a total population of what it governs. The
|
|
84
|
+
// contradiction is between two FICTIONAL TYPES over one address, which is a readability claim —
|
|
85
|
+
// `quality`'s clientele, not a gate's.
|
|
86
|
+
//
|
|
87
|
+
// AND THE TIGHTER RULE IS PRICED, so it is not re-derived from scratch. Refusing a base whose
|
|
88
|
+
// siblings this pass cannot spell removes `a:gCallbackQueue` from
|
|
89
|
+
// `kleod:ProcessInputAndUpdateEntities` (asserted by dumping that row's winning source both ways:
|
|
90
|
+
// 9 `struct Off*` declarations become 2, and the survivor is `gUnk_03004C20`) and COSTS that row
|
|
91
|
+
// score, while protecting no row anywhere in the artifact. NO BRACKET IS QUOTED, and that is a
|
|
92
|
+
// discipline rather than a gap: a bracket is a claim about the whole tree, so BOTH endpoints move
|
|
93
|
+
// whenever anything else does. Read the unablated endpoint off the committed artifact — where
|
|
94
|
+
// `kleod:ProcessInputAndUpdateEntities` scores 211 — and re-measure the ablated one before writing
|
|
95
|
+
// a pair anywhere.
|
|
96
|
+
// "PROTECTS NO ROW" IS A CENSUS AND NOT A SAMPLE, and the population is small because this is an
|
|
97
|
+
// extra GATE: it only ever removes candidates, so only a row whose winner carries `/offmember` can
|
|
98
|
+
// move at all. THAT POPULATION IS A QUERY, not a count to keep in step by hand — the rows of
|
|
99
|
+
// `apps/benchmark/results/results.json` whose `asmlift.candidateLabel` contains `offmember` — and
|
|
100
|
+
// it grows with the corpus, so re-run it before repeating the result. It held eight rows besides
|
|
101
|
+
// `ProcessInputAndUpdateEntities` when the census ran, all unmoved with the rule on:
|
|
102
|
+
// `synthetic:basecell`, `synthetic:bgfixed`, `synthetic:foldsink`, `sa3:sub_802DFC8` and
|
|
103
|
+
// `sa3:sub_803213C` all still MATCH, `kleod:RollRandomLevelVariant` 18,
|
|
104
|
+
// `kleod:CountCollectedGems` 290, `kleod:UpdateWorldMapNodeAnim` 157, each the artifact's own
|
|
105
|
+
// number. A gate needs a row it protects. (One side effect worth recording: with the rule on, PI's
|
|
106
|
+
// winner becomes `unsigned/setup-args/no-ptr-elem/offmember` — an arm that wins no REAL row wins
|
|
107
|
+
// there. It is an ablation's artifact, not a reason to ship either.)
|
|
108
|
+
//
|
|
109
|
+
// ALL-OR-NOTHING PER FUNCTION, and that is a PRICE rather than a property. Every admitted base is
|
|
110
|
+
// respelled together in one candidate, so a function with two admitted bases where the target
|
|
111
|
+
// folded one and kept the other in the operand has no reachable spelling — the coverage hole
|
|
112
|
+
// `l3/ptrfield.ts` measures for its fields. `l3/basecse.ts` is NOT a second witness to it but the
|
|
113
|
+
// counterexample: its `UNFOLDED_GATES` cuts across `LIVEBASE_BLOCK_GATES` rather than refining it,
|
|
114
|
+
// so a partial answer IS reachable there — a proper nonempty subset on 13 of the agbcc rows the
|
|
115
|
+
// artifact carried when that census ran,
|
|
116
|
+
// one of three bases on `synthetic:dmascope` — and that is the way out of this hole too: not a
|
|
117
|
+
// per-base fork, but a SECOND PREDICATE separating the bases the differ has to choose between.
|
|
118
|
+
// The per-base fork is 2^n and the family's standing price for forking ten refusal sites per site
|
|
119
|
+
// was 1024x, so the arbitrary subsets stay unreachable until a row demands one.
|
|
120
|
+
// Measured over klonoa's 69
|
|
121
|
+
// lifting functions (lift → idioms → raise → structure, one pass per configuration): 36 admitted
|
|
122
|
+
// bases over 25 functions map-less and 38 over 24 map-ful, so the multi-base functions really are
|
|
123
|
+
// the majority of the surplus and a missing subset has somewhere to live. The other three
|
|
124
|
+
// checkouts admit 32 bases over 32 functions (af), 10 over 10 (marioparty3) and 2 over 2
|
|
125
|
+
// (snowboardkids2), all map-less.
|
|
126
|
+
import { nextStructIndex } from '../ir/struct-names';
|
|
127
|
+
import { type IrType, T, scalarTypeForAccess } from '../ir/types';
|
|
128
|
+
import type { Expr, SFn, StructType } from './ast';
|
|
129
|
+
import { mapExprChildren, mapStmtExprs, walkExprs } from './ast';
|
|
130
|
+
import { type Gate, firstRejection } from './gates';
|
|
131
|
+
|
|
132
|
+
/** One observed constant-subscript access through a base. */
|
|
133
|
+
interface Site {
|
|
134
|
+
/** the member's byte offset from the base — `idx * width`, the address the node denotes */
|
|
135
|
+
off: number;
|
|
136
|
+
width: number;
|
|
137
|
+
signed: boolean;
|
|
138
|
+
/** the displacement the instruction carried, when it carried one (l3/ast.ts `operandOff`) */
|
|
139
|
+
operandOff?: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The identity of an access's base, for grouping. Leaf bases key by value; everything else keys
|
|
143
|
+
* by its printed shape, so a non-leaf base still reaches the table and is refused by NAME. */
|
|
144
|
+
const baseKey = (e: Expr): string =>
|
|
145
|
+
e.k === 'addr' ? `a:${e.name}` : e.k === 'const' ? `c:${e.value}` : `x:${JSON.stringify(e)}`;
|
|
146
|
+
|
|
147
|
+
/** What the gates judge: one base and every constant-subscript access through it. */
|
|
148
|
+
export interface OffmemberBase {
|
|
149
|
+
key: string;
|
|
150
|
+
/** the base is a bare `addr`/`const` — the population whose address the compiler materializes */
|
|
151
|
+
leafBase: boolean;
|
|
152
|
+
/** some access through this base carries no memory-operand displacement, so nothing says its
|
|
153
|
+
* offset was ever anywhere but the address */
|
|
154
|
+
missingOperandOff: boolean;
|
|
155
|
+
/** some access's subscript is WIDER than the displacement the instruction carried — the address
|
|
156
|
+
* expression held the rest, and how that split maps back onto one member is not measured */
|
|
157
|
+
indexCarriesMore: boolean;
|
|
158
|
+
/** the accesses cannot be declared as a plain C struct seating each at its own offset */
|
|
159
|
+
unspellableLayout: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const OFFMEMBER_GATES: readonly Gate<OffmemberBase>[] = [
|
|
163
|
+
{
|
|
164
|
+
id: 'non-leaf-base',
|
|
165
|
+
why: 'a computed base is already held somewhere, so nothing folded into a literal',
|
|
166
|
+
// A REACH argument, which is why it claims no soundness and owes no guard — and it is the
|
|
167
|
+
// only thing standing between this pass and a dropped qualifier, which the header's
|
|
168
|
+
// no-device-refusal paragraph is the other half of. A `/volatile` base reaches L3 as a CAST
|
|
169
|
+
// over the constant, so this gate excludes it as a side effect of excluding every computed
|
|
170
|
+
// base, and the natural widening ("a cast of a leaf const is still a leaf") would respell a
|
|
171
|
+
// qualified access as an unqualified member with no test failing. Nothing downstream would
|
|
172
|
+
// object. So the widening is forbidden HERE, at this gate, and a round that wants it owes a
|
|
173
|
+
// qualifier-preserving spelling first.
|
|
174
|
+
sound: false,
|
|
175
|
+
rejects: (c) => !c.leafBase,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: 'no-operand-off',
|
|
179
|
+
why: 'an offset that never reached the instruction is not evidence of a member',
|
|
180
|
+
sound: false,
|
|
181
|
+
rejects: (c) => c.missingOperandOff,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: 'index-carries-more',
|
|
185
|
+
why: 'the address carried part of the offset, and that decomposition is unmeasured',
|
|
186
|
+
sound: false,
|
|
187
|
+
rejects: (c) => c.indexCarriesMore,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: 'unspellable-layout',
|
|
191
|
+
why: 'a struct C cannot seat at the observed offsets would address different bytes',
|
|
192
|
+
sound: true,
|
|
193
|
+
guardedBy: 'offmember.test.ts: a base whose accesses no plain struct can seat is refused',
|
|
194
|
+
rejects: (c) => c.unspellableLayout,
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
/** The members this base is spelled through: one field per distinct offset. Shared by the seating
|
|
199
|
+
* PREDICATE and the layout BUILDER so the two cannot judge one field set and declare another —
|
|
200
|
+
* and, since `seatable` now refuses an offset whose views disagree on width OR signedness, every
|
|
201
|
+
* surviving offset has exactly one view and the pick below is an identity on an admitted base.
|
|
202
|
+
* It is kept because this runs on ABLATED tables too, where the gate is gone and something still
|
|
203
|
+
* has to choose; preferring the signed view keeps that choice the widest one. */
|
|
204
|
+
function membersOf(sites: readonly Site[]): Site[] {
|
|
205
|
+
const byOff = new Map<number, Site>();
|
|
206
|
+
for (const s of sites) {
|
|
207
|
+
const prev = byOff.get(s.off);
|
|
208
|
+
if (!prev || (s.signed && !prev.signed)) {
|
|
209
|
+
byOff.set(s.off, s);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return [...byOff.values()].sort((a, b) => a.off - b.off);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Can plain C seat every member at the offset the asm read it at, AND does the member read the
|
|
216
|
+
* same value the access did?
|
|
217
|
+
*
|
|
218
|
+
* FOUR ways it cannot, and the first three are the same defect — a field the declaration would
|
|
219
|
+
* place somewhere other than where the access reads: a NEGATIVE offset, which no member has; two
|
|
220
|
+
* views of ONE offset at different widths (a union); and two offsets whose byte ranges collide.
|
|
221
|
+
*
|
|
222
|
+
* THE FOURTH IS NOT ABOUT PLACEMENT, and it is the easy one to leave out: two views of one
|
|
223
|
+
* offset at one width but
|
|
224
|
+
* DIFFERENT SIGNEDNESS (`ldrb` and `ldrsb` at the same address). One member has one type, so
|
|
225
|
+
* respelling both through it changes what one of the two READS — `scalarTypeForAccess` honours
|
|
226
|
+
* signedness at widths 1 and 2, so an unsigned read becomes sign-extending. That is a value
|
|
227
|
+
* change rather than a spelling change, and the differ can referee it only by luck: a masked or
|
|
228
|
+
* compared result compiles to the same bytes while the published C says something the asm does
|
|
229
|
+
* not. C spells it as a union, which is not a member, so it is refused exactly as the width union
|
|
230
|
+
* is. `l3/basecse.ts` keys `(base, width, signedness)` for the same reason, and `l3/typing.ts`
|
|
231
|
+
* states the rule in the imperative: signedness counts wherever the access extends.
|
|
232
|
+
*
|
|
233
|
+
* Natural ALIGNMENT is not among them, and that is an invariant rather than an omission: a
|
|
234
|
+
* member's offset here is the node's own `idx * width`, so it is a multiple of its width by
|
|
235
|
+
* construction and C's own alignment can always place it. A clause for it would have no
|
|
236
|
+
* inhabitant.
|
|
237
|
+
*
|
|
238
|
+
* This is what `unspellable-layout` asks, and the gate is the ONLY refusal — `layoutFor` below
|
|
239
|
+
* builds whatever it is handed, so ablating the gate really does emit the mislaid struct rather
|
|
240
|
+
* than quietly declining beside it. */
|
|
241
|
+
function seatable(sites: readonly Site[]): boolean {
|
|
242
|
+
let cursor = 0;
|
|
243
|
+
const views = new Map<number, Site>();
|
|
244
|
+
for (const s of sites) {
|
|
245
|
+
const v = views.get(s.off);
|
|
246
|
+
if (v !== undefined && (v.width !== s.width || v.signed !== s.signed)) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
views.set(s.off, s);
|
|
250
|
+
}
|
|
251
|
+
for (const m of membersOf(sites)) {
|
|
252
|
+
if (m.off < cursor) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
cursor = m.off + m.width;
|
|
256
|
+
}
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The struct declaration for a base: each member at its own offset, gaps filled with an explicit
|
|
261
|
+
* `u8[N]` pad so the declaration reproduces the observed offsets on its own — the discipline
|
|
262
|
+
* raise/structs.ts and raise/struct-arrays.ts share. No trailing pad and no `size`: the spelling
|
|
263
|
+
* is `->m`, so `sizeof` is never taken. */
|
|
264
|
+
function layoutFor(name: string, sites: readonly Site[]): StructType {
|
|
265
|
+
const fields: StructType['fields'] = [];
|
|
266
|
+
let cursor = 0;
|
|
267
|
+
let pad = 0;
|
|
268
|
+
for (const m of membersOf(sites)) {
|
|
269
|
+
if (cursor < m.off) {
|
|
270
|
+
fields.push({ off: cursor, type: T.array(T.u(8), m.off - cursor), name: `_pad${pad++}` });
|
|
271
|
+
}
|
|
272
|
+
fields.push({ off: m.off, type: scalarTypeForAccess(m.width, m.signed), name: `m${m.off}` });
|
|
273
|
+
cursor = m.off + m.width;
|
|
274
|
+
}
|
|
275
|
+
return { name, fields };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Every CONSTANT-SUBSCRIPT `index` access, grouped by base, in first-appearance order — the
|
|
279
|
+
* accesses this pass has a member spelling for, which are exactly the ones the declaration it
|
|
280
|
+
* builds will govern. A sibling with a variable subscript or a `lead` is not one of them and is
|
|
281
|
+
* not counted: it keeps its own cast and this pass never rewrites it (see the header's
|
|
282
|
+
* "WHAT THE DECLARATION GOVERNS"). */
|
|
283
|
+
function collect(sfn: SFn): { order: string[]; sites: Map<string, Site[]> } {
|
|
284
|
+
const order: string[] = [];
|
|
285
|
+
const sites = new Map<string, Site[]>();
|
|
286
|
+
for (const e of walkExprs(sfn.body)) {
|
|
287
|
+
if (e.k !== 'index' || e.idx.k !== 'const' || e.lead !== undefined) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const k = baseKey(e.base);
|
|
291
|
+
let list = sites.get(k);
|
|
292
|
+
if (!list) {
|
|
293
|
+
order.push(k);
|
|
294
|
+
list = [];
|
|
295
|
+
sites.set(k, list);
|
|
296
|
+
}
|
|
297
|
+
list.push({ off: e.idx.value * e.width, width: e.width, signed: e.signed, operandOff: e.operandOff });
|
|
298
|
+
}
|
|
299
|
+
return { order, sites };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** The keys `gates` admits, with the struct each one is spelled through. */
|
|
303
|
+
function admit(
|
|
304
|
+
sfn: SFn,
|
|
305
|
+
gates: readonly Gate<OffmemberBase>[],
|
|
306
|
+
firstName: number,
|
|
307
|
+
): Map<string, { struct: StructType; type: IrType }> {
|
|
308
|
+
const { order, sites } = collect(sfn);
|
|
309
|
+
const out = new Map<string, { struct: StructType; type: IrType }>();
|
|
310
|
+
let n = firstName;
|
|
311
|
+
for (const key of order) {
|
|
312
|
+
const list = sites.get(key)!;
|
|
313
|
+
const rejected = firstRejection(gates, {
|
|
314
|
+
key,
|
|
315
|
+
leafBase: key.startsWith('a:') || key.startsWith('c:'),
|
|
316
|
+
missingOperandOff: list.some((s) => s.operandOff === undefined),
|
|
317
|
+
indexCarriesMore: list.some((s) => s.operandOff !== undefined && s.operandOff !== s.off),
|
|
318
|
+
unspellableLayout: !seatable(list),
|
|
319
|
+
});
|
|
320
|
+
if (rejected === null) {
|
|
321
|
+
const struct = layoutFor(`Off${n}`, list);
|
|
322
|
+
out.set(key, { struct, type: T.ptr(T.struct(struct.name, struct.fields)) });
|
|
323
|
+
n++;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** The gate table an ablation swaps out. Optional, so a caller gets the shipped table by default.
|
|
330
|
+
* The pass needs nothing else from the target: the fold this exists for is `foldsConstAddrOffset`
|
|
331
|
+
* and rank.ts asks that before offering the axis at all. */
|
|
332
|
+
export interface OffmemberOpts {
|
|
333
|
+
readonly gates?: readonly Gate<OffmemberBase>[];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** The census without the rewrite, for a caller comparing what two tables would admit. */
|
|
337
|
+
export function offmemberBases(sfn: SFn, opts: OffmemberOpts = {}): readonly string[] {
|
|
338
|
+
return [...admit(sfn, opts.gates ?? OFFMEMBER_GATES, 0).keys()];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Re-spell every admitted base's constant subscripts as members of a synthesized struct.
|
|
342
|
+
* `null` when nothing is admitted — the axis then contributes no candidate. */
|
|
343
|
+
export function spellOperandMembers(sfn: SFn, opts: OffmemberOpts = {}): SFn | null {
|
|
344
|
+
// Past EVERY `Off<N>` the tree already carries, never the first free one (`ir/struct-names.ts`,
|
|
345
|
+
// shared with the other two minters, which also records that this scan returns 0 on every
|
|
346
|
+
// corpus function). The prefix is what keeps this pass clear of raise/structs.ts's `Struct<N>`
|
|
347
|
+
// and raise/struct-arrays.ts's `Elem<N>`; the monotone scan is what keeps it clear of itself.
|
|
348
|
+
const seed = nextStructIndex(
|
|
349
|
+
(sfn.structs ?? []).map((s) => s.name),
|
|
350
|
+
'Off',
|
|
351
|
+
);
|
|
352
|
+
const admitted = admit(sfn, opts.gates ?? OFFMEMBER_GATES, seed);
|
|
353
|
+
if (admitted.size === 0) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
const rewrite = (e: Expr): Expr => {
|
|
357
|
+
if (e.k === 'index' && e.idx.k === 'const' && e.lead === undefined) {
|
|
358
|
+
const hit = admitted.get(baseKey(e.base));
|
|
359
|
+
if (hit) {
|
|
360
|
+
return { k: 'field', base: { k: 'cast', to: hit.type, e: e.base }, name: `m${e.idx.value * e.width}` };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return mapExprChildren(e, rewrite);
|
|
364
|
+
};
|
|
365
|
+
const structs = [...(sfn.structs ?? []), ...[...admitted.values()].map((a) => a.struct)].sort((a, b) =>
|
|
366
|
+
a.name.localeCompare(b.name),
|
|
367
|
+
);
|
|
368
|
+
// `mapStmtExprs` recurses into nested statement lists, so one call per top-level statement
|
|
369
|
+
// covers the whole body.
|
|
370
|
+
return { ...sfn, structs, body: sfn.body.map((st) => mapStmtExprs(st, rewrite)) };
|
|
371
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// L3 re-spelling lever: park incoming ARGUMENTS first in the entry straight-line prefix.
|
|
2
|
+
//
|
|
3
|
+
// A copy of an incoming parameter into a local (`v = a1`) reproduces the register park the
|
|
4
|
+
// compiler performed to free a caller-save register (`mov ip, r1`). The park instruction lifts
|
|
5
|
+
// to pure SSA aliasing — no op, no position — so the emitted order falls out of block emission
|
|
6
|
+
// (materialized statements first, edge copies last), while the compiler may have parked BEFORE
|
|
7
|
+
// any of those statements ran (hipress homes its counter in ip before loading a byte into the
|
|
8
|
+
// vacated r1). Both orders are legitimate C for the same asm; this lever emits the park-first
|
|
9
|
+
// sibling and the differ referees.
|
|
10
|
+
//
|
|
11
|
+
// SCOPE (decline over approximate): only plain assigns in the ENTRY straight-line prefix (the
|
|
12
|
+
// leading run of assigns) move; a park's RHS must be pure over PARAMETERS AND CONSTANTS through
|
|
13
|
+
// scalar nodes only (var/const/un/bin/cast — a memory read or a call would be re-scheduled, not
|
|
14
|
+
// re-spelled), so a constant initializer qualifies as a park and a param leaf is NOT required —
|
|
15
|
+
// what this lever moves is the leading run's ORDER, and a constant is as free of position as a
|
|
16
|
+
// parked register is; and a park never crosses a statement that writes a name it reads, reads or
|
|
17
|
+
// writes its destination (`&v` counts as touching v), or carries an effect. Relative order — of the
|
|
18
|
+
// parks and of everything else — is preserved. Declines (null) when nothing moves.
|
|
19
|
+
//
|
|
20
|
+
// WHAT THE DESTINATION TEST IS, stated because it is narrower than "a local": the target is checked
|
|
21
|
+
// against the PARAMS alone, so every other name an `assign` can carry qualifies — and structure.ts
|
|
22
|
+
// spells a store to a bare scalar GLOBAL as an `assign` like any other, so such a store parks too.
|
|
23
|
+
// The crossing checks are NAME-KEYED, so a crossed statement that reaches the destination (or a
|
|
24
|
+
// name the park reads) through an ALIAS rather than by name is invisible to them — the same
|
|
25
|
+
// name-keyed model every lever at this level defers aliasing to.
|
|
26
|
+
//
|
|
27
|
+
// The kmc hipress residual is this axis's OTHER projection — its keep-load renders first while
|
|
28
|
+
// gcc2.7.2 schedules it last — so a second inhabitant consolidates both into one entry-prefix
|
|
29
|
+
// ordering lever rather than growing a sibling.
|
|
30
|
+
import type { Expr, SFn, Stmt } from './ast';
|
|
31
|
+
import { exprChildren, exprHasEffect } from './ast';
|
|
32
|
+
|
|
33
|
+
type Assign = Extract<Stmt, { k: 'assign' }>;
|
|
34
|
+
|
|
35
|
+
// `addr` counts as touching its name: `foo(&v)` may read or write v through the pointer, so a
|
|
36
|
+
// park must treat it exactly like a direct read-and-write of v.
|
|
37
|
+
const readVars = (e: Expr, acc: Set<string> = new Set()): Set<string> => {
|
|
38
|
+
if (e.k === 'var' || e.k === 'addr') {
|
|
39
|
+
acc.add(e.name);
|
|
40
|
+
}
|
|
41
|
+
for (const c of exprChildren(e)) {
|
|
42
|
+
readVars(c, acc);
|
|
43
|
+
}
|
|
44
|
+
return acc;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const pureOverParams = (e: Expr, params: ReadonlySet<string>): boolean => {
|
|
48
|
+
switch (e.k) {
|
|
49
|
+
case 'var':
|
|
50
|
+
return params.has(e.name);
|
|
51
|
+
case 'const':
|
|
52
|
+
return true;
|
|
53
|
+
case 'un':
|
|
54
|
+
case 'cast':
|
|
55
|
+
return pureOverParams(e.e, params);
|
|
56
|
+
case 'bin':
|
|
57
|
+
return pureOverParams(e.l, params) && pureOverParams(e.r, params);
|
|
58
|
+
default:
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export function parkParamsFirst(sfn: SFn): SFn | null {
|
|
64
|
+
const params = new Set(sfn.params.map((p) => p.name));
|
|
65
|
+
let n = 0;
|
|
66
|
+
while (n < sfn.body.length && sfn.body[n].k === 'assign') {
|
|
67
|
+
n++;
|
|
68
|
+
}
|
|
69
|
+
const prefix = sfn.body.slice(0, n) as Assign[];
|
|
70
|
+
const parks: Assign[] = [];
|
|
71
|
+
const rest: Assign[] = [];
|
|
72
|
+
for (const st of prefix) {
|
|
73
|
+
// `rest` is exactly what this park would cross — a REFUSED earlier park is in it, so a later
|
|
74
|
+
// park is re-checked against it like any other crossed statement.
|
|
75
|
+
if (!params.has(st.name) && pureOverParams(st.value, params)) {
|
|
76
|
+
const reads = readVars(st.value);
|
|
77
|
+
if (
|
|
78
|
+
rest.every(
|
|
79
|
+
(c) =>
|
|
80
|
+
!exprHasEffect(c.value) && // a call can write anything an escaped address reaches
|
|
81
|
+
!reads.has(c.name) &&
|
|
82
|
+
c.name !== st.name &&
|
|
83
|
+
!readVars(c.value).has(st.name),
|
|
84
|
+
)
|
|
85
|
+
) {
|
|
86
|
+
parks.push(st);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
rest.push(st);
|
|
91
|
+
}
|
|
92
|
+
if (parks.length === 0 || parks.every((p, i) => prefix[i] === p)) {
|
|
93
|
+
return null; // nothing to move, or the parks already lead the prefix
|
|
94
|
+
}
|
|
95
|
+
return { ...sfn, body: [...parks, ...rest, ...sfn.body.slice(n)] };
|
|
96
|
+
}
|