@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,290 @@
|
|
|
1
|
+
// asmlift — A WHOLE-WORD READ OF A DECLARED STRUCT LOCAL, every use of which takes one member's
|
|
2
|
+
// bits, READS THOSE MEMBERS.
|
|
3
|
+
//
|
|
4
|
+
// agbcc has no sp-relative halfword or byte load, so `struct S4 { u16 a; u8 b; u8 pad; } s =
|
|
5
|
+
// mk(x); gOut = s.a;` reads the struct-return temp a word at a time and narrows in registers:
|
|
6
|
+
//
|
|
7
|
+
// mov r0, sp / bl mk / ldr r1, [sp] / ldr r0, =gOut / strh r1, [r0]
|
|
8
|
+
//
|
|
9
|
+
// The frame-object audit types the temp as the struct the call returns (frame-objects.ts, the
|
|
10
|
+
// `aggregate` stamp), so the word is bytes of a declared object, and each use of it names a byte
|
|
11
|
+
// range: a narrowing store or extension (the low end), or one of those applied to a right shift
|
|
12
|
+
// by whole bytes, or such a shift alone (the top end, whose zero fill is the member's own zero
|
|
13
|
+
// extension). Where every use names exactly one declared integer member, the word is those
|
|
14
|
+
// member reads — the same bytes, read at the same program point: each member load is placed where
|
|
15
|
+
// the word load was, and the word load is left to dce.
|
|
16
|
+
//
|
|
17
|
+
// REFUSALS, each leaving the word read whole (spelled through a cast, which is the same bytes):
|
|
18
|
+
// * the base is not a frame object the audit typed as a struct, or the read is qualified;
|
|
19
|
+
// * a use is anything but the shapes above (an add, a compare of the whole word, a phi
|
|
20
|
+
// argument, a call argument), or a shift whose uses are not all of one width;
|
|
21
|
+
// * a range names no declared member exactly (a straddle, padding, a bitfield, a nested
|
|
22
|
+
// aggregate) — the member must start at that byte and be an integer of that width.
|
|
23
|
+
import { type Block, type Fn, type Op, type Value, mkOp, mkValue } from '../ir/core';
|
|
24
|
+
import { qualified } from '../ir/discipline';
|
|
25
|
+
import { type IrType, T } from '../ir/types';
|
|
26
|
+
|
|
27
|
+
type StructType = Extract<IrType, { kind: 'struct' }>;
|
|
28
|
+
|
|
29
|
+
/** The struct a frame object's address points at, where the audit stamped one. */
|
|
30
|
+
function frameStructOf(defs: ReadonlyMap<Value, Op>, base: Value): StructType | undefined {
|
|
31
|
+
const d = defs.get(base);
|
|
32
|
+
const t = d?.results[0].type;
|
|
33
|
+
return d?.opcode === 'laddr' && d.attrs.aggregate === true && t?.kind === 'ptr' && t.to.kind === 'struct'
|
|
34
|
+
? t.to
|
|
35
|
+
: undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The declared integer member at exactly `[byte, byte + width)`: its signedness, or undefined. */
|
|
39
|
+
function memberAt(st: StructType, byte: number, width: number): boolean | undefined {
|
|
40
|
+
const f = st.fields.find((m) => m.off === byte);
|
|
41
|
+
return f !== undefined && f.type.kind === 'int' && f.type.width === width * 8 ? f.type.signed : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Use {
|
|
45
|
+
readonly op: Op;
|
|
46
|
+
readonly idx: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The bytes a use keeps of the value it reads, low end first: a narrowing store's or an
|
|
50
|
+
* extension's width; undefined for any other use. */
|
|
51
|
+
function narrowedTo(u: Use): number | undefined {
|
|
52
|
+
if (u.op.opcode === 'store' && u.idx === 1) {
|
|
53
|
+
return u.op.attrs.width as number;
|
|
54
|
+
}
|
|
55
|
+
return u.op.opcode === 'zext' || u.op.opcode === 'sext' ? (u.op.attrs.width as number) / 8 : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** One member a word's uses take: its byte range in the word, and each op it is read for with the
|
|
59
|
+
* rewiring of that op onto the member's load `m` (returning an op to place after the load). */
|
|
60
|
+
interface MemberUse {
|
|
61
|
+
readonly byte: number;
|
|
62
|
+
readonly width: number;
|
|
63
|
+
readonly sites: readonly { readonly op: Op; readonly rewire: (m: Value, signed: boolean) => Op | undefined }[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** WHERE A MEMBER IS READ. The word was read once, where the compiler CSE'd the member reads of
|
|
67
|
+
* the source into it; the source read each member where it is used. So a member read for an op
|
|
68
|
+
* in another block is placed just before that op — where no write to the object lies on any path
|
|
69
|
+
* from the word's load to it, which is the condition under which the two read the same bytes.
|
|
70
|
+
* A write is any op but a load that takes the object's address (its struct-return call). */
|
|
71
|
+
function readableAt(
|
|
72
|
+
load: Op,
|
|
73
|
+
site: Op,
|
|
74
|
+
{ blockOf, preds, writes }: { blockOf: Map<Op, Block>; preds: Map<Block, Block[]>; writes: (op: Op) => boolean },
|
|
75
|
+
): boolean {
|
|
76
|
+
const seen = new Set<Block>();
|
|
77
|
+
const work: [Block, number][] = [[blockOf.get(site)!, blockOf.get(site)!.ops.indexOf(site)]];
|
|
78
|
+
while (work.length > 0) {
|
|
79
|
+
const [b, end] = work.pop()!;
|
|
80
|
+
let reached = false;
|
|
81
|
+
for (let i = end - 1; i >= 0; i--) {
|
|
82
|
+
if (b.ops[i] === load) {
|
|
83
|
+
reached = true;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
if (writes(b.ops[i])) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (reached) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const ps = preds.get(b) ?? [];
|
|
94
|
+
if (ps.length === 0) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
for (const p of ps) {
|
|
98
|
+
if (!seen.has(p)) {
|
|
99
|
+
seen.add(p);
|
|
100
|
+
work.push([p, p.ops.length]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function readDeclaredMembers(fn: Fn, littleEndian: boolean): number {
|
|
108
|
+
const defs = new Map<Value, Op>();
|
|
109
|
+
const uses = new Map<Value, Use[]>();
|
|
110
|
+
const branchArgs = new Set<Value>();
|
|
111
|
+
const blockOf = new Map<Op, Block>();
|
|
112
|
+
const preds = new Map<Block, Block[]>();
|
|
113
|
+
for (const b of fn.blocks) {
|
|
114
|
+
for (const op of b.ops) {
|
|
115
|
+
blockOf.set(op, b);
|
|
116
|
+
op.results.forEach((r) => defs.set(r, op));
|
|
117
|
+
op.operands.forEach((v, idx) => (uses.get(v) ?? uses.set(v, []).get(v)!).push({ op, idx }));
|
|
118
|
+
op.successors.forEach((s) => {
|
|
119
|
+
s.args.forEach((a) => branchArgs.add(a));
|
|
120
|
+
(preds.get(s.block) ?? preds.set(s.block, []).get(s.block)!).push(b);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// every read of `from`, an edge's argument included
|
|
125
|
+
const replaceAll = (from: Value, to: Value): void => {
|
|
126
|
+
for (const { op, idx } of uses.get(from) ?? []) {
|
|
127
|
+
op.operands[idx] = to;
|
|
128
|
+
}
|
|
129
|
+
if (branchArgs.has(from)) {
|
|
130
|
+
for (const b of fn.blocks) {
|
|
131
|
+
for (const op of b.ops) {
|
|
132
|
+
op.successors.forEach((sc) => (sc.args = sc.args.map((a) => (a === from ? to : a))));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
// a narrowing use of `m`: a store takes the member's low bytes, an extension of the member's
|
|
138
|
+
// own kind is the member read, another extension extends the member
|
|
139
|
+
const narrowOnto = (u: Use, m: Value, signed: boolean): undefined => {
|
|
140
|
+
if (u.op.opcode === 'store') {
|
|
141
|
+
u.op.operands[1] = m;
|
|
142
|
+
} else if (signed === (u.op.opcode === 'sext')) {
|
|
143
|
+
replaceAll(u.op.results[0], m);
|
|
144
|
+
} else {
|
|
145
|
+
u.op.operands[0] = m;
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
};
|
|
149
|
+
const narrowSite = (u: Use) => ({ op: u.op, rewire: (m: Value, signed: boolean) => narrowOnto(u, m, signed) });
|
|
150
|
+
// a shift that leaves exactly a member's bits, extended its own way: the member, or the member
|
|
151
|
+
// extended that way where it is declared the other
|
|
152
|
+
const extractSite = (sh: Op, width: number) => ({
|
|
153
|
+
op: sh,
|
|
154
|
+
rewire: (m: Value, signed: boolean): Op | undefined => {
|
|
155
|
+
const want = sh.opcode === 'shr_s';
|
|
156
|
+
if (signed === want) {
|
|
157
|
+
replaceAll(sh.results[0], m);
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const x = mkOp(want ? 'sext' : 'zext', {
|
|
161
|
+
operands: [m],
|
|
162
|
+
results: [mkValue(T.unk(32))],
|
|
163
|
+
attrs: { width: width * 8 },
|
|
164
|
+
});
|
|
165
|
+
replaceAll(sh.results[0], x.results[0]);
|
|
166
|
+
return x;
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
const isShr = (op: Op): boolean => (op.opcode === 'shr_u' || op.opcode === 'shr_s') && op.operands.length === 1;
|
|
170
|
+
const frameOff = (v: Value): number | undefined => {
|
|
171
|
+
const d = defs.get(v);
|
|
172
|
+
return d?.opcode === 'laddr' ? (d.attrs.off as number) : undefined;
|
|
173
|
+
};
|
|
174
|
+
let changed = 0;
|
|
175
|
+
for (const blk of fn.blocks) {
|
|
176
|
+
for (let i = 0; i < blk.ops.length; i++) {
|
|
177
|
+
const load = blk.ops[i];
|
|
178
|
+
if (load.opcode !== 'load' || qualified(load)) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const st = frameStructOf(defs, load.operands[0]);
|
|
182
|
+
const word = load.attrs.width as number;
|
|
183
|
+
const at0 = load.attrs.off as number;
|
|
184
|
+
const v = load.results[0];
|
|
185
|
+
if (st === undefined || branchArgs.has(v)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// the byte a `width`-byte range `shift` bits up the word starts at
|
|
189
|
+
const byteOf = (shift: number, width: number): number => (littleEndian ? shift / 8 : word - shift / 8 - width);
|
|
190
|
+
const plan: MemberUse[] = [];
|
|
191
|
+
let ok = (uses.get(v) ?? []).length > 0;
|
|
192
|
+
for (const u of uses.get(v) ?? []) {
|
|
193
|
+
const width = narrowedTo(u);
|
|
194
|
+
if (width !== undefined && width < word) {
|
|
195
|
+
plan.push({ byte: byteOf(0, width), width, sites: [narrowSite(u)] });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// `(x << a) >> b`: bits [b - a, 32 - a) of the word, the middle members
|
|
199
|
+
if (u.op.opcode === 'shl' && u.op.operands.length === 1 && !branchArgs.has(u.op.results[0])) {
|
|
200
|
+
const a = u.op.attrs.imm as number;
|
|
201
|
+
const shrs = uses.get(u.op.results[0])?.map((x) => x.op) ?? [];
|
|
202
|
+
const b = shrs[0]?.attrs.imm as number | undefined;
|
|
203
|
+
const width = b === undefined ? 0 : (32 - b) / 8;
|
|
204
|
+
if (
|
|
205
|
+
b !== undefined &&
|
|
206
|
+
shrs.every((x) => isShr(x) && x.attrs.imm === b && !branchArgs.has(x.results[0])) &&
|
|
207
|
+
b >= a &&
|
|
208
|
+
(b - a) % 8 === 0 &&
|
|
209
|
+
(width === 1 || width === 2) &&
|
|
210
|
+
32 - a <= word * 8
|
|
211
|
+
) {
|
|
212
|
+
plan.push({ byte: byteOf(b - a, width), width, sites: shrs.map((x) => extractSite(x, width)) });
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
ok = false;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
const shift = isShr(u.op) ? (u.op.attrs.imm as number) : 0;
|
|
219
|
+
if (shift <= 0 || shift % 8 !== 0 || shift >= word * 8 || branchArgs.has(u.op.results[0])) {
|
|
220
|
+
ok = false;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
const top = word - shift / 8;
|
|
224
|
+
const shUses = uses.get(u.op.results[0]) ?? [];
|
|
225
|
+
const widths = new Set(shUses.map(narrowedTo));
|
|
226
|
+
const narrowed = [...widths][0];
|
|
227
|
+
if (
|
|
228
|
+
u.op.opcode === 'shr_u' &&
|
|
229
|
+
shUses.length > 0 &&
|
|
230
|
+
widths.size === 1 &&
|
|
231
|
+
narrowed !== undefined &&
|
|
232
|
+
narrowed < top
|
|
233
|
+
) {
|
|
234
|
+
plan.push({ byte: byteOf(shift, narrowed), width: narrowed, sites: shUses.map(narrowSite) });
|
|
235
|
+
} else if (word === 4 && (top === 1 || top === 2)) {
|
|
236
|
+
// the shift's fill is the top member's own extension — of a whole word, whose bits above the
|
|
237
|
+
// member are the shift's alone, not a narrower load's extension
|
|
238
|
+
plan.push({ byte: byteOf(shift, top), width: top, sites: [extractSite(u.op, top)] });
|
|
239
|
+
} else {
|
|
240
|
+
ok = false;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const signs = plan.map((p) => memberAt(st, at0 + p.byte, p.width));
|
|
245
|
+
if (!ok || signs.some((s) => s === undefined)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const obj = frameOff(load.operands[0]);
|
|
249
|
+
const writes = (op: Op): boolean => op.opcode !== 'load' && op.operands.some((o) => frameOff(o) === obj);
|
|
250
|
+
const objectIsBranchArg = [...branchArgs].some((a) => frameOff(a) === obj);
|
|
251
|
+
const mkLoad = (p: MemberUse, signed: boolean): Op =>
|
|
252
|
+
mkOp('load', {
|
|
253
|
+
operands: [load.operands[0]],
|
|
254
|
+
results: [mkValue(T.unk(32))],
|
|
255
|
+
attrs: { off: at0 + p.byte, width: p.width, signed },
|
|
256
|
+
});
|
|
257
|
+
// a member read for an op of the word's own block shares one load where the word was read
|
|
258
|
+
const shared = new Map<string, Op>();
|
|
259
|
+
const after: Op[] = [];
|
|
260
|
+
const sunk: [Op, Op[]][] = [];
|
|
261
|
+
plan.forEach((p, k) => {
|
|
262
|
+
const signed = signs[k]!;
|
|
263
|
+
for (const site of p.sites) {
|
|
264
|
+
const elsewhere = blockOf.get(site.op) !== blk;
|
|
265
|
+
if (elsewhere && !objectIsBranchArg && readableAt(load, site.op, { blockOf, preds, writes })) {
|
|
266
|
+
const m = mkLoad(p, signed);
|
|
267
|
+
const extra = site.rewire(m.results[0], signed);
|
|
268
|
+
sunk.push([site.op, extra === undefined ? [m] : [m, extra]]);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const key = `${p.byte}:${p.width}`;
|
|
272
|
+
const m = shared.get(key) ?? shared.set(key, mkLoad(p, signed)).get(key)!;
|
|
273
|
+
const extra = site.rewire(m.results[0], signed);
|
|
274
|
+
if (extra !== undefined) {
|
|
275
|
+
after.push(extra);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
for (const [site, ops] of sunk) {
|
|
280
|
+
const b = blockOf.get(site)!;
|
|
281
|
+
b.ops.splice(b.ops.indexOf(site), 0, ...ops);
|
|
282
|
+
ops.forEach((o) => blockOf.set(o, b));
|
|
283
|
+
}
|
|
284
|
+
blk.ops.splice(blk.ops.indexOf(load) + 1, 0, ...shared.values(), ...after);
|
|
285
|
+
i = blk.ops.indexOf(load);
|
|
286
|
+
changed++;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return changed;
|
|
290
|
+
}
|
package/src/raise/arrays.ts
CHANGED
|
@@ -78,7 +78,11 @@ export function recognizeArrays(fn: Fn): number {
|
|
|
78
78
|
}
|
|
79
79
|
b.ops[i] = mkOp('astore', {
|
|
80
80
|
operands: [m.base, m.index, op.operands[1]],
|
|
81
|
-
|
|
81
|
+
// the signedness raise/storesign.ts read off a stored constant, when it read one
|
|
82
|
+
attrs: carryDiscipline(op, {
|
|
83
|
+
elemSize: op.attrs.width as number,
|
|
84
|
+
...(op.attrs.signed !== undefined && { signed: op.attrs.signed }),
|
|
85
|
+
}),
|
|
82
86
|
});
|
|
83
87
|
count++;
|
|
84
88
|
}
|
|
@@ -1,32 +1,53 @@
|
|
|
1
1
|
// asmlift — the decider for ir/discipline.ts's `declared` placement: a memory access of an object
|
|
2
|
-
// the project's symbol map declares volatile.
|
|
2
|
+
// the project's symbol map, or its declaration context, declares volatile.
|
|
3
3
|
//
|
|
4
|
-
// The stamp is a function of the IR, the map the function is lifted and structured under,
|
|
5
|
-
// compiler, and nothing else: an access whose base reaches a
|
|
6
|
-
// the access names (`declaresVolatile`)
|
|
7
|
-
//
|
|
4
|
+
// The stamp is a function of the IR, the map the function is lifted and structured under, the
|
|
5
|
+
// declaration context's globals and the compiler, and nothing else: an access whose base reaches a
|
|
6
|
+
// global the map qualifies, at the byte the access names (`declaresVolatile`), or one the context
|
|
7
|
+
// declares with a volatile byte and the map does not lay out (`ContextGlobals.volatiles`, per
|
|
8
|
+
// object, not per byte), or one through the value of a pointer the context declares to point at a
|
|
9
|
+
// volatile (`ContextGlobals.pointeeVolatiles`) — and, for a read, one the compiler could have made
|
|
10
|
+
// of a `volatile` (target.ts `readCouldBeVolatile`, which the device pin asks too). It is put as the
|
|
8
11
|
// lift is made (pipeline.ts `liftStamped`), before the first pass that asks whether a read may be
|
|
9
12
|
// deleted, folded or moved, and re-derived by every pass that changes which object an access names
|
|
10
13
|
// and by structuring. Structuring the same function under another map — the `/raw-globals` setting
|
|
11
14
|
// has none — re-runs it, and an op the new map does not qualify loses the stamp, so a structuring
|
|
12
|
-
// never answers for a map it was not given. The
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
15
|
+
// never answers for a map it was not given. The context is not the map: every candidate compiles
|
|
16
|
+
// against it whichever names it spells, so it is read in both settings.
|
|
17
|
+
//
|
|
18
|
+
// The base is read as the structurer will read it: a join whose every edge passes one address is
|
|
19
|
+
// that address (`addressThroughJoins`), and one every edge of which reaches one object — two of its
|
|
20
|
+
// addresses, or a pointer a loop steps through it — is an access of that object, as one every edge
|
|
21
|
+
// of which moves a pointer global's value is an access of its pointee (`reachThroughJoins`, one
|
|
22
|
+
// reader for both). A join whose edges reach different globals (`c ? gP : &gV`) is an access of any
|
|
23
|
+
// of them, placed only where every one is declared. A base with an edge that reaches no name — a
|
|
24
|
+
// pointer parameter, a raw address — has no declaration in evidence, and the access stays
|
|
25
|
+
// unplaced unless the lift pinned it (`device`).
|
|
16
26
|
//
|
|
17
27
|
// A read structuring stamps that the earlier stamps left plain was plain to every raising pass, and a
|
|
18
28
|
// short-circuit fold may have lifted it out of the arm it ran in. structure.ts declines on such a
|
|
19
29
|
// read in a `&&`/`||`'s guarded operand rather than place it.
|
|
20
|
-
import {
|
|
30
|
+
import { globalCellOf } from '../ir/alias';
|
|
21
31
|
import { type Fn, type Op, type Value, defOpMap } from '../ir/core';
|
|
22
32
|
import { DECLARED_VOLATILE } from '../ir/discipline';
|
|
23
33
|
import { MEM_BASE_OPS } from '../ir/opcodes';
|
|
34
|
+
import type { ContextGlobals } from '../proto-context';
|
|
24
35
|
import { type SymbolInfo, type SymbolMap, declaresVolatile, symbolsByName } from '../symbols';
|
|
25
36
|
import { type TargetDescription, readCouldBeVolatile } from '../target';
|
|
26
37
|
import { valueNumberOf } from './gvn';
|
|
27
38
|
|
|
28
|
-
/** The compiler fact the stamp reads.
|
|
29
|
-
|
|
39
|
+
/** The compiler fact the stamp reads, and the declaration context's globals when there is one.
|
|
40
|
+
* The context is a REQUIRED key, `undefined` when there is none: the stamp clears every stamp it
|
|
41
|
+
* does not re-derive, so a re-stamp handed the compiler behaviors alone would unplace every read
|
|
42
|
+
* the context made volatile. Build one with pipeline.ts `stampBehaviorsFor`. */
|
|
43
|
+
export type StampBehaviors = Pick<TargetDescription['compilerBehaviors'], 'volatileReadsExtendInRegister'> & {
|
|
44
|
+
readonly contextGlobals: ContextGlobals | undefined;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Does the map lay out this object's volatility itself? A layout answers per member, which is
|
|
48
|
+
* finer than the context's per-object fact; an entry with none (a name and a size off the symbol
|
|
49
|
+
* table) leaves the context's answer standing. */
|
|
50
|
+
const laysOut = (info: SymbolInfo | undefined): boolean => Array.isArray(info?.layout) && info.layout.length > 0;
|
|
30
51
|
|
|
31
52
|
/** May this memory access be stamped: a store, or a read the compiler could have made of a
|
|
32
53
|
* `volatile`? */
|
|
@@ -41,6 +62,29 @@ const stampable = (op: Op, behaviors: StampBehaviors): boolean =>
|
|
|
41
62
|
* is such a parameter, and the stamp is put before that pass, so it reads the base the structurer
|
|
42
63
|
* will. */
|
|
43
64
|
function addressThroughJoins(fn: Fn): (v: Value) => Value {
|
|
65
|
+
const incoming = joinIncoming(fn);
|
|
66
|
+
const numberOf = valueNumberOf(fn);
|
|
67
|
+
const through = (v: Value, onWalk: Set<Value>): Value | null => {
|
|
68
|
+
const ins = incoming.get(v);
|
|
69
|
+
if (ins === undefined) {
|
|
70
|
+
return v;
|
|
71
|
+
}
|
|
72
|
+
if (onWalk.has(v)) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
onWalk.add(v);
|
|
76
|
+
const each = ins.filter((a) => a !== v).map((a) => through(a, onWalk));
|
|
77
|
+
onWalk.delete(v);
|
|
78
|
+
const first = each[0];
|
|
79
|
+
return first !== undefined && first !== null && each.every((a) => a !== null && numberOf(a) === numberOf(first))
|
|
80
|
+
? first
|
|
81
|
+
: null;
|
|
82
|
+
};
|
|
83
|
+
return (v) => through(v, new Set()) ?? v;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Every block parameter's incoming values, one per in-edge (the entry block's excepted). */
|
|
87
|
+
function joinIncoming(fn: Fn): Map<Value, Value[]> {
|
|
44
88
|
const incoming = new Map<Value, Value[]>();
|
|
45
89
|
for (const b of fn.blocks) {
|
|
46
90
|
for (const op of b.ops) {
|
|
@@ -56,48 +100,180 @@ function addressThroughJoins(fn: Fn): (v: Value) => Value {
|
|
|
56
100
|
}
|
|
57
101
|
}
|
|
58
102
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
103
|
+
return incoming;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** What an address reaches: an access of a global OBJECT (`gW.b`, `gVA[i]`), or of what the pointer a
|
|
107
|
+
* global holds points at (`*gP`, `gQ->b`) — the POINTEE of that global. */
|
|
108
|
+
type Reach = { readonly kind: 'object' | 'pointee'; readonly sym: string };
|
|
109
|
+
|
|
110
|
+
/** The globals an address reaches, one per edge that reaches a different one, each as an object or
|
|
111
|
+
* as a pointer's pointee, read through address arithmetic and joins alike: a `gaddr` is its object;
|
|
112
|
+
* a word read from a global's own first byte is that global's value, so an address moved from it
|
|
113
|
+
* reaches the global's pointee (asked only of a global in `pointers`, the ones the context declares
|
|
114
|
+
* to point at a volatile: any other word read is an index, not a base) — and so is a word read from
|
|
115
|
+
* a join every edge of which is such a global's address; an `add` with one reaching operand and a
|
|
116
|
+
* `sub` from one reach what they do; and a block parameter reaches what each in-edge's value
|
|
117
|
+
* reaches, an edge that passes the parameter itself moved by an offset (a strength-reduced loop
|
|
118
|
+
* pointer `v0 = v0 + 2`) aside. So `c ? &gW.a : &gW.b` and a pointer seeded with `&gVA` and stepped
|
|
119
|
+
* through it are accesses of that one object, `c ? gP + 1 : gP + 2` and a pointer seeded with `gP`
|
|
120
|
+
* and stepped are accesses of gP's pointee, and `c ? gP : gQ` or `c ? gP : &gV` is an access of
|
|
121
|
+
* either, which the caller places only where every one is declared. ONE reader answers all of
|
|
122
|
+
* them, so a shape the object path reads the pointee path reads too. Null where an edge reaches
|
|
123
|
+
* none. `addressThroughJoins` — one value on every edge — is narrower. */
|
|
124
|
+
function reachThroughJoins(
|
|
125
|
+
fn: Fn,
|
|
126
|
+
defs: Map<Value, Op>,
|
|
127
|
+
pointers: ReadonlySet<string> | undefined,
|
|
128
|
+
): (v: Value) => readonly Reach[] | null {
|
|
129
|
+
const incoming = joinIncoming(fn);
|
|
130
|
+
const SELF = Symbol('self');
|
|
131
|
+
type Step = readonly Reach[] | typeof SELF | null;
|
|
132
|
+
const union = (steps: readonly (readonly Reach[])[]): Reach[] => {
|
|
133
|
+
const out: Reach[] = [];
|
|
134
|
+
for (const r of steps.flat()) {
|
|
135
|
+
if (!out.some((o) => o.kind === r.kind && o.sym === r.sym)) {
|
|
136
|
+
out.push(r);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
};
|
|
141
|
+
/** The globals whose own address every edge of `v` passes, unmoved; null where one passes anything else. */
|
|
142
|
+
// A value's answer is the same from every walk that never cut a cycle below it, so those are
|
|
143
|
+
// remembered: without that, a chain of diamonds whose arms both step one pointer is walked once
|
|
144
|
+
// per path, 2^n times, on every stamp. An answer that cut a cycle depends on the walk and is not.
|
|
145
|
+
let cuts = 0;
|
|
146
|
+
const addressMemo = new Map<Value, string[] | null>();
|
|
147
|
+
const reachMemo = new Map<Value, Step>();
|
|
148
|
+
const addresses = (v: Value, onWalk: Set<Value>): string[] | null => {
|
|
149
|
+
const d = defs.get(v);
|
|
150
|
+
if (d?.opcode === 'gaddr') {
|
|
151
|
+
return [d.attrs.sym as string];
|
|
152
|
+
}
|
|
153
|
+
const ins = d === undefined ? incoming.get(v) : undefined;
|
|
62
154
|
if (ins === undefined) {
|
|
63
|
-
return
|
|
155
|
+
return null;
|
|
64
156
|
}
|
|
65
157
|
if (onWalk.has(v)) {
|
|
158
|
+
cuts++;
|
|
66
159
|
return null;
|
|
67
160
|
}
|
|
161
|
+
if (addressMemo.has(v)) {
|
|
162
|
+
return addressMemo.get(v)!;
|
|
163
|
+
}
|
|
164
|
+
const before = cuts;
|
|
68
165
|
onWalk.add(v);
|
|
69
|
-
const each = ins.filter((a) => a !== v).map((a) =>
|
|
166
|
+
const each = ins.filter((a) => a !== v).map((a) => addresses(a, onWalk));
|
|
70
167
|
onWalk.delete(v);
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
168
|
+
const out = each.length > 0 && each.every((a) => a !== null) ? [...new Set(each.flat())] : null;
|
|
169
|
+
if (cuts === before) {
|
|
170
|
+
addressMemo.set(v, out);
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
};
|
|
174
|
+
const reach = (v: Value, onWalk: Set<Value>): Step => {
|
|
175
|
+
if (reachMemo.has(v)) {
|
|
176
|
+
return reachMemo.get(v)!;
|
|
177
|
+
}
|
|
178
|
+
const before = cuts;
|
|
179
|
+
const out = reachOnce(v, onWalk);
|
|
180
|
+
if (cuts === before) {
|
|
181
|
+
reachMemo.set(v, out);
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
};
|
|
185
|
+
const reachOnce = (v: Value, onWalk: Set<Value>): Step => {
|
|
186
|
+
const d = defs.get(v);
|
|
187
|
+
if (d?.opcode === 'gaddr') {
|
|
188
|
+
return [{ kind: 'object', sym: d.attrs.sym as string }];
|
|
189
|
+
}
|
|
190
|
+
if (d?.opcode === 'load' && ((d.attrs.off as number | undefined) ?? 0) === 0) {
|
|
191
|
+
const syms = addresses(d.operands[0], new Set());
|
|
192
|
+
return syms !== null && syms.every((sym) => pointers?.has(sym) === true)
|
|
193
|
+
? syms.map((sym) => ({ kind: 'pointee', sym }))
|
|
194
|
+
: null;
|
|
195
|
+
}
|
|
196
|
+
if (d?.opcode === 'add' && d.operands.length === 2) {
|
|
197
|
+
const a = reach(d.operands[0], onWalk);
|
|
198
|
+
const b = reach(d.operands[1], onWalk);
|
|
199
|
+
if (a !== null && b !== null) {
|
|
200
|
+
// An object's address plus a pointer's value is an index into the object (a byte array
|
|
201
|
+
// subscripted by a global the context also declares a pointer, or by a join of two such
|
|
202
|
+
// values); two of a kind name none, and neither does a side that mixes the kinds.
|
|
203
|
+
if (a === SELF || b === SELF) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const all = (r: readonly Reach[], kind: Reach['kind']): boolean => r.every((x) => x.kind === kind);
|
|
207
|
+
if (all(a, 'object') && all(b, 'pointee')) {
|
|
208
|
+
return a;
|
|
209
|
+
}
|
|
210
|
+
return all(b, 'object') && all(a, 'pointee') ? b : null;
|
|
211
|
+
}
|
|
212
|
+
return a ?? b;
|
|
213
|
+
}
|
|
214
|
+
if (d?.opcode === 'sub' && d.operands.length === 2) {
|
|
215
|
+
return reach(d.operands[0], onWalk);
|
|
216
|
+
}
|
|
217
|
+
const ins = d === undefined ? incoming.get(v) : undefined;
|
|
218
|
+
if (ins === undefined) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
if (onWalk.has(v)) {
|
|
222
|
+
cuts++;
|
|
223
|
+
return SELF;
|
|
224
|
+
}
|
|
225
|
+
onWalk.add(v);
|
|
226
|
+
const each = ins.map((a) => reach(a, onWalk)).filter((r) => r !== SELF);
|
|
227
|
+
onWalk.delete(v);
|
|
228
|
+
return each.length > 0 && each.every((r) => r !== null) ? union(each as (readonly Reach[])[]) : null;
|
|
229
|
+
};
|
|
230
|
+
return (v) => {
|
|
231
|
+
const r = reach(v, new Set());
|
|
232
|
+
return r === null || r === SELF ? null : r;
|
|
75
233
|
};
|
|
76
|
-
return (v) => through(v, new Set()) ?? v;
|
|
77
234
|
}
|
|
78
235
|
|
|
79
|
-
/** Stamp every memory access of `fn` to an object `symbols`
|
|
80
|
-
* compiler behaving as `behaviors` says could have made it of one —
|
|
81
|
-
* other op. Returns the ops it stamped that were not stamped before. */
|
|
236
|
+
/** Stamp every memory access of `fn` to an object `symbols` or `behaviors.contextGlobals` declares
|
|
237
|
+
* volatile — a read only where a compiler behaving as `behaviors` says could have made it of one —
|
|
238
|
+
* and clear the stamp from every other op. Returns the ops it stamped that were not stamped before. */
|
|
82
239
|
export function stampDeclaredVolatile(
|
|
83
240
|
fn: Fn,
|
|
84
241
|
symbols: ReadonlyMap<string, SymbolInfo> | undefined,
|
|
85
242
|
behaviors: StampBehaviors,
|
|
86
243
|
): Op[] {
|
|
87
244
|
const added: Op[] = [];
|
|
88
|
-
const
|
|
89
|
-
const
|
|
245
|
+
const contextVolatiles = behaviors.contextGlobals?.volatiles;
|
|
246
|
+
const pointees = behaviors.contextGlobals?.pointeeVolatiles;
|
|
247
|
+
const reads =
|
|
248
|
+
symbols !== undefined ||
|
|
249
|
+
(contextVolatiles !== undefined && contextVolatiles.size > 0) ||
|
|
250
|
+
(pointees !== undefined && pointees.size > 0);
|
|
251
|
+
const defs = reads ? defOpMap(fn) : undefined;
|
|
252
|
+
const addressOf = reads ? addressThroughJoins(fn) : undefined;
|
|
253
|
+
const reachOf = reads ? reachThroughJoins(fn, defs!, pointees) : undefined;
|
|
90
254
|
for (const b of fn.blocks) {
|
|
91
255
|
for (const op of b.ops) {
|
|
92
256
|
const addr = defs && stampable(op, behaviors) ? addressOf!(op.operands[0]) : undefined;
|
|
93
|
-
const
|
|
257
|
+
const reached = addr !== undefined ? reachOf!(addr) : null;
|
|
94
258
|
// A subscript reaches the object while naming no cell, so only a `load`/`store` asks by byte,
|
|
95
|
-
// and an access that names no byte is placed only where every byte it could reach is
|
|
259
|
+
// and an access that names no byte is placed only where every byte it could reach is
|
|
260
|
+
// qualified. An access that may reach any of several globals is placed only where each one
|
|
261
|
+
// declares it.
|
|
96
262
|
const cell =
|
|
97
|
-
|
|
263
|
+
reached?.length === 1 && reached[0].kind === 'object' && (op.opcode === 'load' || op.opcode === 'store')
|
|
98
264
|
? globalCellOf(defs!, addr!, op.attrs.off as number)
|
|
99
265
|
: null;
|
|
100
|
-
const
|
|
266
|
+
const declares = (r: Reach): boolean => {
|
|
267
|
+
if (r.kind === 'pointee') {
|
|
268
|
+
return pointees?.has(r.sym) === true;
|
|
269
|
+
}
|
|
270
|
+
const info = symbols?.get(r.sym);
|
|
271
|
+
return (
|
|
272
|
+
declaresVolatile(info, cell === null ? null : cell.byte) ||
|
|
273
|
+
(contextVolatiles?.has(r.sym) === true && !laysOut(info))
|
|
274
|
+
);
|
|
275
|
+
};
|
|
276
|
+
const declared = reached !== null && reached.every(declares);
|
|
101
277
|
// A fresh record either way: a rebuilder may hand two ops one `attrs` object.
|
|
102
278
|
if (declared && op.attrs[DECLARED_VOLATILE] !== true) {
|
|
103
279
|
op.attrs = { ...op.attrs, [DECLARED_VOLATILE]: true };
|