@asmlift/core 0.8.1 → 0.10.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 +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +25 -7
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
// asmlift — un-threading a loop exit the compiler jump-threaded past the test that follows the loop
|
|
2
|
+
// (ISA-neutral CFG pass).
|
|
3
|
+
//
|
|
4
|
+
// `for (i = 0; i < 3; i++) if (x == t[i].a || x == t[i].b) break; if (x == t[i].a) A; else B;` —
|
|
5
|
+
// the code after the loop repeats the test the first `break` just made. GCC's jump threading
|
|
6
|
+
// (agbcc jump.c:4517 `thread_jumps`, on from -O1, toplev.c:3598) sees that the branch out of the
|
|
7
|
+
// loop on `x == t[i].a` lands on a block R that compares the same operands again, and retargets the
|
|
8
|
+
// branch straight at R's taken arm:
|
|
9
|
+
//
|
|
10
|
+
// E: ld v, t[i].a; cmp x, v; beq A ; was: beq R
|
|
11
|
+
// ... ; the loop's other exits reach R
|
|
12
|
+
// R(j): ld w, t[j].a; cmp x, w; bne B ; falls into A
|
|
13
|
+
//
|
|
14
|
+
// The loop then leaves for two blocks, A and R, and no loop spelling owns that: the function
|
|
15
|
+
// declines with 'unrecovered back-edge'. Pointing E back at R — the edge the source wrote — gives
|
|
16
|
+
// the loop one way out, and R re-tests on that path what E just tested, so it takes the edge E took.
|
|
17
|
+
//
|
|
18
|
+
// SOUND WHEN R'S BRANCH IS DECIDED BY E'S on the path E→R, and the rewrite then runs extra code that
|
|
19
|
+
// observes nothing. That is the compiler's own precondition read backwards (jump.c
|
|
20
|
+
// `rtx_equal_for_thread_p` on the operands, `comparison_dominates_p` on the codes, no register or
|
|
21
|
+
// memory the skipped insns depend on modified in between), and it is what the gates check:
|
|
22
|
+
// • R runs nothing observable: no effect, no qualified or device access, no op that traps, so
|
|
23
|
+
// running it once more on E's path changes nothing but which edge is taken;
|
|
24
|
+
// • R's condition, read with R's params bound to the args E now hands it, is E's condition or its
|
|
25
|
+
// negation (`sameAtEntry`, ir/entry.ts — the predicate the re-roll and guard fusion read), through
|
|
26
|
+
// memory reads in R and in E's block after its last effect only: both read the memory E's branch
|
|
27
|
+
// saw, and a read anywhere else may see other memory;
|
|
28
|
+
// • the edge that decides R's branch on E's sense reaches E's old target T with E's own args —
|
|
29
|
+
// directly (S == T, agbcc), or through one block S that runs nothing observable and branches to
|
|
30
|
+
// T (gcc2.7.2kmc moves the THEN arm's `li v0,1` into the threaded branch's delay slot, so E
|
|
31
|
+
// carries the arm's value as an arg and the arm itself is skipped).
|
|
32
|
+
// Then E→R→(S→)T computes what E→T did, and on every other path nothing moved. R may also be a
|
|
33
|
+
// block that runs nothing observable and forwards (`br`) to the block Q that re-tests: gcc2.7.2 -O1
|
|
34
|
+
// re-masks a `u8` operand on each exit's own block (the latch fall-through and a `beql` delay
|
|
35
|
+
// slot), which merge into one exit target ahead of the re-test. Q is then judged as R is above, on
|
|
36
|
+
// the values R's `br` hands it.
|
|
37
|
+
//
|
|
38
|
+
// SCOPE, not soundness (the heuristic gates): only the decline shape. R must be another exit target
|
|
39
|
+
// of the same loop and E the loop's only edge to T, so each rewrite takes one exit target away; and
|
|
40
|
+
// T must not merely return, because an exit that returns is an early `return` the structurer already
|
|
41
|
+
// spells. Every function that lifted without this pass keeps its CFG.
|
|
42
|
+
//
|
|
43
|
+
// THE ARGS E HANDS R, per param: a value, among those the loop's in-loop edges into R hand it, that
|
|
44
|
+
// is defined at E (dominates E's block). A bottom-tested loop's latch hands R the stepped counter,
|
|
45
|
+
// which E never sees, and its in-body exits hand R the counter E reads; only the second is defined
|
|
46
|
+
// at E. When kmc puts the step in the threaded branch's delay slot (`beq a1,v0,L` / `addiu v1,v1,4`),
|
|
47
|
+
// the stepped pointer is defined at E too, and R has two candidates. Each choice is only a proposal:
|
|
48
|
+
// the comparison above is what makes it sound, so every combination of candidates (at most
|
|
49
|
+
// `MAX_PROPOSALS`) is judged, and the first the gates admit is taken; a param with no such value
|
|
50
|
+
// refuses. Inhabitants: `synthetic:thrbreak:agbcc` (top-tested, every exit hands one counter,
|
|
51
|
+
// S == T), the pointer walk `synthetic:thrwalk`
|
|
52
|
+
// `for (p = t; p < t + 3; p++) if (x == p->a || x == p->b) break; if (x == p->a) A; else B;`
|
|
53
|
+
// (gcc2.7.2kmc: bottom-tested, through S; gcc2.7.2: R a forwarder, through S), and
|
|
54
|
+
// `synthetic:thrwalkba`, its tests swapped, where kmc's delay-slot step gives R two candidates.
|
|
55
|
+
// Not `synthetic:thrbreak:gcc2.7.2kmc`, whatever the counter's width: kmc indexes `t[i]` there by
|
|
56
|
+
// a value each exit computes on its own block (`i<<2`, re-masked `i & 0xff` for a `u8`, in the exit
|
|
57
|
+
// branch's delay slot), so no value R reads is defined at E. That row lifts through the break tail
|
|
58
|
+
// its exits share instead (structure.ts `codeBreakArm`). Nor a gcc2.7.2 walk with two separate
|
|
59
|
+
// break tests (`if (x == p->a) break; if (y == p->b) break;`): every in-loop edge hands R an
|
|
60
|
+
// `x & 0xff` its own block re-masked, none defined at E, so it declines loudly.
|
|
61
|
+
import { Block, Fn, Op, Value, dominators, predecessors, successorsOf } from '../ir/core';
|
|
62
|
+
import { effectful, placedAt, spelledWhenDead } from '../ir/discipline';
|
|
63
|
+
import { type EntryReading, sameAtEntry } from '../ir/entry';
|
|
64
|
+
import { analyzeLoops } from '../ir/loops';
|
|
65
|
+
import { NEGATED_ICMP, opSig } from '../ir/opcodes';
|
|
66
|
+
import { simplifyTrivialPhis } from '../ir/simplify';
|
|
67
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
68
|
+
import { returnsPurely } from './latch';
|
|
69
|
+
|
|
70
|
+
/** One loop exit edge E→T and another exit target R of the same loop that re-tests E's test. */
|
|
71
|
+
export interface ThreadedExit {
|
|
72
|
+
/** E's block (in the loop) and which successor of its `cond_br` leaves for T */
|
|
73
|
+
from: Block;
|
|
74
|
+
index: number;
|
|
75
|
+
/** E's target, and the block R the edge would be pointed back at: the re-test, or a forwarder to it */
|
|
76
|
+
target: Block;
|
|
77
|
+
retest: Block;
|
|
78
|
+
/** does another exit edge of the same loop reach T? */
|
|
79
|
+
targetShared: boolean;
|
|
80
|
+
/** does every path from T reach a `ret` without an effect on the way? */
|
|
81
|
+
targetReturns: boolean;
|
|
82
|
+
/** does R (or the re-test it forwards to, or the S it reaches T through) run an op whose second
|
|
83
|
+
* run is observable? */
|
|
84
|
+
observable: boolean;
|
|
85
|
+
/** the args E would hand R: per param, one of the values the in-loop edges into R hand it that is
|
|
86
|
+
* defined at E (one proposal; each combination of such values is a site of its own) */
|
|
87
|
+
args: Value[];
|
|
88
|
+
/** once E reaches R, does every value R and the blocks it reaches read still dominate its reader? */
|
|
89
|
+
dominated: boolean;
|
|
90
|
+
/** is R's condition, on E's args, E's condition or its negation — and which one decides S? */
|
|
91
|
+
decided: boolean;
|
|
92
|
+
/** does the decided edge reach T with E's args? */
|
|
93
|
+
onward: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const UNTHREAD_GATES: readonly Gate<ThreadedExit>[] = [
|
|
97
|
+
{
|
|
98
|
+
id: 'target-shared',
|
|
99
|
+
why: 'pointing one of several exit edges into a block elsewhere takes no exit target away',
|
|
100
|
+
sound: false,
|
|
101
|
+
rejects: (c) => c.targetShared,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'target-returns',
|
|
105
|
+
why: 'an exit that only returns is an early return arm, which loop recovery spells without this',
|
|
106
|
+
sound: false,
|
|
107
|
+
rejects: (c) => c.targetReturns,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'retest-observable',
|
|
111
|
+
why: 'the re-test now runs on the exit path as well, so a second run of it must observe nothing',
|
|
112
|
+
sound: true,
|
|
113
|
+
guardedBy: [
|
|
114
|
+
'unthread.test.ts: a re-test block that stores is left alone',
|
|
115
|
+
'unthread.test.ts: a re-test whose onward block stores is left alone',
|
|
116
|
+
'unthread.test.ts: a forwarder that stores is left alone',
|
|
117
|
+
],
|
|
118
|
+
rejects: (c) => c.observable,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'not-dominated',
|
|
122
|
+
why: 'a value the re-test (or a block after it) reads from above it must exist on the exit edge too',
|
|
123
|
+
sound: true,
|
|
124
|
+
guardedBy: [
|
|
125
|
+
'unthread.test.ts: a re-test reading a value from a block the exit does not pass is left alone',
|
|
126
|
+
'unthread.test.ts: a forwarder handing the re-test a value from the in-body exit’s block is left alone',
|
|
127
|
+
],
|
|
128
|
+
rejects: (c) => !c.dominated,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'test-differs',
|
|
132
|
+
why: 'only a re-test the exit’s own test decides takes the edge the exit took',
|
|
133
|
+
sound: true,
|
|
134
|
+
guardedBy: [
|
|
135
|
+
'unthread.test.ts: a re-test of another operand is left alone',
|
|
136
|
+
'unthread.test.ts: a re-test reading memory the exit block writes after its own read is left alone',
|
|
137
|
+
],
|
|
138
|
+
rejects: (c) => !c.decided,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 'onward-differs',
|
|
142
|
+
why: 'the decided edge must hand the exit target what the exit edge handed it',
|
|
143
|
+
sound: true,
|
|
144
|
+
guardedBy: [
|
|
145
|
+
'unthread.test.ts: a re-test whose THEN arm hands the join another value is left alone',
|
|
146
|
+
'unthread.test.ts: a re-test whose decided edge is the other one is left alone',
|
|
147
|
+
],
|
|
148
|
+
rejects: (c) => !c.onward,
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
/** May a second run of `op` be observed? An effect, a placed access (volatile, device, a helper
|
|
153
|
+
* call), a trap — or `undef`, whose two copies are two storages' contents and not one value. */
|
|
154
|
+
const observable = (op: Op): boolean =>
|
|
155
|
+
effectful(op) || placedAt(op) !== null || opSig(op.opcode)?.traps === true || op.opcode === 'undef';
|
|
156
|
+
|
|
157
|
+
/** Point every threaded loop exit the gates admit back at the re-test it skipped; returns how many. */
|
|
158
|
+
export function unthreadLoopExits(fn: Fn, gates: readonly Gate<ThreadedExit>[] = UNTHREAD_GATES): number {
|
|
159
|
+
let done = 0;
|
|
160
|
+
// one at a time: each rewrite changes the exits the next is judged against. Bounded — every rewrite
|
|
161
|
+
// moves an exit edge off a block it was the loop's only edge into.
|
|
162
|
+
for (let cap = fn.blocks.length * 4; cap > 0 && unthreadOne(fn, gates); cap--) {
|
|
163
|
+
done++;
|
|
164
|
+
}
|
|
165
|
+
if (done > 0) {
|
|
166
|
+
simplifyTrivialPhis(fn);
|
|
167
|
+
}
|
|
168
|
+
return done;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function unthreadOne(fn: Fn, gates: readonly Gate<ThreadedExit>[]): boolean {
|
|
172
|
+
const preds = predecessors(fn);
|
|
173
|
+
const dom = dominators(fn);
|
|
174
|
+
const defs = new Map<Value, Op>();
|
|
175
|
+
const defBlock = new Map<Value, Block>();
|
|
176
|
+
for (const b of fn.blocks) {
|
|
177
|
+
b.params.forEach((p) => defBlock.set(p, b));
|
|
178
|
+
for (const op of b.ops) {
|
|
179
|
+
op.results.forEach((r) => {
|
|
180
|
+
defs.set(r, op);
|
|
181
|
+
defBlock.set(r, b);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const dominatesAt = (v: Value, at: Block): boolean => {
|
|
186
|
+
const d = defBlock.get(v);
|
|
187
|
+
return d !== undefined && dom.get(at)!.has(d);
|
|
188
|
+
};
|
|
189
|
+
const forest = analyzeLoops(fn, dom);
|
|
190
|
+
for (const nl of forest.byHeader.values()) {
|
|
191
|
+
const targets = [...new Set(nl.exitEdges.map((e) => e.to))];
|
|
192
|
+
if (targets.length < 2) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
for (const E of nl.body) {
|
|
196
|
+
const eTerm = E.ops[E.ops.length - 1];
|
|
197
|
+
if (eTerm?.opcode !== 'cond_br') {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
for (let index = 0; index < eTerm.successors.length; index++) {
|
|
201
|
+
const T = eTerm.successors[index].block;
|
|
202
|
+
if (nl.body.has(T) || eTerm.successors[1 - index].block === T) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
for (const R of targets) {
|
|
206
|
+
if (R === T || R === E || eTerm.successors[1 - index].block === R) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const site = judge(nl.body, E, index, T, R, preds, dom, defs, defBlock, dominatesAt).find(
|
|
210
|
+
(c) => firstRejection(gates, c) === null,
|
|
211
|
+
);
|
|
212
|
+
if (site === undefined) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
eTerm.successors[index] = { block: R, args: site.args };
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function judge(
|
|
225
|
+
body: ReadonlySet<Block>,
|
|
226
|
+
E: Block,
|
|
227
|
+
index: number,
|
|
228
|
+
T: Block,
|
|
229
|
+
R: Block,
|
|
230
|
+
preds: ReadonlyMap<Block, Block[]>,
|
|
231
|
+
dom: ReadonlyMap<Block, Set<Block>>,
|
|
232
|
+
defs: ReadonlyMap<Value, Op>,
|
|
233
|
+
defBlock: ReadonlyMap<Value, Block>,
|
|
234
|
+
dominatesAt: (v: Value, at: Block) => boolean,
|
|
235
|
+
): ThreadedExit[] {
|
|
236
|
+
// THE SHAPE, untabled: R re-tests — it ends in a `cond_br` (only a branch re-takes E's decision),
|
|
237
|
+
// or forwards to the block Q that does (`retestOf`) — and each of its params is handed, by some
|
|
238
|
+
// in-loop edge into R, a value defined at E — a candidate arg. One site per combination of
|
|
239
|
+
// candidates, refused as a whole past `MAX_PROPOSALS`.
|
|
240
|
+
const Q = retestOf(R, body, T);
|
|
241
|
+
if (Q === null) {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
const into = [...body].flatMap((p) => terminatorEdges(p).filter((s) => s.block === R));
|
|
245
|
+
let proposals: Value[][] = [[]];
|
|
246
|
+
for (let k = 0; k < R.params.length; k++) {
|
|
247
|
+
const vals = [...new Set(into.map((s) => s.args[k]).filter((v) => v !== undefined && dominatesAt(v, E)))];
|
|
248
|
+
proposals = proposals.flatMap((p) => vals.map((v) => [...p, v]));
|
|
249
|
+
if (proposals.length === 0 || proposals.length > MAX_PROPOSALS) {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return proposals.map((args) => judgeArgs(body, E, index, T, R, Q, preds, dom, defs, defBlock, args));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** The block whose `cond_br` re-tests on R's path: R itself, or the one block R forwards to with a
|
|
257
|
+
* `br`. gcc2.7.2 -O1 re-computes a re-test operand (`andi v1,a0,0xff`) on each exit's own block —
|
|
258
|
+
* the latch fall-through and the `beql` delay slot, which merge into one exit target — so R is that
|
|
259
|
+
* block and the re-test sits behind it, entered from the loop's guard as well. */
|
|
260
|
+
function retestOf(R: Block, body: ReadonlySet<Block>, T: Block): Block | null {
|
|
261
|
+
const rTerm = R.ops[R.ops.length - 1];
|
|
262
|
+
if (rTerm?.opcode === 'cond_br') {
|
|
263
|
+
return R;
|
|
264
|
+
}
|
|
265
|
+
if (rTerm?.opcode !== 'br') {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
const Q = rTerm.successors[0].block;
|
|
269
|
+
return Q !== R && Q !== T && !body.has(Q) && Q.ops[Q.ops.length - 1]?.opcode === 'cond_br' ? Q : null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** How many arg combinations one (exit, re-test) pair may propose. kmc's delay-slot step gives a
|
|
273
|
+
* pointer walk two; the bound keeps a re-test with many params from multiplying its comparisons. */
|
|
274
|
+
const MAX_PROPOSALS = 8;
|
|
275
|
+
|
|
276
|
+
function judgeArgs(
|
|
277
|
+
body: ReadonlySet<Block>,
|
|
278
|
+
E: Block,
|
|
279
|
+
index: number,
|
|
280
|
+
T: Block,
|
|
281
|
+
R: Block,
|
|
282
|
+
Q: Block,
|
|
283
|
+
preds: ReadonlyMap<Block, Block[]>,
|
|
284
|
+
dom: ReadonlyMap<Block, Set<Block>>,
|
|
285
|
+
defs: ReadonlyMap<Value, Op>,
|
|
286
|
+
defBlock: ReadonlyMap<Value, Block>,
|
|
287
|
+
args: Value[],
|
|
288
|
+
): ThreadedExit {
|
|
289
|
+
const eTerm = E.ops[E.ops.length - 1];
|
|
290
|
+
const qTerm = Q.ops[Q.ops.length - 1];
|
|
291
|
+
const site: ThreadedExit = {
|
|
292
|
+
from: E,
|
|
293
|
+
index,
|
|
294
|
+
target: T,
|
|
295
|
+
retest: R,
|
|
296
|
+
targetShared: [...body].some((p) => p !== E && successorsOf(p).includes(T)),
|
|
297
|
+
targetReturns: returnsPurely(T),
|
|
298
|
+
observable: R.ops.some(observable) || Q.ops.some(observable),
|
|
299
|
+
args,
|
|
300
|
+
dominated: false,
|
|
301
|
+
decided: false,
|
|
302
|
+
onward: false,
|
|
303
|
+
};
|
|
304
|
+
// The edge E→R takes away, of the blocks R reaches, exactly the dominators in R's that are not in
|
|
305
|
+
// E's (any other dominator either dominates both or sits on every path from R). So no value
|
|
306
|
+
// defined in one of those may be read — as an operand or an edge arg — by R or a block it reaches.
|
|
307
|
+
// When R is entered from above the loop (agbcc, kmc), that set is empty; gcc2.7.2's R behind the
|
|
308
|
+
// in-body exit is dominated by that exit's block, whose values the re-test does not read.
|
|
309
|
+
const lost = new Set([...dom.get(R)!].filter((d) => d !== R && !dom.get(E)!.has(d)));
|
|
310
|
+
site.dominated = lost.size === 0 || !readsFrom(R, lost, defBlock);
|
|
311
|
+
// the reads a comparison may look through: R's and Q's own (they run nothing observable, so they
|
|
312
|
+
// see the memory E's branch saw), the onward block's for the same reason, and E's after its last
|
|
313
|
+
// effect
|
|
314
|
+
const lastEffect = E.ops.reduce((at, op, i) => (effectful(op) ? i : at), -1);
|
|
315
|
+
const eReads = new Set(E.ops.slice(lastEffect + 1));
|
|
316
|
+
// on E→R(→Q): R's params hold E's args, and Q's what R's `br` hands it — an R param read as the arg
|
|
317
|
+
// it holds, any other value as itself (one substitution: a value R computes off its own params
|
|
318
|
+
// compares by identity below it, which only refuses)
|
|
319
|
+
const entry = new Map(R.params.map((p, k) => [p, args[k]]));
|
|
320
|
+
if (Q !== R) {
|
|
321
|
+
const fwd = R.ops[R.ops.length - 1].successors[0].args;
|
|
322
|
+
Q.params.forEach((p, k) => entry.set(p, entry.get(fwd[k]) ?? fwd[k]));
|
|
323
|
+
}
|
|
324
|
+
const reading = (onwardBlock: Block | null): EntryReading => ({
|
|
325
|
+
entry,
|
|
326
|
+
through: (v) => {
|
|
327
|
+
const d = defs.get(v);
|
|
328
|
+
if (d === undefined || observable(d)) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
if (!spelledWhenDead(d)) {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
const b = defBlock.get(v);
|
|
335
|
+
return b === R || b === Q || (onwardBlock !== null && b === onwardBlock) || (b === E && eReads.has(d));
|
|
336
|
+
},
|
|
337
|
+
normalize: true,
|
|
338
|
+
});
|
|
339
|
+
// which of Q's edges E's sense decides: E's edge `index` holds when E's condition is
|
|
340
|
+
// (index 0 ? true : false); Q's edge 0 holds when Q's is true
|
|
341
|
+
const rd = reading(null);
|
|
342
|
+
const same = sameAtEntry(defs, qTerm.operands[0], eTerm.operands[0], rd, false);
|
|
343
|
+
const negated = !same && sameAtEntry(defs, qTerm.operands[0], eTerm.operands[0], rd, true);
|
|
344
|
+
site.decided = same || negated;
|
|
345
|
+
// The sense, where the comparison said none, is the one the codes alone suggest: what an ablated
|
|
346
|
+
// `test-differs` would act on, so each later rule judges the rewrite that ablation would make.
|
|
347
|
+
const asNegated =
|
|
348
|
+
negated ||
|
|
349
|
+
(!same && NEGATED_ICMP[defs.get(qTerm.operands[0])?.opcode ?? ''] === defs.get(eTerm.operands[0])?.opcode);
|
|
350
|
+
const eHolds = index === 0;
|
|
351
|
+
const rHolds = asNegated ? !eHolds : eHolds;
|
|
352
|
+
const decided = qTerm.successors[rHolds ? 0 : 1];
|
|
353
|
+
const eArgs = eTerm.successors[index].args;
|
|
354
|
+
if (decided.block === T) {
|
|
355
|
+
site.onward =
|
|
356
|
+
decided.args.length === eArgs.length && decided.args.every((a, k) => sameAtEntry(defs, a, eArgs[k], rd));
|
|
357
|
+
return site;
|
|
358
|
+
}
|
|
359
|
+
// through one block S: no params, one edge on to T, entered only from Q, and (judged with R's and
|
|
360
|
+
// Q's ops above) nothing observable
|
|
361
|
+
const S = decided.block;
|
|
362
|
+
const sTerm = S.ops[S.ops.length - 1];
|
|
363
|
+
if (
|
|
364
|
+
S === R ||
|
|
365
|
+
S === Q ||
|
|
366
|
+
body.has(S) ||
|
|
367
|
+
S.params.length > 0 ||
|
|
368
|
+
sTerm?.opcode !== 'br' ||
|
|
369
|
+
sTerm.successors[0].block !== T ||
|
|
370
|
+
(preds.get(S) ?? []).some((p) => p !== Q)
|
|
371
|
+
) {
|
|
372
|
+
return site;
|
|
373
|
+
}
|
|
374
|
+
site.observable ||= S.ops.some(observable);
|
|
375
|
+
const sd = reading(S);
|
|
376
|
+
const sArgs = sTerm.successors[0].args;
|
|
377
|
+
site.onward = sArgs.length === eArgs.length && sArgs.every((a, k) => sameAtEntry(defs, a, eArgs[k], sd));
|
|
378
|
+
return site;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const terminatorEdges = (b: Block) => b.ops[b.ops.length - 1]?.successors ?? [];
|
|
382
|
+
|
|
383
|
+
/** Does `from`, or a block it reaches, read a value defined in one of `blocks`? */
|
|
384
|
+
function readsFrom(from: Block, blocks: ReadonlySet<Block>, defBlock: ReadonlyMap<Value, Block>): boolean {
|
|
385
|
+
const seen = new Set<Block>([from]);
|
|
386
|
+
const work = [from];
|
|
387
|
+
const inside = (v: Value) => blocks.has(defBlock.get(v)!);
|
|
388
|
+
for (let b = work.pop(); b !== undefined; b = work.pop()) {
|
|
389
|
+
if (b.ops.some((op) => op.operands.some(inside) || op.successors.some((e) => e.args.some(inside)))) {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
for (const n of successorsOf(b)) {
|
|
393
|
+
if (!seen.has(n)) {
|
|
394
|
+
seen.add(n);
|
|
395
|
+
work.push(n);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// asmlift — 64-bit runtime-helper lowering (L1 recognition).
|
|
2
|
+
//
|
|
3
|
+
// The peer of `raise/softdiv.ts`, one width up, and NOT gated on a hardware capability. A soft
|
|
4
|
+
// division is a division the ISA has no instruction for, so a target with a divider never emits
|
|
5
|
+
// one; no ISA this repo targets has 64-bit integer arithmetic at all, so `bl __muldi3` is software
|
|
6
|
+
// on every one of them whatever `hwDivide` says.
|
|
7
|
+
//
|
|
8
|
+
// By the time this runs the frontend has already read the call's arguments as 64-bit values and
|
|
9
|
+
// split its result back into the register pair (`frontend/thumb.ts`, at the `bl`), so the rewrite
|
|
10
|
+
// itself is the same one-line splice `softdiv` does: the helper's op over the same operands,
|
|
11
|
+
// reusing the SAME result value so every existing use already points at it.
|
|
12
|
+
//
|
|
13
|
+
// The signedness of the OPERATION comes from the helper name where the name splits it
|
|
14
|
+
// (`__divdi3`/`__udivdi3`) and from the operands where it does not — agbcc's `__muldi3` serves
|
|
15
|
+
// both spellings, so nothing here may read a signedness off it. See the table's own note.
|
|
16
|
+
import { Fn, mkOp } from '../ir/core';
|
|
17
|
+
import { arrivesAsDeclared, helperOp, isFloatHelper, isWideHelper, lookupHelper } from '../runtime-helpers';
|
|
18
|
+
import type { TargetDescription } from '../target';
|
|
19
|
+
|
|
20
|
+
/** Rewrite each recognised 64-bit helper call to the op it computes, in place. Returns whether
|
|
21
|
+
* anything changed. Runs BEFORE type recovery, so the operands get their signedness there. */
|
|
22
|
+
export function recognizeWideHelpers(fn: Fn, target: TargetDescription): boolean {
|
|
23
|
+
const table = target.runtimeHelpers ?? {};
|
|
24
|
+
let changed = false;
|
|
25
|
+
for (const b of fn.blocks) {
|
|
26
|
+
for (let i = 0; i < b.ops.length; i++) {
|
|
27
|
+
const op = b.ops[i];
|
|
28
|
+
if (op.opcode !== 'call') {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const helper = lookupHelper(table, String(op.attrs.target));
|
|
32
|
+
if (!helper?.op || !isWideHelper(helper) || isFloatHelper(helper)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// ITS C PARAMETERS AT THEIR STATED WIDTHS, not its argument registers and not their count:
|
|
36
|
+
// the frontend has paired the registers up, so a `__ashrdi3` that occupied three of them
|
|
37
|
+
// arrives here as a 64-bit value and a word. That pair construction is the evidence the fold
|
|
38
|
+
// rests on, and `arrivesAsDeclared` is the precondition for reading it — an operand count
|
|
39
|
+
// says nothing about what the operands hold, and a call carrying the right count of the
|
|
40
|
+
// wrong things folds into an operation over one half of each value.
|
|
41
|
+
//
|
|
42
|
+
// WHAT DECLINES HERE DOES NOT PASS THROUGH: `refuseUnmodelledHelpers` below gaps every
|
|
43
|
+
// surviving call to a name this table carries, so a shape this cannot fold gets the loud
|
|
44
|
+
// answer rather than a plausible one.
|
|
45
|
+
if (
|
|
46
|
+
!arrivesAsDeclared(
|
|
47
|
+
helper,
|
|
48
|
+
op.operands.map((o) => o.type),
|
|
49
|
+
op.results.map((r) => r.type),
|
|
50
|
+
)
|
|
51
|
+
) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
b.ops.splice(i, 1, helperOp(helper.op, String(op.attrs.target), op.operands, op.results[0]));
|
|
55
|
+
changed = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return changed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Turn every surviving call to one of the target's runtime helpers into a gap. Returns whether
|
|
62
|
+
* anything changed. Runs after the helper recognisers, so what it sees is what they declined.
|
|
63
|
+
*
|
|
64
|
+
* RE-EMITTING A COMPILER'S OWN RUNTIME CALL IS NOT A RECOVERY, and it is worse than a plain
|
|
65
|
+
* miss: it MATCHES. Hand `mwcceppc` the source `return __div2i(a, b);` and it emits the `bl
|
|
66
|
+
* __div2i` the row was lifted from, byte for byte — so the differ scores asmlift's failure to
|
|
67
|
+
* model 64-bit division exactly as it would score modelling it. A row that cannot tell the two
|
|
68
|
+
* apart is measuring nothing, and four cells of the synthetic 64-bit family were banking that.
|
|
69
|
+
*
|
|
70
|
+
* ONLY THE NAMES THE TARGET CARRIES, and a name outside the table stays an ordinary callee —
|
|
71
|
+
* which is right, because a project's own `__`-prefixed function is not this compiler's runtime
|
|
72
|
+
* and asmlift cannot tell them apart by spelling. A target with NO table therefore refuses
|
|
73
|
+
* nothing and spells every helper call it makes, which is the configuration this refusal exists
|
|
74
|
+
* to remove (`TargetDescription.runtimeHelpers`).
|
|
75
|
+
*
|
|
76
|
+
* An `opaque` rather than a throw, so the gap behaves like every other one: strict mode declines
|
|
77
|
+
* naming it, annotate mode marks it and leaves the rest of the function standing. */
|
|
78
|
+
export function refuseUnmodelledHelpers(fn: Fn, target: TargetDescription): boolean {
|
|
79
|
+
const table = target.runtimeHelpers;
|
|
80
|
+
if (!table) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
let changed = false;
|
|
84
|
+
for (const b of fn.blocks) {
|
|
85
|
+
for (let i = 0; i < b.ops.length; i++) {
|
|
86
|
+
const op = b.ops[i];
|
|
87
|
+
// Membership, through the table's one reader: `in` would also answer for `toString` and every
|
|
88
|
+
// other name on `Object.prototype`, and this is the refusal, so the fabricated reason would be
|
|
89
|
+
// the whole of what a caller saw.
|
|
90
|
+
if (op.opcode !== 'call' || lookupHelper(table, String(op.attrs.target)) === undefined) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
b.ops.splice(
|
|
94
|
+
i,
|
|
95
|
+
1,
|
|
96
|
+
mkOp('opaque', {
|
|
97
|
+
operands: [...op.operands],
|
|
98
|
+
results: [...op.results],
|
|
99
|
+
attrs: { helper: op.attrs.target },
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
changed = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return changed;
|
|
106
|
+
}
|