@asmlift/core 0.10.0 → 0.11.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/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +33 -12
- package/src/backend/cpp.ts +1 -0
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +17 -5
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +73 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +2 -1
- package/src/frontend/call-plan.ts +197 -45
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/device-pins.ts +18 -8
- package/src/frontend/frame-objects.ts +504 -61
- package/src/frontend/frontend.ts +20 -0
- package/src/frontend/jump-table.ts +35 -20
- package/src/frontend/mips.ts +226 -8
- package/src/frontend/ppc.ts +26 -2
- package/src/frontend/register-struct-call.ts +1 -86
- package/src/frontend/ssa.ts +263 -1
- package/src/frontend/stackargs.ts +95 -17
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +410 -61
- package/src/ir/alias.ts +29 -8
- package/src/ir/bits.ts +1081 -66
- package/src/ir/core.ts +154 -4
- package/src/ir/entry.ts +3 -5
- package/src/ir/opcodes.ts +28 -0
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/verify.ts +16 -0
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +294 -2
- package/src/l3/basecse.ts +35 -10
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +34 -6
- package/src/l3/dce.ts +52 -8
- package/src/l3/ptrcell.ts +2 -0
- package/src/l3/regspell.ts +2 -0
- package/src/l3/reindex.ts +13 -4
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/tailmerge.ts +2 -0
- package/src/pipeline.ts +507 -23
- package/src/proto-context.ts +135 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +1 -1
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/offsetnames.ts +412 -10
- package/src/raise/pre-recovery.ts +8 -5
- package/src/raise/pre-structure.ts +24 -5
- package/src/raise/reroll.ts +4 -24
- package/src/raise/shortcircuit.ts +192 -23
- package/src/raise/unpre.ts +594 -0
- package/src/rank-variations.ts +97 -4
- package/src/rank.ts +258 -43
- package/src/structure/analysis.ts +56 -10
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +46 -1
- package/src/structure/structure.ts +4221 -368
- package/src/structure/switch-recover.ts +345 -26
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +52 -15
- package/src/target.ts +269 -8
- package/src/trace.ts +28 -14
- package/src/variation-definitions.ts +207 -6
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +63 -2
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// L3 respell variation `/case-end-goto`: a search loop's hit flag, read once after its loop where the
|
|
2
|
+
// code after the loop ends the switch arm the loop sits in, rewritten as the source's `goto` to that
|
|
3
|
+
// arm's end — the case-end goto (l3/ast.ts `caseEndGoto`, docs/asmlift-101.md §2.7):
|
|
4
|
+
//
|
|
5
|
+
// f = 0; for (j…) {
|
|
6
|
+
// for (j…) { if (hit) { s(j); goto case_end; }
|
|
7
|
+
// if (hit) { s(j); f = 1; break; } }
|
|
8
|
+
// } → k(i);
|
|
9
|
+
// if (f == 0) { k(i); } case_end:
|
|
10
|
+
// break; break;
|
|
11
|
+
//
|
|
12
|
+
// The structurer writes the flag where a two-level exit's second target lies past the code after
|
|
13
|
+
// the loop (structure.ts `TwoLevelExit`, `emitForeverExit`). Where that target is the end of the
|
|
14
|
+
// switch arm, agbcc threads the source's goto past the loop around the switch (jump.c
|
|
15
|
+
// `follow_jumps`), and on agbcc, KMC, gcc 2.7.2 and IDO the goto compiles to the source's bytes
|
|
16
|
+
// where the flag never does (`synthetic:sw_hitexit`); but inside asmlift's own candidates the flag
|
|
17
|
+
// can still score closer (gcc 2.7.2), so both are emitted, the differ referees, and a tie goes to
|
|
18
|
+
// the candidate without the goto (rank.ts `gotoCount`).
|
|
19
|
+
//
|
|
20
|
+
// The structurer is not the only writer the rewrite reaches: it reads the tree, not who built it,
|
|
21
|
+
// so a flag the source declared and the compiler kept is rewritten too where it has the same shape
|
|
22
|
+
// (IDO keeps a `u8 found` a source sets to 2 in a case arm, and lifts it as that flag). That is
|
|
23
|
+
// sound for the reason below, which reads only the tree, and ranked like the rest.
|
|
24
|
+
//
|
|
25
|
+
// SOUND OVER THE TREE ALONE. `f` is a local mentioned only by `f = 0;` right before the loop, by
|
|
26
|
+
// each hit's `f = c; break;` (c a nonzero constant, the `break` the loop's own: reached from the
|
|
27
|
+
// loop's body through `if`s only) and by the test right after the loop (`CASE_END_GOTO_GATES`
|
|
28
|
+
// `flag-elsewhere`). So after the loop `f == 0` exactly when it left by any other way than a hit,
|
|
29
|
+
// and `X` runs exactly then — which is what `goto L;` at each hit and `X` unguarded after the loop
|
|
30
|
+
// run. After `X` both spellings go on to whatever follows the `if`, and that is the arm's end with
|
|
31
|
+
// nothing run on the way (`target-not-arm-end`): a switch `break;` right after the `if`, which takes
|
|
32
|
+
// the label, or the `if` last in a list whose falling off runs nothing before leaving the switch,
|
|
33
|
+
// which gets a labelled `break;` appended. `L: break;` leaves the switch as falling off the arm
|
|
34
|
+
// does. The label sits in the loop's own list, no loop between it and its arm, and
|
|
35
|
+
// the hits' `break`s are reached through `if`s only, so no switch stands between them and it: the
|
|
36
|
+
// form `caseGotoViolation` holds every case-end goto to.
|
|
37
|
+
//
|
|
38
|
+
// A RESPELL, NOT A STRUCTURE VARIATION — the third fork of docs/level-tower.md. The rewrite reads
|
|
39
|
+
// nothing but the flag's tree, so asked of the structured tree it reaches every site a structure
|
|
40
|
+
// option inside `emitForeverExit` would. A structure variation doubles the settings every
|
|
41
|
+
// enumeration walks even where it changes no tree, which level-tower.md measures on this one; the
|
|
42
|
+
// respell costs one walk of each structured tree, and nothing more where no flag is read after its
|
|
43
|
+
// loop. What it gives up is composition with the other pre-respell variation (`/unmerge` runs on
|
|
44
|
+
// the structured tree beside it, not on its output); no row has asked for both.
|
|
45
|
+
//
|
|
46
|
+
// Outer sites first, and an inner site whose `if` the outer rewrite leaves right before the outer
|
|
47
|
+
// label takes that label, so two search loops in sequence into one case end share one `L:`.
|
|
48
|
+
import { type Expr, type Loop, type SFn, type Stmt, type SwitchCase, isLoop, mapStmtLists, stmtChildren } from './ast';
|
|
49
|
+
import { type Gate, firstRejection } from './gates';
|
|
50
|
+
import { localMentions, readsOf } from './mentions';
|
|
51
|
+
|
|
52
|
+
/** A flag read after its loop that the rewrite would replace (`CASE_END_GOTO_GATES`). */
|
|
53
|
+
export interface CaseEndSite {
|
|
54
|
+
/** what runs after the `if (f == 0)` is not the arm's end: no switch `break;` follows it, and it
|
|
55
|
+
* is not last in a list whose falling off leaves the switch with nothing run. Outside a switch
|
|
56
|
+
* arm, or in a loop inside one, neither can hold (a switch `break;` sits only in an arm with no
|
|
57
|
+
* loop between, `refuseMisboundBreak`), so this is also the rule that keeps such a flag */
|
|
58
|
+
readonly targetNotArmEnd: boolean;
|
|
59
|
+
/** `f` is a param or global, or mentioned anywhere but its `f = 0;`, its hits and its one test */
|
|
60
|
+
readonly flagElsewhere: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** When a flag read after its loop is rewritten as a case-end goto. The argument is the file's. */
|
|
64
|
+
export const CASE_END_GOTO_GATES: readonly Gate<CaseEndSite>[] = [
|
|
65
|
+
{
|
|
66
|
+
id: 'target-not-arm-end',
|
|
67
|
+
why: 'the goto skips only the guarded code, so what follows it must leave the switch with nothing run',
|
|
68
|
+
sound: true,
|
|
69
|
+
guardedBy: [
|
|
70
|
+
'case-end-goto.test.ts: a hit whose target is a join running code keeps its flag',
|
|
71
|
+
'case-end-goto.test.ts: a flag read outside a switch arm, or in a loop inside one, keeps its flag',
|
|
72
|
+
],
|
|
73
|
+
rejects: (c) => c.targetNotArmEnd,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'flag-elsewhere',
|
|
77
|
+
why: 'the flag is zero after the loop exactly when no hit ran only if nothing else writes or reads it',
|
|
78
|
+
sound: true,
|
|
79
|
+
guardedBy: 'case-end-goto.test.ts: a flag read or written anywhere else keeps its flag',
|
|
80
|
+
rejects: (c) => c.flagElsewhere,
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
/** `f == 0`, the structurer's test of a flag (structure.ts `emitForeverExit`) */
|
|
85
|
+
const flagTest = (e: Expr): string | null =>
|
|
86
|
+
e.k === 'bin' && e.op === '==' && e.l.k === 'var' && e.r.k === 'const' && e.r.value === 0 ? e.l.name : null;
|
|
87
|
+
|
|
88
|
+
/** The hits in `body`, a loop's body: each `f = c; break;` (c a nonzero constant) reached through
|
|
89
|
+
* `if`s only, so the `break` is that loop's own. */
|
|
90
|
+
function hitsIn(body: readonly Stmt[], flag: string): number {
|
|
91
|
+
return body.reduce((n, s, i) => {
|
|
92
|
+
const next = body[i + 1];
|
|
93
|
+
if (
|
|
94
|
+
s.k === 'assign' &&
|
|
95
|
+
s.name === flag &&
|
|
96
|
+
s.value.k === 'const' &&
|
|
97
|
+
s.value.value !== 0 &&
|
|
98
|
+
next?.k === 'break' &&
|
|
99
|
+
next.ofSwitch !== true &&
|
|
100
|
+
next.label === undefined
|
|
101
|
+
) {
|
|
102
|
+
return n + 1;
|
|
103
|
+
}
|
|
104
|
+
return s.k === 'if' ? n + hitsIn(s.then, flag) + hitsIn(s.else, flag) : n;
|
|
105
|
+
}, 0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** `body` with each hit `hitsIn` counts written `goto label;` */
|
|
109
|
+
function jumpHits(body: readonly Stmt[], flag: string, label: string): Stmt[] {
|
|
110
|
+
return body.flatMap((s, i): Stmt[] => {
|
|
111
|
+
const prev = body[i - 1];
|
|
112
|
+
if (s.k === 'break' && prev?.k === 'assign' && prev.name === flag && s.ofSwitch !== true) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
if (s.k === 'assign' && s.name === flag) {
|
|
116
|
+
return [{ k: 'caseEndGoto', label }];
|
|
117
|
+
}
|
|
118
|
+
return s.k === 'if' ? [{ ...s, then: jumpHits(s.then, flag, label), else: jumpHits(s.else, flag, label) }] : [s];
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** `f = 0; LOOP; if (f == 0) { X }` starting at `list[i]`, the structurer's flag read after its loop */
|
|
123
|
+
function siteAt(
|
|
124
|
+
list: readonly Stmt[],
|
|
125
|
+
i: number,
|
|
126
|
+
): { flag: string; loop: Loop; test: Extract<Stmt, { k: 'if' }> } | null {
|
|
127
|
+
const [init, loop, test] = [list[i], list[i + 1], list[i + 2]];
|
|
128
|
+
if (
|
|
129
|
+
init?.k !== 'assign' ||
|
|
130
|
+
init.value.k !== 'const' ||
|
|
131
|
+
init.value.value !== 0 ||
|
|
132
|
+
loop === undefined ||
|
|
133
|
+
!isLoop(loop) ||
|
|
134
|
+
test?.k !== 'if' ||
|
|
135
|
+
test.else.length > 0 ||
|
|
136
|
+
flagTest(test.cond) !== init.name
|
|
137
|
+
) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return { flag: init.name, loop, test };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Every flag read after its loop where the code after the loop ends its switch arm, rewritten as
|
|
144
|
+
* a case-end goto, or null where none is (the variation's decline). */
|
|
145
|
+
export function jumpToCaseEnds(sfn: SFn, gates: readonly Gate<CaseEndSite>[] = CASE_END_GOTO_GATES): SFn | null {
|
|
146
|
+
const mentions = localMentions(sfn);
|
|
147
|
+
const labels = new Set<string>();
|
|
148
|
+
const collect = (s: Stmt): void => {
|
|
149
|
+
if (s.k === 'switch') {
|
|
150
|
+
s.cases.forEach((c) => c.label !== undefined && labels.add(c.label));
|
|
151
|
+
} else if (s.k === 'break' && s.label !== undefined) {
|
|
152
|
+
labels.add(s.label);
|
|
153
|
+
}
|
|
154
|
+
stmtChildren(s).forEach(collect);
|
|
155
|
+
};
|
|
156
|
+
sfn.body.forEach(collect);
|
|
157
|
+
const fresh = (): string => {
|
|
158
|
+
let label = 'case_end';
|
|
159
|
+
for (let n = 2; labels.has(label); n++) {
|
|
160
|
+
label = `case_end_${n}`;
|
|
161
|
+
}
|
|
162
|
+
labels.add(label);
|
|
163
|
+
return label;
|
|
164
|
+
};
|
|
165
|
+
const dropped = new Set<string>();
|
|
166
|
+
|
|
167
|
+
/** `inArm`: the list is inside a switch arm with no loop between; `leaves`: falling off its end
|
|
168
|
+
* leaves that switch with nothing run on the way. */
|
|
169
|
+
const rewrite = (list: readonly Stmt[], inArm: boolean, leaves: boolean): Stmt[] => {
|
|
170
|
+
const out = [...list];
|
|
171
|
+
for (let i = 0; i < out.length; i++) {
|
|
172
|
+
const site = siteAt(out, i);
|
|
173
|
+
if (site !== null) {
|
|
174
|
+
const { flag, loop, test } = site;
|
|
175
|
+
const after = out[i + 3];
|
|
176
|
+
const end = after?.k === 'break' && after.ofSwitch === true ? after : undefined;
|
|
177
|
+
const m = mentions.get(flag);
|
|
178
|
+
const hits = hitsIn(loop.body, flag);
|
|
179
|
+
const refused = firstRejection(gates, {
|
|
180
|
+
targetNotArmEnd: end === undefined && !(after === undefined && leaves),
|
|
181
|
+
flagElsewhere:
|
|
182
|
+
m === undefined || hits === 0 || m.assigns !== hits + 1 || readsOf(m) !== 1 || m.addrTaken !== 0,
|
|
183
|
+
});
|
|
184
|
+
if (refused === null) {
|
|
185
|
+
const label = end?.label ?? fresh();
|
|
186
|
+
const jumped: Stmt = { ...loop, body: jumpHits(loop.body, flag, label) };
|
|
187
|
+
out.splice(i, end ? 4 : 3, descend(jumped, false, undefined, false), ...test.then, {
|
|
188
|
+
k: 'break',
|
|
189
|
+
ofSwitch: true,
|
|
190
|
+
label,
|
|
191
|
+
});
|
|
192
|
+
dropped.add(flag);
|
|
193
|
+
// `X` is visited next, in this list, so a site inside it whose `if` now sits right before
|
|
194
|
+
// the label takes that label
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
out[i] = descend(out[i], inArm, out[i + 1], i === out.length - 1 && leaves);
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
};
|
|
202
|
+
/** `s` with its lists rewritten; `next` follows it in its list, and `last` says it ends a list
|
|
203
|
+
* whose falling off leaves the switch */
|
|
204
|
+
const descend = (s: Stmt, inArm: boolean, next: Stmt | undefined, last: boolean): Stmt => {
|
|
205
|
+
if (s.k === 'switch') {
|
|
206
|
+
const arm = (c: SwitchCase): SwitchCase => ({ ...c, body: rewrite(c.body, true, !c.fallsThrough) });
|
|
207
|
+
return { ...s, cases: s.cases.map(arm), ...(s.default ? { default: rewrite(s.default, true, true) } : {}) };
|
|
208
|
+
}
|
|
209
|
+
if (isLoop(s)) {
|
|
210
|
+
return mapStmtLists(s, (l) => rewrite(l, false, false));
|
|
211
|
+
}
|
|
212
|
+
if (s.k === 'if') {
|
|
213
|
+
const leaves = inArm && ((next?.k === 'break' && next.ofSwitch === true) || (next === undefined && last));
|
|
214
|
+
return { ...s, then: rewrite(s.then, inArm, leaves), else: rewrite(s.else, inArm, leaves) };
|
|
215
|
+
}
|
|
216
|
+
return s;
|
|
217
|
+
};
|
|
218
|
+
const body = rewrite(sfn.body, false, false);
|
|
219
|
+
if (dropped.size === 0) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
return { ...sfn, body, locals: sfn.locals.filter((l) => !dropped.has(l.name)) };
|
|
223
|
+
}
|
package/src/l3/coalesce.ts
CHANGED
|
@@ -12,7 +12,16 @@
|
|
|
12
12
|
// and whether fall-through joins the two arms onto one path. `coalesceCandidates` offers both.
|
|
13
13
|
import { typeToString } from '../ir/types';
|
|
14
14
|
import type { Expr, SFn, Stmt } from './ast';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
exprChildren,
|
|
17
|
+
isLoop,
|
|
18
|
+
mapExprChildren,
|
|
19
|
+
mentionedName,
|
|
20
|
+
stmtChildren,
|
|
21
|
+
stmtExprs,
|
|
22
|
+
switchArmSuccessors,
|
|
23
|
+
switchJumpsBack,
|
|
24
|
+
} from './ast';
|
|
16
25
|
import { type Gate, firstRejection } from './gates';
|
|
17
26
|
|
|
18
27
|
function namesIn(e: Expr, out: Set<string>): void {
|
|
@@ -122,8 +131,12 @@ function spans(body: Stmt[]): Map<string, Span> {
|
|
|
122
131
|
};
|
|
123
132
|
/** the loop set a while/do-while's own condition and children run under — its own. A `for` is
|
|
124
133
|
* routed separately: `stmtChildren` hands back its init too, and the init is the one child its
|
|
125
|
-
* loop does not re-run.
|
|
126
|
-
|
|
134
|
+
* loop does not re-run. A switch whose case-label `goto` lands on an arm laid out above it
|
|
135
|
+
* (`switchJumpsBack`) counts too: that jump is a back edge with no loop statement, and the arm
|
|
136
|
+
* it lands on runs after the one that jumps, so a local the lower arm writes can clobber one the
|
|
137
|
+
* upper arm still reads. */
|
|
138
|
+
const under = (s: Stmt, loops: readonly Stmt[]): readonly Stmt[] =>
|
|
139
|
+
isLoop(s) || (s.k === 'switch' && switchJumpsBack(s)) ? [...loops, s] : loops;
|
|
127
140
|
const walk = (list: Stmt[], loops: readonly Stmt[]): void => {
|
|
128
141
|
for (const s of list) {
|
|
129
142
|
// A `for`'s INIT runs ONCE, ahead of the condition and outside the loop; its cond, inc and
|
|
@@ -208,9 +221,11 @@ export interface MergePair {
|
|
|
208
221
|
* earlier one — and a back edge returns only to the head of its OWN loop, whose body is one
|
|
209
222
|
* contiguous preorder range. So the reordering can reach a PAIR only when some loop holds a
|
|
210
223
|
* mention of both locals: there the survivor's write can be followed, on the next iteration, by
|
|
211
|
-
* the absorbed local's read.
|
|
212
|
-
*
|
|
213
|
-
*
|
|
224
|
+
* the absorbed local's read. The one other back edge L3 holds is a case-label `goto` aimed at an
|
|
225
|
+
* arm laid out above it, which returns into its own switch's range the same way, so `spans`
|
|
226
|
+
* records such a switch as a loop and this rule covers it. Where no loop holds both — two sibling
|
|
227
|
+
* loops, or one local living before the loop the other lives in — no back edge connects the two
|
|
228
|
+
* ranges and preorder IS execution order. The ancestor chain is what the span records, not the innermost loop: an outer
|
|
214
229
|
* loop re-runs an inner one's statements, so it can reorder a pair that no inner loop shares.
|
|
215
230
|
*
|
|
216
231
|
* `first-is-write` AND `const-fed` ARE INDEPENDENT, and neither masks the other: a survivor whose
|
|
@@ -332,8 +347,9 @@ export interface ArmPair {
|
|
|
332
347
|
b: string;
|
|
333
348
|
/** the confining branch has a loop ancestor, so it can run more than once */
|
|
334
349
|
ifInLoop: boolean;
|
|
335
|
-
/** control can run from one of the two arms INTO the other — `switch` fall-through
|
|
336
|
-
*
|
|
350
|
+
/** control can run from one of the two arms INTO the other — `switch` fall-through or a
|
|
351
|
+
* case-label `goto`, the two ways two arms of a branch land on a single path. Always false for
|
|
352
|
+
* an `if`. */
|
|
337
353
|
armsJoined: boolean;
|
|
338
354
|
sameType: boolean;
|
|
339
355
|
/** either local is object-volatile or carries a pointee-volatile qualifier (see MergePair) */
|
|
@@ -498,9 +514,10 @@ function mentionIndex(): {
|
|
|
498
514
|
* `switch` are one rule here rather than two walkers.
|
|
499
515
|
*
|
|
500
516
|
* An `if` has exactly two arms and no way to reach one from the other. A `switch`'s arms are its
|
|
501
|
-
* `case` bodies, and
|
|
502
|
-
*
|
|
503
|
-
* TRANSITIVE chain and not merely
|
|
517
|
+
* `case` bodies, and two of them land on a single path where one runs on into the other: by
|
|
518
|
+
* FALL-THROUGH, or by the case-label `goto` that ends an arm (l3/ast.ts), which lands where a
|
|
519
|
+
* fall-through into its target would. The reach is the TRANSITIVE chain of both and not merely
|
|
520
|
+
* the adjacent pair.
|
|
504
521
|
*
|
|
505
522
|
* ITS REACH, MEASURED, because a soundness argument with no corpus witness should say so. Counted
|
|
506
523
|
* 2026-09-19 with a throwaway counter in this walk, over `pnpm bench sweep --fan --map-modes
|
|
@@ -522,10 +539,21 @@ const branchArms = (st: Stmt): { arms: Stmt[][]; joined: (i: number, j: number)
|
|
|
522
539
|
return { arms: [st.then, st.else], joined: () => false };
|
|
523
540
|
}
|
|
524
541
|
if (st.k === 'switch') {
|
|
525
|
-
|
|
542
|
+
// Arm `i` runs on into the arms `switchArmSuccessors` chains it to, by falling through or by the
|
|
543
|
+
// `goto` that ends it (l3/ast.ts) — two arms on one path either way — so `reach[i]` is that
|
|
544
|
+
// chain, the `default` included as a node (a case falling into it and jumping out of it puts
|
|
545
|
+
// both on the path), though never offered as an arm itself.
|
|
546
|
+
const next = switchArmSuccessors(st);
|
|
547
|
+
const reach = next.map((_, i) => {
|
|
548
|
+
const seen = new Set<number>();
|
|
549
|
+
for (let at = next[i]; at !== undefined && !seen.has(at); at = next[at]) {
|
|
550
|
+
seen.add(at);
|
|
551
|
+
}
|
|
552
|
+
return seen;
|
|
553
|
+
});
|
|
526
554
|
return {
|
|
527
|
-
arms: cases.map((c) => c.body),
|
|
528
|
-
joined: (i, j) =>
|
|
555
|
+
arms: st.cases.map((c) => c.body),
|
|
556
|
+
joined: (i, j) => reach[i].has(j) || reach[j].has(i),
|
|
529
557
|
};
|
|
530
558
|
}
|
|
531
559
|
return null;
|
package/src/l3/constinit.ts
CHANGED
|
@@ -338,17 +338,45 @@ export function constantImageRefusal(sfn: SFn): { image: string; refused: string
|
|
|
338
338
|
* signedness keeps its cast, which is the element type it spells. */
|
|
339
339
|
function asElement(e: Expr, by: ReadonlyMap<string, Fold>): Expr {
|
|
340
340
|
const inner = mapExprChildren(e, (c) => asElement(c, by));
|
|
341
|
-
|
|
341
|
+
// …and a read of the element's BYTES at the other signedness, converted to the element's own
|
|
342
|
+
// type, IS the element: `(s8)((u8 *)a)[i]` is `a[i]` on `s8 a[16]`. agbcc has no `ldrsb` with an
|
|
343
|
+
// immediate offset, so a signed byte read through the frame base is `ldrb` and `lsl #24; asr #24`,
|
|
344
|
+
// which the lift reads as the unsigned byte sign-extended; C extends the element itself the same
|
|
345
|
+
// way. Only a conversion to exactly the element type: a wider or a narrower one is another value.
|
|
346
|
+
// A DECLARED array's byte (a map global's, a proven frame part's) is read signed before this, by
|
|
347
|
+
// the structurer (structure.ts `declaredExtendedRead`); the type this pass gives a local exists
|
|
348
|
+
// only here, so for those the identity is applied here.
|
|
349
|
+
if (inner.k === 'cast' && inner.to.kind === 'int' && inner.volatile !== true && inner.e.k === 'index') {
|
|
350
|
+
const read = inner.e;
|
|
351
|
+
const elem = elementOf(read, by);
|
|
352
|
+
if (elem !== undefined && elem.width === inner.to.width && elem.signed === inner.to.signed) {
|
|
353
|
+
return asElement({ ...read, signed: elem.signed }, by);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (inner.k !== 'index') {
|
|
342
357
|
return inner;
|
|
343
358
|
}
|
|
359
|
+
const elem = elementOf(inner, by);
|
|
360
|
+
if (elem === undefined || elem.signed !== inner.signed) {
|
|
361
|
+
return inner;
|
|
362
|
+
}
|
|
363
|
+
return { ...inner, base: { k: 'var', name: wholeObject(inner.base)! }, subscript: true };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** The element type of the folded local `read` reads one element of — at the element's width,
|
|
367
|
+
* whatever its signedness — or undefined for any other access. */
|
|
368
|
+
function elementOf(
|
|
369
|
+
read: Extract<Expr, { k: 'index' }>,
|
|
370
|
+
by: ReadonlyMap<string, Fold>,
|
|
371
|
+
): Extract<IrType, { kind: 'int' }> | undefined {
|
|
372
|
+
if ((read.lead?.length ?? 0) > 0 || read.baseElem !== undefined) {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
344
375
|
// the object the base names whole, as the fold's own census of element accesses reads it (`uses`)
|
|
345
|
-
const name = wholeObject(
|
|
376
|
+
const name = wholeObject(read.base);
|
|
346
377
|
const fold = name === null ? undefined : by.get(name);
|
|
347
378
|
const elem = fold?.type.kind === 'array' ? fold.type.elem : undefined;
|
|
348
|
-
|
|
349
|
-
return inner;
|
|
350
|
-
}
|
|
351
|
-
return { ...inner, base: { k: 'var', name: name! }, subscript: true };
|
|
379
|
+
return elem?.kind === 'int' && elem.width === read.width * 8 ? elem : undefined;
|
|
352
380
|
}
|
|
353
381
|
|
|
354
382
|
/** `sfn` with every anonymous image folded into the initializer of each local its copies fill,
|
package/src/l3/dce.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// unresolved `?` value trips the contract first and never reaches DCE; `mustKeep` treating `?` as
|
|
26
26
|
// keep is defense-in-depth for any future caller that skips that check.
|
|
27
27
|
import type { Expr, SFn, Stmt } from './ast';
|
|
28
|
-
import { exprChildren, keepsHelperRel, negateCond, stmtChildren, stmtExprs } from './ast';
|
|
28
|
+
import { caseEndGotoLeaves, exprChildren, keepsHelperRel, negateCond, stmtChildren, stmtExprs } from './ast';
|
|
29
29
|
|
|
30
30
|
/** Accumulate every LOCAL-eligible `var` name read anywhere in `e` (recurses all sub-exprs). An
|
|
31
31
|
* `addr` name counts too: taking a local's address makes every store to it observable through the
|
|
@@ -106,6 +106,24 @@ function allAddrNamesInto(stmts: Stmt[], out: Set<string>): void {
|
|
|
106
106
|
interface JumpLive {
|
|
107
107
|
brk: ReadonlySet<string>;
|
|
108
108
|
cont?: ReadonlySet<string>; // absent in a switch no loop encloses
|
|
109
|
+
/** live at a case label of the innermost switch: what its `goto` (l3/ast.ts) lands on — absent
|
|
110
|
+
* where no switch encloses the statement, or a loop stands between it and the switch */
|
|
111
|
+
caseLabel?: ReadonlySet<string>;
|
|
112
|
+
/** live at the end of the innermost switch arm around the statement, loops between included: what
|
|
113
|
+
* a case-end `goto` lands on — absent where no switch encloses it */
|
|
114
|
+
caseEnd?: ReadonlySet<string>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The conservative set live throughout a loop: what is live after it and every read in it, and,
|
|
118
|
+
* where a case-end `goto` in its body leaves it, what is live at the arm's end that goto lands on —
|
|
119
|
+
* a later iteration may take it. */
|
|
120
|
+
function loopLiveOf(s: Stmt & { body: Stmt[] }, live: ReadonlySet<string>, jumps: JumpLive | null): Set<string> {
|
|
121
|
+
const loopLive = new Set(live);
|
|
122
|
+
allReadsInto([s], loopLive);
|
|
123
|
+
if (jumps?.caseEnd && caseEndGotoLeaves(s.body)) {
|
|
124
|
+
jumps.caseEnd.forEach((n) => loopLive.add(n));
|
|
125
|
+
}
|
|
126
|
+
return loopLive;
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
/** Backward live-variable walk over one block. `liveOut` is the set of locals live on exit;
|
|
@@ -165,6 +183,26 @@ function dceBlock(
|
|
|
165
183
|
rev.push(s);
|
|
166
184
|
break;
|
|
167
185
|
}
|
|
186
|
+
// The jump lands on the `break;` that ends its arm (l3/ast.ts, contracts.ts
|
|
187
|
+
// `assertCaseGotosNarrow`), whose every read the switch below already counts live.
|
|
188
|
+
case 'caseEndGoto': {
|
|
189
|
+
if (!jumps?.caseEnd) {
|
|
190
|
+
throw new Error('dce: a case-end goto outside a switch arm');
|
|
191
|
+
}
|
|
192
|
+
setLive(new Set(jumps.caseEnd));
|
|
193
|
+
rev.push(s);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case 'goto': {
|
|
197
|
+
// The jump lands on a case label of the switch it sits in (l3/ast.ts, contracts.ts
|
|
198
|
+
// `assertCaseGotosNarrow`), whose every read the switch below already counts live.
|
|
199
|
+
if (!jumps?.caseLabel) {
|
|
200
|
+
throw new Error('dce: a goto outside the switch whose case it names');
|
|
201
|
+
}
|
|
202
|
+
setLive(new Set(jumps.caseLabel));
|
|
203
|
+
rev.push(s);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
168
206
|
case 'break':
|
|
169
207
|
case 'continue': {
|
|
170
208
|
// A mid-body `if (c) { v = x; break; }` carries the loop's exit value in `v`, while the
|
|
@@ -207,9 +245,12 @@ function dceBlock(
|
|
|
207
245
|
case 'dowhile': {
|
|
208
246
|
// Conservative: any local read anywhere in the loop is live throughout it, so a
|
|
209
247
|
// loop-carried store is never cut. Body DCE removes only what is dead on EVERY path.
|
|
210
|
-
const loopLive =
|
|
211
|
-
|
|
212
|
-
|
|
248
|
+
const loopLive = loopLiveOf(s, live, jumps);
|
|
249
|
+
const b = dceBlock(s.body, loopLive, locals, volatiles, {
|
|
250
|
+
brk: loopLive,
|
|
251
|
+
cont: loopLive,
|
|
252
|
+
caseEnd: jumps?.caseEnd,
|
|
253
|
+
});
|
|
213
254
|
const nlive = new Set(loopLive);
|
|
214
255
|
for (const r of b.liveIn) {
|
|
215
256
|
nlive.add(r);
|
|
@@ -219,9 +260,12 @@ function dceBlock(
|
|
|
219
260
|
break;
|
|
220
261
|
}
|
|
221
262
|
case 'for': {
|
|
222
|
-
const loopLive =
|
|
223
|
-
|
|
224
|
-
|
|
263
|
+
const loopLive = loopLiveOf(s, live, jumps);
|
|
264
|
+
const b = dceBlock(s.body, loopLive, locals, volatiles, {
|
|
265
|
+
brk: loopLive,
|
|
266
|
+
cont: loopLive,
|
|
267
|
+
caseEnd: jumps?.caseEnd,
|
|
268
|
+
});
|
|
225
269
|
const nlive = new Set(loopLive);
|
|
226
270
|
for (const r of b.liveIn) {
|
|
227
271
|
nlive.add(r);
|
|
@@ -235,7 +279,7 @@ function dceBlock(
|
|
|
235
279
|
// read anywhere in the switch as live throughout — no case-body store is ever cut.
|
|
236
280
|
const swLive = new Set(live);
|
|
237
281
|
allReadsInto([s], swLive);
|
|
238
|
-
const inSwitch: JumpLive = { brk: swLive, cont: jumps?.cont };
|
|
282
|
+
const inSwitch: JumpLive = { brk: swLive, cont: jumps?.cont, caseLabel: swLive, caseEnd: swLive };
|
|
239
283
|
const cases = s.cases.map((c) => ({ ...c, body: dceBlock(c.body, swLive, locals, volatiles, inSwitch).out }));
|
|
240
284
|
const def = s.default ? dceBlock(s.default, swLive, locals, volatiles, inSwitch).out : s.default;
|
|
241
285
|
const nlive = new Set(swLive);
|
package/src/l3/ptrcell.ts
CHANGED
package/src/l3/regspell.ts
CHANGED
package/src/l3/reindex.ts
CHANGED
|
@@ -93,6 +93,8 @@ import {
|
|
|
93
93
|
Expr,
|
|
94
94
|
SFn,
|
|
95
95
|
Stmt,
|
|
96
|
+
SwitchCase,
|
|
97
|
+
caseEndGotoLeaves,
|
|
96
98
|
mapExprChildren,
|
|
97
99
|
mapStmtExprs,
|
|
98
100
|
mentionedName,
|
|
@@ -303,8 +305,13 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
|
|
|
303
305
|
|
|
304
306
|
/** A `break`/`continue` that would target the ENCLOSING loop: found at if/switch depth, but not
|
|
305
307
|
* inside a nested loop (whose own exits target itself). A switch's own `break` (`ofSwitch`)
|
|
306
|
-
* leaves only its switch, and the loop's tail still runs after it, so it is not one.
|
|
308
|
+
* leaves only its switch, and the loop's tail still runs after it, so it is not one. A case-end
|
|
309
|
+
* `goto` whose label lies outside `stmts` leaves every loop it sits in, at any depth. */
|
|
307
310
|
function hasLoopExit(stmts: Stmt[]): boolean {
|
|
311
|
+
return caseEndGotoLeaves(stmts) || hasShallowLoopExit(stmts);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function hasShallowLoopExit(stmts: Stmt[]): boolean {
|
|
308
315
|
return stmts.some((s) => {
|
|
309
316
|
switch (s.k) {
|
|
310
317
|
case 'break':
|
|
@@ -312,9 +319,9 @@ function hasLoopExit(stmts: Stmt[]): boolean {
|
|
|
312
319
|
case 'continue':
|
|
313
320
|
return true;
|
|
314
321
|
case 'if':
|
|
315
|
-
return
|
|
322
|
+
return hasShallowLoopExit(s.then) || hasShallowLoopExit(s.else);
|
|
316
323
|
case 'switch':
|
|
317
|
-
return s.cases.some((c) =>
|
|
324
|
+
return s.cases.some((c) => hasShallowLoopExit(c.body)) || hasShallowLoopExit(s.default ?? []);
|
|
318
325
|
default:
|
|
319
326
|
return false;
|
|
320
327
|
}
|
|
@@ -502,12 +509,14 @@ function reindexStmt(s: Stmt, walk: WalkLoop, iv: string): Stmt | null {
|
|
|
502
509
|
return {
|
|
503
510
|
...s,
|
|
504
511
|
scrutinee,
|
|
505
|
-
cases: cases as
|
|
512
|
+
cases: cases as SwitchCase[],
|
|
506
513
|
default: dflt ?? undefined,
|
|
507
514
|
};
|
|
508
515
|
}
|
|
509
516
|
case 'break':
|
|
510
517
|
case 'continue':
|
|
518
|
+
case 'goto':
|
|
519
|
+
case 'caseEndGoto':
|
|
511
520
|
return s;
|
|
512
521
|
}
|
|
513
522
|
}
|
package/src/l3/scopebase.ts
CHANGED