@asmlift/core 0.9.0 → 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 +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +165 -50
- package/src/backend/cpp.ts +2 -1
- package/src/backend/pascal.ts +31 -12
- package/src/contracts.ts +173 -16
- package/src/declare.ts +69 -20
- package/src/frontend/asmdata.ts +7 -1
- package/src/frontend/call-plan.ts +964 -60
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +23 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1232 -192
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2076 -215
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2711 -232
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1460 -44
- package/src/frontend/stackargs.ts +106 -57
- package/src/frontend/thumb.ts +2044 -318
- package/src/ir/alias.ts +42 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +100 -5
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +159 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +109 -9
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +80 -11
- package/src/l3/ast.ts +290 -18
- package/src/l3/basecse.ts +33 -5
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +2 -2
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +31 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +9 -4
- package/src/l3/reindex.ts +283 -96
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +4 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +541 -24
- package/src/proto-context.ts +905 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +10 -26
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +102 -19
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +694 -0
- package/src/raise/shortcircuit.ts +26 -12
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +89 -31
- package/src/rank.ts +307 -54
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +165 -2
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +2180 -1054
- package/src/structure/switch-recover.ts +244 -100
- package/src/symbols.ts +6 -0
- package/src/target.ts +765 -52
- package/src/trace.ts +26 -8
- package/src/variation-definitions.ts +126 -14
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +5 -1
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
/** THE DEAD-READ PIN: a memory read the lifted function never uses was `volatile` in the source, so
|
|
2
|
+
* it is stamped `volatile` (the `device` placement, ir/discipline.ts) before anything can delete
|
|
3
|
+
* it, and the structurer spells it through a qualified pointer or declines.
|
|
4
|
+
*
|
|
5
|
+
* The claim is the COMPILER's, and it is the one ir/discipline.ts `spelledWhenDead` already rests
|
|
6
|
+
* on: an optimizing compiler's dead-code pass deletes every plain read whose value nothing uses.
|
|
7
|
+
* A pass that runs after it can still leave one behind — a split, a merge, a spill — and the gates
|
|
8
|
+
* below name each one found. agbcc expands an expression statement with no target (stmt.c
|
|
9
|
+
* `expand_expr_stmt`) and its flow pass keeps an insn whose result is dead only when
|
|
10
|
+
* `volatile_refs_p` says it touches a `MEM_VOLATILE_P` cell
|
|
11
|
+
* (flow.c:1296, rtlanal.c:1628); measured, agbcc at -O2 and -O0, ido 7.1, gcc 2.7.2 (KMC) and
|
|
12
|
+
* mwcc 2.4.2 each emit no read for `gR; *p; *(int *)0x4000004;` and keep every one of them
|
|
13
|
+
* qualified. So a read still in the target whose result is overwritten or never consumed is the
|
|
14
|
+
* `dmaRegs[2];` that ends a DMA macro, a `REG_KEYINPUT;` acknowledge, an `extern volatile` poll
|
|
15
|
+
* read once for its side effect — whatever the address, symbol or literal or a pointer parameter.
|
|
16
|
+
* Left plain, the lift's own dead-code pass (pattern/engine.ts `dce`) drops it, and the recompile
|
|
17
|
+
* loses an access the machine made. This pin is the ONE place that claim is decided: the structurer
|
|
18
|
+
* spells a read nothing uses only when it is qualified (structure.ts `volatileQualifiable`).
|
|
19
|
+
*
|
|
20
|
+
* DEAD means what that pass would delete (ir/discipline.ts `deadOps`, the one definition): a
|
|
21
|
+
* `load`/`aload` whose result reaches no op the pass keeps, through any chain of ops it would delete
|
|
22
|
+
* with it — an extension of the read nothing reads is as dead as the read. It is decided on the RAW
|
|
23
|
+
* lift, after the frontend's SSA is final and before any raise rewrite, so a read only asmlift's
|
|
24
|
+
* own folds orphan is never counted — save one the frontend orphans itself by dropping a spill
|
|
25
|
+
* nothing reloads, which `spilled` refuses. One use the pass counts is not one here: the operand of the
|
|
26
|
+
* `ret` of a function its OWN declaration says is void, which the structurer suppresses — the
|
|
27
|
+
* read-back that ends `void f(int b) { REG = b; REG; }` is in the return register at `bx lr`.
|
|
28
|
+
*
|
|
29
|
+
* THE READS ARE DECIDED OUTERMOST FIRST. A dead read whose value, through dead ops, is the address
|
|
30
|
+
* of another dead read waits until that one is decided: kept, it is a use, and the inner read is
|
|
31
|
+
* live — `*(volatile int *)*pp;` qualifies the read of `*pp`'s target, not the read of `pp`.
|
|
32
|
+
*
|
|
33
|
+
* The reads the rule must not claim are tabled (`DEAD_READ_GATES`, in this order) and REPORTED: a
|
|
34
|
+
* caller may hand its own table (`tallying`, l3/gates.ts) to census the refusals.
|
|
35
|
+
* • a read of a function built at a level where the compiler keeps a plain read nothing uses
|
|
36
|
+
* (target.ts `keepsUnusedReadsAt`) on a compiler that names the register it discards a
|
|
37
|
+
* volatile read into (target.ts `discardsUnusedReadsInto`), save one the machine loaded into
|
|
38
|
+
* that register and left there (`discarded`): mwcc at `-O0` keeps `x = p[5]; x = 0;`'s load in
|
|
39
|
+
* r31 and IDO at `-O1` in t6, so a read in a home says nothing about `volatile`, where `*p;`
|
|
40
|
+
* through a volatile pointer is `lwz r0` and `lw zero` — and a level no flag names, or one a
|
|
41
|
+
* word the flag table cannot read may set (mwcc `-opt off`), is judged as keeping. A load whose
|
|
42
|
+
* value, or a value computed from it, is written to another register is not discarded
|
|
43
|
+
* (`leftInDiscardRegister`: mwcc's `lwz r0` + `extsb r31,r0` of `signed char x; x = p[5];
|
|
44
|
+
* x = 0;`); the second word of IDO's doubleword discard, `lw at,4(a0)` after `lw zero,0(a0)`,
|
|
45
|
+
* is (frontend/mips.ts `emitLoad`);
|
|
46
|
+
* • a read of a global the project declares with no volatile byte where it reads (`declared-plain`:
|
|
47
|
+
* the symbol map where it lays the object out, else the declaration context): the source spelled
|
|
48
|
+
* the read plain, so the compiler kept it for some other reason — agbcc merges the identical
|
|
49
|
+
* arms of `if (g.c) x = 0x800; else x = 0x800;` (kleod's `sub_08042024`) after flow has decided
|
|
50
|
+
* which insns are dead, and the read the deleted branch tested survives, plain;
|
|
51
|
+
* • a read of the function's own frame object (`laddr`): a local nothing outside the function
|
|
52
|
+
* observes, whose dead read is a reload, not an access to keep;
|
|
53
|
+
* • any read of a function that holds an unmodelled instruction (`opaque`, frontend/opaque.ts):
|
|
54
|
+
* the lift does not know what that instruction reads, so it cannot call a value unused —
|
|
55
|
+
* mwcc's `lbz r0, 76(r3)` + `rlwimi r0, r4, 7, 24, 24` + `stb` reads the byte it inserts into,
|
|
56
|
+
* and the opaque standing for the `rlwimi` does not name it;
|
|
57
|
+
* • a read still held, at a return whose width the lift guessed, in the register that carries
|
|
58
|
+
* the second word of a two-word return (`unreadPairHalf`): asmlift returns one word where no
|
|
59
|
+
* declaration pins the width, so a value in the pair's second register is unused by the LIFT,
|
|
60
|
+
* not by the source — agbcc's leaf `long long k(long long *p) { return *p; }` is
|
|
61
|
+
* `ldr r1, [r0, #4]; ldr r0, [r0]; bx lr`, and stamping the r1 load would publish a one-word
|
|
62
|
+
* return that drops the high half beside an invented volatile read of RAM, byte-exact;
|
|
63
|
+
* • a read still held in an argument register at a call whose arity no declaration states
|
|
64
|
+
* (call-plan.ts `heldAtUndeclaredCall`): the guess passes what it counts, so a value it leaves
|
|
65
|
+
* out is unused by the lift, not by the source — agbcc's `g(a, c ? p[1] : p[2])` leaves r0 at
|
|
66
|
+
* its incoming value, the guess passes nothing, and stamping the r1 loads publishes `g();` beside
|
|
67
|
+
* two invented volatile reads;
|
|
68
|
+
* • a read whose value was stored to the function's own stack (`spilled`, ssa.ts `frameWrites`):
|
|
69
|
+
* each frontend keeps a stack slot as a variable and drops a store nothing reloads, so a read
|
|
70
|
+
* only that store consumed looks dead because the lift dropped its consumer — KMC's `struct S
|
|
71
|
+
* s = *p; return s.a;` stores both words to the frame, IDO a `long long` it truncates;
|
|
72
|
+
* • a read that is the address of a read the pin left plain (`addresses-plain-read`): the outer
|
|
73
|
+
* read was decided first, and the pointer it read through was the compiler's to compute;
|
|
74
|
+
* • a read the compiler could not have made of a `volatile` (target.ts `readCouldBeVolatile`): on
|
|
75
|
+
* a compiler that extends a qualified narrow read in a register, a sign-extending load is a
|
|
76
|
+
* plain read in the source, the same rule frontend/device-pins.ts applies to a device read;
|
|
77
|
+
* • `undecided-at-keeping-level`, whose verdict REFUSES THE LIFT instead of leaving the read
|
|
78
|
+
* plain: a read at a keeping level on a compiler that names no discard register, which none of
|
|
79
|
+
* the gates above claims. agbcc, gcc 2.7.2 and KMC at `-O0` load a volatile `*p;` into a
|
|
80
|
+
* register nothing then reads (`ldr r0`, `lw v0`), and a `register` local assigned and
|
|
81
|
+
* overwritten (`register int x; x = p[5]; x = 0;`) is the same instructions; agbcc's
|
|
82
|
+
* `if (*vp) {}` is the same `ldr; cmp; beq` as the plain `if (p[0]) {}`. Neither spelling is in
|
|
83
|
+
* evidence, and either one, published, is a function the machine's source did not write:
|
|
84
|
+
* dropped, the volatile access is gone; stamped, the plain one gains an access. It sits BEFORE
|
|
85
|
+
* the two gates below, which rest on an optimiser's premise a keeping level does not keep;
|
|
86
|
+
* • a read a compare consumed (`compared`, gathered by each frontend that fuses a compare into the
|
|
87
|
+
* branch that reads it): a compare no branch reads leaves no op, so the read looks dead only
|
|
88
|
+
* because the lift has no op for its consumer — mwcc 2.3.3 at `-O4,s` keeps
|
|
89
|
+
* `lwz r0, g; cmpwi r0, 0` for `if (g) { (g != 0); }`;
|
|
90
|
+
* • a read the machine computed with (`computed`): an op other than an extension or a read
|
|
91
|
+
* consumes the value, or an extension of it — an op dead in the lift too, but one the compiler
|
|
92
|
+
* emitted, so it did not judge the value unused there. KMC splits a `long long` sum after flow,
|
|
93
|
+
* and keeps the high word's `addu` of `v += 3; return (int)v;`.
|
|
94
|
+
* RESIDUE, untabled: reads stamped with a `volatile` the source did not write, or spelled at
|
|
95
|
+
* another width. The first four recompile byte-exact and compute the same values; the next two
|
|
96
|
+
* inherit a wrong input; the last performs the same accesses and does not recompile byte-exact.
|
|
97
|
+
* • a bare read an inline `asm("" :: "r"(x))` took whole. agbcc leaves the statement as a bare
|
|
98
|
+
* `.code 16` the Thumb frontend skips, so the read it keeps alive looks dead; where the asm took
|
|
99
|
+
* an extraction (sa3's `Task_150_80A619C` reads `gPlayers[0].charFlags.character` with
|
|
100
|
+
* `lsl`/`lsr` first), `computed` leaves it plain;
|
|
101
|
+
* • the unused half of a DImode move under a declaration that pins a one-word return. agbcc splits
|
|
102
|
+
* the move into two word loads AFTER flow has deleted dead insns
|
|
103
|
+
* (`thumb_load_double_from_address`), so `int f(long long *p) { long long v = *p; return
|
|
104
|
+
* (int)v; }` keeps `ldr r1, [r0, #4]`. With no such declaration the pair gate refuses it on Thumb
|
|
105
|
+
* and PowerPC; O32 declines the v0:v1 pair before the pin is asked;
|
|
106
|
+
* • a plain read under an arithmetic or a sign-extending cast nothing uses, at mwcc `-O0`:
|
|
107
|
+
* `p[5] + 1;` and `(char)p[5];` keep `lwz r0, 20(r3)` (the cast with an `extsb`/`extsh` of r0
|
|
108
|
+
* where `char` is signed), which is the register the compiler discards a volatile read into,
|
|
109
|
+
* so the read is stamped. A zero-extending cast (`clrlwi r0,r0,24`: `(unsigned char)`, and
|
|
110
|
+
* `(char)` under `-char unsigned`, as the 2.4.7 and 2.3.3 projects build) lowers to an `and`,
|
|
111
|
+
* which `computed` rejects, so that read is left plain. No unit of Mario Party 4's 409 at
|
|
112
|
+
* `-O0,p` holds one;
|
|
113
|
+
* • merged identical arms where no declaration is in evidence: the same instructions as `gR;` on an
|
|
114
|
+
* object nothing declares, and as `*p;` through a pointer parameter;
|
|
115
|
+
* • a read returned by a getter wrongly declared void: the declaration is trusted, as the pair
|
|
116
|
+
* gate trusts a declared width;
|
|
117
|
+
* • hand-written assembly, which is outside the compiler claim: klonoa's m4a `sub_0804FE6C` returns
|
|
118
|
+
* a byte in r3, which no ABI the lift models reads, so the lift's return model has already lost
|
|
119
|
+
* that byte and its read is stamped;
|
|
120
|
+
* • IDO's doubleword discard (`volatile long long`/`double` `*p;`, `lw zero` + `lw at`) is
|
|
121
|
+
* stamped as two word reads, which recompile to two `lw zero`.
|
|
122
|
+
* DROPPED, untabled: volatile reads left plain, which the lift's dead-code pass deletes.
|
|
123
|
+
* • a volatile read assigned to a local nothing reads (`x = *vp; x = 0;`, `int x = *vp;`) at a
|
|
124
|
+
* keeping level: the same instructions as its plain twin on every compiler measured (mwcc's
|
|
125
|
+
* r31 home, or `lwz r0` + `extsb r31,r0` for a `char` local, IDO's `t6` and a frame store,
|
|
126
|
+
* agbcc's and gcc's frame store), so no fact tells the two apart, and the plain one is the
|
|
127
|
+
* population: of the 925 non-frame loads nothing reads over Mario Party 4's -O0,p units, 924
|
|
128
|
+
* sit in r17-r31 and f18-f31 homes, and the other is a volatile read in r0;
|
|
129
|
+
* • a read held in the return pair's second register at a return whose width the lift guessed,
|
|
130
|
+
* at a keeping level on a compiler with no discard register: agbcc `-O0` keeps
|
|
131
|
+
* `return (*p, 1);`'s volatile read in r1, and `held-at-word-return` leaves it plain before
|
|
132
|
+
* the refusal is asked. A declaration that pins the return width sends it to the refusal;
|
|
133
|
+
* • a read in the discard register that a dead compare or computation consumes, at mwcc `-O0`:
|
|
134
|
+
* `-*vp;` (`neg r0,r0`), `!*vp;` and `*vp ? 1 : 2;` are the same instructions as their plain
|
|
135
|
+
* twins, which mwcc keeps too; the volatile bitfield `s->b;` (`lbz r0` + `clrlwi r0,r0,27`) is
|
|
136
|
+
* not — its plain twin compiles to nothing — and is dropped all the same;
|
|
137
|
+
* • a read in a home that a dead compare consumes at IDO `-O1`: `if (*vp) {}` and `*vp ? 1 : 2;`
|
|
138
|
+
* are `lw t6` + `beqz t6`, where the plain twins compile to nothing, so the compare is evidence
|
|
139
|
+
* of `volatile` there, and no gate reads it (at `-O0` the plain twin is the same code). */
|
|
140
|
+
import { globalBaseOf, globalCellOf } from '../ir/alias';
|
|
141
|
+
import { type Block, type Op, type Successor, type Value, forwardingTarget } from '../ir/core';
|
|
142
|
+
import { deadOps, spelledWhenDead } from '../ir/discipline';
|
|
143
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
144
|
+
import { type FnProto, declaresVoidReturn, ownReturnWidth } from '../proto';
|
|
145
|
+
import type { ContextGlobals } from '../proto-context';
|
|
146
|
+
import { type SymbolInfo, type SymbolMap, declaresVolatile, symbolsByName } from '../symbols';
|
|
147
|
+
import { type TargetDescription, readCouldBeVolatile } from '../target';
|
|
148
|
+
import { FrontendUnsupportedError } from './errors';
|
|
149
|
+
import type { SsaBuilder } from './ssa';
|
|
150
|
+
|
|
151
|
+
/** What a dead-read gate judges: the read, and the facts about it the gates read. */
|
|
152
|
+
export interface DeadRead {
|
|
153
|
+
readonly op: Op;
|
|
154
|
+
/** its address is this function's own frame object, an offset of a `laddr` */
|
|
155
|
+
readonly fromFrame: boolean;
|
|
156
|
+
/** the function holds an instruction the lift could not model (an `opaque`) */
|
|
157
|
+
readonly unmodelled: boolean;
|
|
158
|
+
/** its value, or a value only it feeds, is still in the return pair's second register at a return
|
|
159
|
+
* whose width the lift guessed (`unreadPairHalf`) */
|
|
160
|
+
readonly heldAtWordReturn: boolean;
|
|
161
|
+
/** its value, or a value only it feeds, is still in an argument register at a call whose arity no
|
|
162
|
+
* declaration states (call-plan.ts `heldAtUndeclaredCall`) */
|
|
163
|
+
readonly heldAtUndeclaredCall: boolean;
|
|
164
|
+
/** its value, or a value only it feeds, is an operand of a compare (`DeadReadFacts.compared`) */
|
|
165
|
+
readonly compared: boolean;
|
|
166
|
+
/** its value, or a value only it feeds, was stored to the function's own stack
|
|
167
|
+
* (`DeadReadFacts.spilled`) */
|
|
168
|
+
readonly spilled: boolean;
|
|
169
|
+
/** its value, or an extension of it, is an operand of an op that is neither an extension nor a
|
|
170
|
+
* read (`usesOf`): arithmetic the machine emitted */
|
|
171
|
+
readonly computed: boolean;
|
|
172
|
+
/** its value, or an extension of it, is the address of a read the pin left plain (`usesOf`) */
|
|
173
|
+
readonly addressesPlainRead: boolean;
|
|
174
|
+
/** its address reaches one global, declared with no volatile byte where it reads (`declaredPlain`) */
|
|
175
|
+
readonly declaredPlain: boolean;
|
|
176
|
+
/** the machine loaded it into the register the compiler discards an unused read into
|
|
177
|
+
* (`DeadReadFacts.discarded`, target.ts `discardsUnusedReadsInto`) */
|
|
178
|
+
readonly discarded: boolean;
|
|
179
|
+
readonly target: TargetDescription;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The gate whose verdict is a REFUSAL of the lift, not a read left plain: see its entry in the
|
|
183
|
+
* file header. A table without it (`without`) leaves the read to the gates after it. */
|
|
184
|
+
const UNDECIDED = 'undecided-at-keeping-level';
|
|
185
|
+
|
|
186
|
+
export const DEAD_READ_GATES: readonly Gate<DeadRead>[] = [
|
|
187
|
+
{
|
|
188
|
+
id: 'keeps-unused-reads',
|
|
189
|
+
why: 'at these flags the compiler keeps a plain read nothing uses, and this one is not in the register it discards a volatile read into',
|
|
190
|
+
sound: true,
|
|
191
|
+
guardedBy: 'dead-reads.test.ts: keeps-unused-reads: mwcc -O0 keeps a dead assignment plain',
|
|
192
|
+
rejects: (c) =>
|
|
193
|
+
c.target.compilerBehaviors.keepsUnusedReads === true &&
|
|
194
|
+
c.target.compilerBehaviors.discardsUnusedReadsInto !== undefined &&
|
|
195
|
+
!c.discarded,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 'declared-plain',
|
|
199
|
+
why: 'the declaration of the object read has no volatile byte there, so the source spelled the read plain',
|
|
200
|
+
sound: false,
|
|
201
|
+
guardedBy: 'dead-reads.test.ts: declared-plain: a dead read of a global declared plain stays deletable',
|
|
202
|
+
rejects: (c) => c.declaredPlain,
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 'frame-object',
|
|
206
|
+
why: 'a dead read of a local of this function is a reload, which nothing outside it observes',
|
|
207
|
+
sound: false,
|
|
208
|
+
guardedBy: 'dead-reads.test.ts: frame-object: a dead read of a frame object stays deletable',
|
|
209
|
+
rejects: (c) => c.fromFrame,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: 'unmodelled-instruction',
|
|
213
|
+
why: 'an instruction the lift could not model may be what reads the value, so the read is not proved dead',
|
|
214
|
+
sound: false,
|
|
215
|
+
guardedBy: 'dead-reads.test.ts: unmodelled-instruction: a read an unmodelled instruction may consume stays plain',
|
|
216
|
+
rejects: (c) => c.unmodelled,
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: 'held-at-word-return',
|
|
220
|
+
why: 'the value may be the second word of a two-word return the lift lowered as one word',
|
|
221
|
+
sound: false,
|
|
222
|
+
guardedBy: 'dead-reads.test.ts: held-at-word-return: a leaf 64-bit return from memory keeps no volatile high word',
|
|
223
|
+
rejects: (c) => c.heldAtWordReturn,
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: 'held-at-undeclared-call',
|
|
227
|
+
why: 'the value may be an argument a guessed arity left out of the call',
|
|
228
|
+
sound: false,
|
|
229
|
+
guardedBy: 'dead-reads.test.ts: held-at-undeclared-call: a read the guessed arity leaves out stays plain',
|
|
230
|
+
rejects: (c) => c.heldAtUndeclaredCall,
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: 'spilled',
|
|
234
|
+
why: 'the machine stored the value to its stack; the lift keeps the slot as a variable and drops a store nothing reloads',
|
|
235
|
+
sound: false,
|
|
236
|
+
guardedBy: 'dead-reads.test.ts: spilled: a read whose value was stored to the frame stays plain',
|
|
237
|
+
rejects: (c) => c.spilled,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: 'addresses-plain-read',
|
|
241
|
+
why: 'the value is the address of a read left plain, which the compiler used it to make',
|
|
242
|
+
sound: false,
|
|
243
|
+
guardedBy:
|
|
244
|
+
'dead-reads.test.ts: addresses-plain-read: a read that is the address of a read the pin left plain is that read’s use',
|
|
245
|
+
rejects: (c) => c.addressesPlainRead,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: 'not-a-volatile-read',
|
|
249
|
+
why: 'a read that extends in a way the compiler never extends a volatile one was plain in the source',
|
|
250
|
+
sound: false,
|
|
251
|
+
guardedBy: 'dead-reads.test.ts: not-a-volatile-read: a dead sign-extending narrow read stays plain on agbcc',
|
|
252
|
+
rejects: (c) => !readCouldBeVolatile(c.target.compilerBehaviors, c.op),
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: UNDECIDED,
|
|
256
|
+
why: 'at these flags the compiler keeps a plain read nothing uses and has no register of its own for a volatile one, so the lift refuses',
|
|
257
|
+
sound: true,
|
|
258
|
+
guardedBy:
|
|
259
|
+
'dead-reads.test.ts: undecided-at-keeping-level: a keeping level on a compiler with no discard register refuses a read no evidence claims',
|
|
260
|
+
rejects: (c) =>
|
|
261
|
+
c.target.compilerBehaviors.keepsUnusedReads === true &&
|
|
262
|
+
c.target.compilerBehaviors.discardsUnusedReadsInto === undefined,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
id: 'compared',
|
|
266
|
+
why: 'a compare consumed the value; the lift has no op for a compare no branch reads',
|
|
267
|
+
sound: false,
|
|
268
|
+
guardedBy: 'dead-reads.test.ts: compared: a read only a dead compare consumes stays plain',
|
|
269
|
+
rejects: (c) => c.compared,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: 'computed',
|
|
273
|
+
why: 'the machine computed with the value, so the compiler did not judge it unused where it emitted that op',
|
|
274
|
+
sound: false,
|
|
275
|
+
guardedBy: 'dead-reads.test.ts: computed: a read a dead add consumes stays plain',
|
|
276
|
+
rejects: (c) => c.computed,
|
|
277
|
+
},
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
/** What a frontend knows about its lift that the IR does not carry, for `pinDeadReads`. */
|
|
281
|
+
export interface DeadReadFacts {
|
|
282
|
+
/** the function's own name, for the refusal `undecided-at-keeping-level` makes */
|
|
283
|
+
readonly name?: string;
|
|
284
|
+
/** the function's own declaration: a declared void return's `ret` operand is no use */
|
|
285
|
+
readonly own?: FnProto;
|
|
286
|
+
/** what the return pair's second register may hold at a guessed word return (`unreadPairHalf`) */
|
|
287
|
+
readonly atWordReturn?: ReadonlySet<Value>;
|
|
288
|
+
/** what an argument register may hold at a call of undeclared arity (call-plan.ts) */
|
|
289
|
+
readonly atUndeclaredCall?: ReadonlySet<Value>;
|
|
290
|
+
/** every value a compare consumed, where a compare leaves no op of its own: Thumb's `cmp` and
|
|
291
|
+
* PowerPC's `cmpw`/`cmpwi`/`cmplw`/`cmplwi` and record forms fuse into the branch that reads
|
|
292
|
+
* them, and one no branch reads is gone */
|
|
293
|
+
readonly compared?: ReadonlySet<Value>;
|
|
294
|
+
/** every value the machine stored to its own stack, which the lift keeps as a slot variable
|
|
295
|
+
* (ssa.ts `frameWrites`) */
|
|
296
|
+
readonly spilled?: ReadonlySet<Value>;
|
|
297
|
+
/** every value a load put in the register the compiler discards an unused read into
|
|
298
|
+
* (target.ts `discardsUnusedReadsInto`): mwcc's `lwz r0`, IDO's `lw zero` */
|
|
299
|
+
readonly discarded?: ReadonlySet<Value>;
|
|
300
|
+
/** where a read's object is declared: the project's symbol map and the declaration context */
|
|
301
|
+
readonly declarations?: Declarations;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The loads a frontend saw the discard register take (`discarded`) that no write outside that
|
|
305
|
+
* register carried, which is what `DeadReadFacts.discarded` holds. `written` is every value the
|
|
306
|
+
* frontend wrote to another register; each is walked back through the ops that computed it, and a
|
|
307
|
+
* discarded load the walk reaches was put in a home, so it is no read the compiler discarded:
|
|
308
|
+
* mwcc at -O0,p reads `signed char x; x = p[5]; x = 0;` as `lwz r0,20(r3)` + `extsb r31,r0`, its
|
|
309
|
+
* `unsigned char` twin as `clrlwi r31,r0,24`, and a volatile twin of either is the same
|
|
310
|
+
* instructions. Keyed on the write, not on the mnemonics that move or extend, so how a frontend
|
|
311
|
+
* lowers the op does not decide it. A block parameter ends the walk. An extension the discard
|
|
312
|
+
* register keeps (`(char)*vp;` is `extsb r0,r0`) stays discarded. */
|
|
313
|
+
export function leftInDiscardRegister(
|
|
314
|
+
irBlocks: readonly Block[],
|
|
315
|
+
discarded: ReadonlySet<Value>,
|
|
316
|
+
written: readonly Value[],
|
|
317
|
+
): Set<Value> {
|
|
318
|
+
if (discarded.size === 0) {
|
|
319
|
+
return new Set();
|
|
320
|
+
}
|
|
321
|
+
const defOf = new Map<Value, Op>();
|
|
322
|
+
for (const blk of irBlocks) {
|
|
323
|
+
for (const op of blk.ops) {
|
|
324
|
+
op.results.forEach((r) => defOf.set(r, op));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const seen = new Set<Value>();
|
|
328
|
+
const homed = new Set<Value>();
|
|
329
|
+
const stack = [...written];
|
|
330
|
+
while (stack.length > 0) {
|
|
331
|
+
const v = stack.pop()!;
|
|
332
|
+
if (seen.has(v)) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
seen.add(v);
|
|
336
|
+
if (discarded.has(v)) {
|
|
337
|
+
homed.add(v);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
stack.push(...(defOf.get(v)?.operands ?? []));
|
|
341
|
+
}
|
|
342
|
+
return new Set([...discarded].filter((v) => !homed.has(v)));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** The declarations `declared-plain` reads. */
|
|
346
|
+
export interface Declarations {
|
|
347
|
+
readonly symbols?: SymbolMap;
|
|
348
|
+
readonly context?: ContextGlobals;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** IS THE GLOBAL `sym` DECLARED WITH NO VOLATILE BYTE AT `byte`? (`declared-plain`) `null` is an
|
|
352
|
+
* access that names no byte, which is plain only where no byte of the object is qualified. The map
|
|
353
|
+
* answers before the context where it lays the object out, as raise/declared-volatile.ts reads them;
|
|
354
|
+
* a struct the map names with no layout, and an object neither declares, answer no — nothing is in
|
|
355
|
+
* evidence. So does an array whose element the map does not name a base type (no `elemSigned`):
|
|
356
|
+
* the map carries the qualifiers of the array's element chain and no element layout, so klonoa's
|
|
357
|
+
* `struct EntityAnimationInfo gEntityAnimationInfo[]`, whose `frame` member is `volatile u8`,
|
|
358
|
+
* reads as an array with no volatile byte. A struct member that nests a struct holding a volatile
|
|
359
|
+
* field is still judged by the member's own qualifier: the map's member facts do not tell it from
|
|
360
|
+
* an enum, and calling every enum member unknown would stamp the merged-arm reads this gate
|
|
361
|
+
* refuses. */
|
|
362
|
+
export function declaredPlain(
|
|
363
|
+
sym: string,
|
|
364
|
+
byte: number | null,
|
|
365
|
+
info: SymbolInfo | undefined,
|
|
366
|
+
context: ContextGlobals | undefined,
|
|
367
|
+
): boolean {
|
|
368
|
+
const layout = Array.isArray(info?.layout) ? info.layout : [];
|
|
369
|
+
if (layout.length > 0) {
|
|
370
|
+
return byte === null
|
|
371
|
+
? info?.volatile !== true && !layout.some((f) => f.volatile === true)
|
|
372
|
+
: !declaresVolatile(info, byte);
|
|
373
|
+
}
|
|
374
|
+
if (context?.volatiles.has(sym) === true) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
if (info?.declared === true && info.shape !== undefined && info.shape !== 'struct') {
|
|
378
|
+
if (info.shape === 'array' && typeof info.elemSigned !== 'boolean') {
|
|
379
|
+
return info.volatile !== true && context?.declared.has(sym) === true;
|
|
380
|
+
}
|
|
381
|
+
return info.volatile !== true;
|
|
382
|
+
}
|
|
383
|
+
return context?.declared.has(sym) === true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** THE RETURN PAIR'S SECOND REGISTER AT A WORD RETURN, gathered by a frontend as it emits a `ret`
|
|
387
|
+
* of one word (`held-at-word-return`): every value `pairReg` may hold at the end of block `bi`,
|
|
388
|
+
* added to `into` — asked through `holdsValue`, which reads no register (a read of one some path
|
|
389
|
+
* leaves undefined would mint a parameter) and ends a path where a call destroyed it.
|
|
390
|
+
*
|
|
391
|
+
* Nothing is gathered where the function's own declaration pins the return: a void one, or a
|
|
392
|
+
* scalar whose width it states. A declared one-word return leaves the pair's second register
|
|
393
|
+
* unused by the source too, and a declared wider one is refused after the lift (pipeline.ts
|
|
394
|
+
* `refuseNarrowReturnsOfWideDeclaration`). */
|
|
395
|
+
export function unreadPairHalf(
|
|
396
|
+
ssa: Pick<SsaBuilder, 'holdsValue'>,
|
|
397
|
+
own: FnProto | undefined,
|
|
398
|
+
pairReg: string,
|
|
399
|
+
bi: number,
|
|
400
|
+
into: Set<Value>,
|
|
401
|
+
): void {
|
|
402
|
+
if (declaresVoidReturn(own) || ownReturnWidth(own) !== undefined) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
ssa.holdsValue(pairReg, bi, (v) => {
|
|
406
|
+
into.add(v);
|
|
407
|
+
return false; // keep walking: every path's value, not the first
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Whether a value was COMPUTED ONLY TO BE RETURNED: an op with no effect and no memory read
|
|
412
|
+
* (`spelledWhenDead` answers no) whose value nothing but a return reads, directly or through the
|
|
413
|
+
* joins that carry it there, or a join of such values. An entry parameter is the caller's value and
|
|
414
|
+
* merges nothing, so it never is one. The evidence is the compiler's, the claim `spelledWhenDead`
|
|
415
|
+
* already rests on: an optimizing compiler deletes a pure computation nobody reads, so one that
|
|
416
|
+
* survives with a return as its only reader was written for that return. A value no op reads at
|
|
417
|
+
* all answers yes too, which is what a frontend asks of the value a return home holds where the
|
|
418
|
+
* `ret` does not read it (frontend/fpu.ts `settleReturnHome`). Read over `blocks` as they stand. */
|
|
419
|
+
export function computedOnlyToReturn(blocks: readonly Block[]): (v: Value) => boolean {
|
|
420
|
+
const defOp = new Map<Value, Op>();
|
|
421
|
+
const incoming = new Map<Value, Value[]>();
|
|
422
|
+
const uses = new Map<Value, Array<Op | Value | undefined>>();
|
|
423
|
+
const push = <K, V>(m: Map<K, V[]>, k: K, v: V) => {
|
|
424
|
+
const l = m.get(k);
|
|
425
|
+
if (l) {
|
|
426
|
+
l.push(v);
|
|
427
|
+
} else {
|
|
428
|
+
m.set(k, [v]);
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
for (const blk of blocks) {
|
|
432
|
+
for (const op of blk.ops) {
|
|
433
|
+
op.results.forEach((r) => defOp.set(r, op));
|
|
434
|
+
op.operands.forEach((v) => push(uses, v, op));
|
|
435
|
+
for (const s of op.successors) {
|
|
436
|
+
s.args.forEach((v, i) => {
|
|
437
|
+
const p = s.block.params[i] as Value | undefined;
|
|
438
|
+
push(uses, v, p);
|
|
439
|
+
if (p !== undefined) {
|
|
440
|
+
push(incoming, p, v);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// A join is followed to what it merges, and every value on the way may have only returns, or
|
|
447
|
+
// joins that are themselves read only by returns, as readers. A cycle adds nothing the first
|
|
448
|
+
// visit did not judge.
|
|
449
|
+
const returnOnly = (v: Value, seen: Set<Value>): boolean => {
|
|
450
|
+
if (seen.has(v)) {
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
seen.add(v);
|
|
454
|
+
return (uses.get(v) ?? []).every(
|
|
455
|
+
(u) => u !== undefined && ('opcode' in u ? u.opcode === 'ret' : returnOnly(u, seen)),
|
|
456
|
+
);
|
|
457
|
+
};
|
|
458
|
+
const computed = (v: Value, seen: Set<Value>): boolean => {
|
|
459
|
+
if (seen.has(v)) {
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
seen.add(v);
|
|
463
|
+
if (!returnOnly(v, new Set())) {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
const op = defOp.get(v);
|
|
467
|
+
if (op !== undefined) {
|
|
468
|
+
return !spelledWhenDead(op);
|
|
469
|
+
}
|
|
470
|
+
const ins = incoming.get(v);
|
|
471
|
+
return ins !== undefined && ins.every((a) => computed(a, seen));
|
|
472
|
+
};
|
|
473
|
+
return (v) => computed(v, new Set());
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Stamp `volatile` on every read these blocks never use that no gate refuses, outermost first.
|
|
477
|
+
* `facts` is what the frontend gathered while it filled the blocks.
|
|
478
|
+
*
|
|
479
|
+
* CALLED BY EACH FRONTEND, at the end of its lift, and not once in pipeline.ts `liftStamped`: the
|
|
480
|
+
* pair, call and compare gates need what a return, a call or a compare left in a register no op
|
|
481
|
+
* reads, which only the frontend's SSA builder answers, and only while it is filling the blocks.
|
|
482
|
+
* The IR it hands back has no register left to ask. */
|
|
483
|
+
export function pinDeadReads(
|
|
484
|
+
irBlocks: readonly Block[],
|
|
485
|
+
target: TargetDescription,
|
|
486
|
+
facts: DeadReadFacts = {},
|
|
487
|
+
gates: readonly Gate<DeadRead>[] = DEAD_READ_GATES,
|
|
488
|
+
): void {
|
|
489
|
+
const defOf = new Map<Value, Op>();
|
|
490
|
+
for (const b of irBlocks) {
|
|
491
|
+
for (const op of b.ops) {
|
|
492
|
+
for (const r of op.results) {
|
|
493
|
+
defOf.set(r, op);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// An offset of a frame object: the `laddr` itself, or a sum or difference with one beside it.
|
|
498
|
+
const fromFrame = (v: Value, depth = 0): boolean => {
|
|
499
|
+
const d = defOf.get(v);
|
|
500
|
+
if (d === undefined || depth > 8) {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
if (d.opcode === 'laddr') {
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
return (d.opcode === 'add' || d.opcode === 'sub') && d.operands.some((o) => fromFrame(o, depth + 1));
|
|
507
|
+
};
|
|
508
|
+
const unmodelled = irBlocks.some((b) => b.ops.some((op) => op.opcode === 'opaque'));
|
|
509
|
+
let byName: Map<string, SymbolInfo> | undefined;
|
|
510
|
+
const plainGlobal = (op: Op): boolean => {
|
|
511
|
+
const sym = globalBaseOf(defOf, op.operands[0]);
|
|
512
|
+
if (sym === null) {
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
const symbols = facts.declarations?.symbols;
|
|
516
|
+
byName ??= symbols === undefined ? new Map() : symbolsByName(symbols);
|
|
517
|
+
const cell = op.opcode === 'load' ? globalCellOf(defOf, op.operands[0], op.attrs.off as number) : null;
|
|
518
|
+
return declaredPlain(sym, cell?.byte ?? null, byName.get(sym), facts.declarations?.context);
|
|
519
|
+
};
|
|
520
|
+
const voidReturn = declaresVoidReturn(facts.own);
|
|
521
|
+
// A branch whose two edges reach one block with the same arguments chooses nothing, so its
|
|
522
|
+
// condition is no use either: agbcc at -O0 keeps `if (*vp) {}` as `ldr; cmp; beq` to the next
|
|
523
|
+
// instruction, which the structurer would spell `*p == 0;` with the read plain. The edges are
|
|
524
|
+
// compared by DESTINATION (ir/core.ts `forwardingTarget`), not by identity: agbcc keeps
|
|
525
|
+
// `if (*vp) {} else {}` and `*vp ? 1 : 2;` as `beq .L4; b .L4`, whose fallthrough edge reaches
|
|
526
|
+
// the join through a bodyless forwarder. An edge carrying arguments is compared as it stands,
|
|
527
|
+
// since a forwarder past it would drop the values it supplies.
|
|
528
|
+
const destination = (e: Successor): Block => (e.args.length === 0 ? forwardingTarget(e.block) : e.block);
|
|
529
|
+
const choosesNothing = (op: Op): boolean =>
|
|
530
|
+
op.opcode === 'cond_br' &&
|
|
531
|
+
op.successors.length === 2 &&
|
|
532
|
+
destination(op.successors[0]) === destination(op.successors[1]) &&
|
|
533
|
+
op.successors[0].args.length === op.successors[1].args.length &&
|
|
534
|
+
op.successors[0].args.every((a, i) => a === op.successors[1].args[i]);
|
|
535
|
+
const usesOperands = (op: Op): boolean => !(voidReturn && op.opcode === 'ret') && !choosesNothing(op);
|
|
536
|
+
const isRead = (op: Op): boolean => op.opcode === 'load' || op.opcode === 'aload';
|
|
537
|
+
const decided = new Set<Op>();
|
|
538
|
+
for (;;) {
|
|
539
|
+
const dead = deadOps(irBlocks, usesOperands);
|
|
540
|
+
// The dead ops whose value reaches one of `values`, through the dead ops it feeds: an extension
|
|
541
|
+
// of the read a register holds is the read held.
|
|
542
|
+
const reaching = (values: ReadonlySet<Value> | undefined): Set<Op> => {
|
|
543
|
+
const out = new Set<Op>();
|
|
544
|
+
const walk = (v: Value): void => {
|
|
545
|
+
const d = defOf.get(v);
|
|
546
|
+
if (d !== undefined && dead.has(d) && !out.has(d)) {
|
|
547
|
+
out.add(d);
|
|
548
|
+
d.operands.forEach(walk);
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
values?.forEach(walk);
|
|
552
|
+
return out;
|
|
553
|
+
};
|
|
554
|
+
const open = [...dead].filter((op) => isRead(op) && !decided.has(op));
|
|
555
|
+
// A read that, through dead ops, computes another open read's address waits for it.
|
|
556
|
+
const feeds = new Set<Op>();
|
|
557
|
+
const feed = (v: Value): void => {
|
|
558
|
+
const d = defOf.get(v);
|
|
559
|
+
if (d !== undefined && dead.has(d) && !feeds.has(d)) {
|
|
560
|
+
feeds.add(d);
|
|
561
|
+
if (!isRead(d)) {
|
|
562
|
+
d.operands.forEach(feed);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
open.forEach((r) => r.operands.forEach(feed));
|
|
567
|
+
const ready = open.filter((r) => !feeds.has(r));
|
|
568
|
+
if (ready.length === 0) {
|
|
569
|
+
return; // nothing open, or (never in SSA) only reads that wait on each other
|
|
570
|
+
}
|
|
571
|
+
const held = reaching(facts.atWordReturn);
|
|
572
|
+
const atCall = reaching(facts.atUndeclaredCall);
|
|
573
|
+
const compared = reaching(facts.compared);
|
|
574
|
+
const spilled = reaching(facts.spilled);
|
|
575
|
+
const discarded = reaching(facts.discarded);
|
|
576
|
+
// Who consumes a value among the dead ops: an extension passes the read on, any other op the
|
|
577
|
+
// machine computed with it. A read among them was decided before this one (outermost first)
|
|
578
|
+
// and left plain, so the address it took was the compiler's use.
|
|
579
|
+
const usersOf = new Map<Value, Op[]>();
|
|
580
|
+
for (const d of dead) {
|
|
581
|
+
for (const o of d.operands) {
|
|
582
|
+
usersOf.set(o, [...(usersOf.get(o) ?? []), d]);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// What consumes the value through its extensions: an op that computes with it, and a read it
|
|
586
|
+
// is the address of.
|
|
587
|
+
const usesOf = (v: Value, out = { computed: false, addresses: false }, depth = 0) => {
|
|
588
|
+
for (const u of usersOf.get(v) ?? []) {
|
|
589
|
+
if ((u.opcode === 'zext' || u.opcode === 'sext') && depth < 8) {
|
|
590
|
+
usesOf(u.results[0], out, depth + 1);
|
|
591
|
+
} else if (isRead(u) && u.operands[0] === v) {
|
|
592
|
+
out.addresses = true;
|
|
593
|
+
} else {
|
|
594
|
+
out.computed = true;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return out;
|
|
598
|
+
};
|
|
599
|
+
for (const op of ready) {
|
|
600
|
+
decided.add(op);
|
|
601
|
+
const uses = usesOf(op.results[0]);
|
|
602
|
+
const read: DeadRead = {
|
|
603
|
+
op,
|
|
604
|
+
fromFrame: fromFrame(op.operands[0]),
|
|
605
|
+
unmodelled,
|
|
606
|
+
heldAtWordReturn: held.has(op),
|
|
607
|
+
heldAtUndeclaredCall: atCall.has(op),
|
|
608
|
+
compared: compared.has(op),
|
|
609
|
+
spilled: spilled.has(op),
|
|
610
|
+
computed: uses.computed,
|
|
611
|
+
addressesPlainRead: uses.addresses,
|
|
612
|
+
declaredPlain: plainGlobal(op),
|
|
613
|
+
discarded: discarded.has(op),
|
|
614
|
+
target,
|
|
615
|
+
};
|
|
616
|
+
const rejection = firstRejection(gates, read);
|
|
617
|
+
if (rejection === UNDECIDED) {
|
|
618
|
+
throw new FrontendUnsupportedError(
|
|
619
|
+
`cannot lift '${facts.name ?? '?'}': a read nothing uses at a level that keeps plain ones, which ${target.compiler} ` +
|
|
620
|
+
`loads into no register of its own — a volatile read and a register local are the same code here`,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
if (rejection === null) {
|
|
624
|
+
op.attrs = { ...op.attrs, volatile: true };
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|