@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,382 @@
|
|
|
1
|
+
// asmlift — a local aggregate's constant INITIALIZER, recovered from the copy that fills it.
|
|
2
|
+
//
|
|
3
|
+
// `u8 a[16] = { 0, 1, 2, … };` is not a statement in the machine. A compiler that keeps the
|
|
4
|
+
// aggregate in memory places the initial image as a constant of its own, under a label the source
|
|
5
|
+
// never wrote, and copies it into the frame at entry: agbcc 2.9 writes `.LC0:` in .rodata and
|
|
6
|
+
// `ldr r1,=.LC0; mov r0,sp; mov r2,#16; bl memcpy` (varasm.c `output_constant_def`, expr.c
|
|
7
|
+
// `store_constructor` → `emit_block_move`). The lift reads that image as an ANONYMOUS object
|
|
8
|
+
// (ir/core.ts `LocalObject.anonymous`), so the tree holds the copy as written —
|
|
9
|
+
// `memcpy(sp0, .LC0, 16)` — and the image in `SFn.images`, which no declaration spells. This pass
|
|
10
|
+
// spells the pair back as the declaration it came from: the local's initializer, and no copy.
|
|
11
|
+
//
|
|
12
|
+
// IT RUNS AT EMISSION, NOT IN STRUCTURE. Between the two the tree belongs to the contracts and the
|
|
13
|
+
// respell variations, and every one of them is right about the copy as it stands: it is a call the
|
|
14
|
+
// asm makes (contracts.ts `assertEffectsPreserved` counts it), and it takes the local's address,
|
|
15
|
+
// which is what keeps dead-store elimination and the coalescers off the object. A folded tree
|
|
16
|
+
// would hold a local nothing mentions, which l3/dce.ts deletes — and with it the initializer the
|
|
17
|
+
// compiler keeps (agbcc copies `u8 a[16] = {…}` even when nothing reads `a`). So structure()
|
|
18
|
+
// asks this pass whether the fold is possible and refuses the function loudly where it is not,
|
|
19
|
+
// and the C-family emitter folds (backend/cfamily.ts), the same seam l3/slotorder.ts uses.
|
|
20
|
+
//
|
|
21
|
+
// THE FOLD IS SOUND exactly when the declaration's initializer and the copy are one program:
|
|
22
|
+
// • the copy is a block copy the C standard fixes (proto.ts `blockCopyRoles`), a TOP-LEVEL
|
|
23
|
+
// statement of the body, so it runs once and on every path that reaches it;
|
|
24
|
+
// • its destination is a whole local integer array of the copied size — or a reservation the
|
|
25
|
+
// frame rounded up by less than a word, reached by nothing but elements inside the copy and its
|
|
26
|
+
// whole address — not volatile, and nothing before the copy mentions it but a local assigned
|
|
27
|
+
// its address alone, so moving the copy to the declaration changes no value anything reads (an
|
|
28
|
+
// initializer runs at entry, the copy at its own position);
|
|
29
|
+
// • its source is the whole image, which nothing but such copies mentions, and whose size is the
|
|
30
|
+
// count — one image may fill several locals, since the compiler places one per identical
|
|
31
|
+
// constant (varasm.c `output_constant_def`), each into its own local. Several arrays in one
|
|
32
|
+
// frame are each their own local where the frame audit proves the boundaries between them
|
|
33
|
+
// (frontend/frame-objects.ts `provenParts`). KNOWN GAP: where it keeps the frame's
|
|
34
|
+
// address-taken bytes as one local instead — a neighbour handed to a callee nothing bounds, or
|
|
35
|
+
// a compiler whose placement of such arrays is not known (target.ts `addressTakenArrayOrder`)
|
|
36
|
+
// — the image refuses at the size check;
|
|
37
|
+
// • the image writes one element width (a struct's mixed widths, or padding alone, has no
|
|
38
|
+
// element type this pass could declare). An element it relocates holds an address, spelled as
|
|
39
|
+
// the address constant it is (`u32 a[16] = { (u32)&h1, … }`, which agbcc copies from the same
|
|
40
|
+
// `.word h1` image as the pointer array the source declared, compiled).
|
|
41
|
+
// The local keeps its own element type where the body reaches it by an offset or a cast — those
|
|
42
|
+
// were spelled against it, so its width must be the image's — and takes the image's where only
|
|
43
|
+
// its whole address and element accesses, which each print their own width, reach it.
|
|
44
|
+
//
|
|
45
|
+
// Thumb is the only frontend that reads an anonymous image today. The rule is asked of the tree,
|
|
46
|
+
// for any compiler, but the only copy it reads is a call (proto.ts `blockCopyRoles`). KNOWN GAP: an
|
|
47
|
+
// INLINE copy — agbcc's `ldmia`/`stmia` for a word-aligned image below its memcpy sizes, lw/sw or
|
|
48
|
+
// lwl/lwr+swl/swr (IDO, gcc 2.7.2, KMC), lwz/stw (mwcc) — declines, naming the copy it does not
|
|
49
|
+
// read; no benchmark row has one, and the MIPS and PPC frontends refuse the address-taken frame
|
|
50
|
+
// local before the copy is reached.
|
|
51
|
+
import { type IrType, typeToString } from '../ir/types';
|
|
52
|
+
import { blockCopyRoles } from '../proto';
|
|
53
|
+
import {
|
|
54
|
+
type Expr,
|
|
55
|
+
type InitAddress,
|
|
56
|
+
type SFn,
|
|
57
|
+
type SImage,
|
|
58
|
+
type Stmt,
|
|
59
|
+
mapExprChildren,
|
|
60
|
+
mapStmtExprs,
|
|
61
|
+
mentionedName,
|
|
62
|
+
walkExprs,
|
|
63
|
+
} from './ast';
|
|
64
|
+
import { localMentions, mentionsAnyLocal } from './mentions';
|
|
65
|
+
|
|
66
|
+
/** `e` without the casts around it. */
|
|
67
|
+
function bare(e: Expr): Expr {
|
|
68
|
+
return e.k === 'cast' ? bare(e.e) : e;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The value of `e` where it is integer arithmetic on constants alone, or null. A copy's count is
|
|
72
|
+
* a constant the machine may build in several instructions — agbcc spells 256 and up as an 8-bit
|
|
73
|
+
* immediate shifted (`mov r2,#0x80; lsl r2,r2,#1`), which the tree keeps as `128 << 1`. */
|
|
74
|
+
function constantValue(e: Expr): number | null {
|
|
75
|
+
const b = bare(e);
|
|
76
|
+
if (b.k === 'const') {
|
|
77
|
+
return b.value;
|
|
78
|
+
}
|
|
79
|
+
if (b.k !== 'bin') {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const l = constantValue(b.l);
|
|
83
|
+
const r = constantValue(b.r);
|
|
84
|
+
if (l === null || r === null) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
switch (b.op) {
|
|
88
|
+
case '+':
|
|
89
|
+
return l + r;
|
|
90
|
+
case '-':
|
|
91
|
+
return l - r;
|
|
92
|
+
case '*':
|
|
93
|
+
return l * r;
|
|
94
|
+
case '<<':
|
|
95
|
+
return r >= 0 && r < 32 ? l * 2 ** r : null;
|
|
96
|
+
case '|':
|
|
97
|
+
return l >= 0 && r >= 0 && l < 2 ** 31 && r < 2 ** 31 ? l | r : null;
|
|
98
|
+
default:
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The object `e` names whole — `x` or `&x` — or null. */
|
|
104
|
+
function wholeObject(e: Expr): string | null {
|
|
105
|
+
const b = bare(e);
|
|
106
|
+
return b.k === 'postincr' ? null : (mentionedName(b) ?? null);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Every mention of `name` as an expression in `body`. */
|
|
110
|
+
function mentions(body: readonly Stmt[], name: string): number {
|
|
111
|
+
let n = 0;
|
|
112
|
+
for (const e of walkExprs(body as Stmt[])) {
|
|
113
|
+
if (mentionedName(e) === name) {
|
|
114
|
+
n++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return n;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const byteSize = (t: IrType): number | null =>
|
|
121
|
+
t.kind === 'int'
|
|
122
|
+
? t.width / 8
|
|
123
|
+
: t.kind === 'array'
|
|
124
|
+
? (() => {
|
|
125
|
+
const e = byteSize(t.elem);
|
|
126
|
+
return e === null ? null : e * t.count;
|
|
127
|
+
})()
|
|
128
|
+
: null;
|
|
129
|
+
|
|
130
|
+
/** `v`, an element of `width` bytes, read at the given signedness. */
|
|
131
|
+
function resign(v: number, width: number, signed: boolean): number {
|
|
132
|
+
const bits = 8 * width;
|
|
133
|
+
const u = ((v % 2 ** bits) + 2 ** bits) % 2 ** bits;
|
|
134
|
+
return signed && u >= 2 ** (bits - 1) ? u - 2 ** bits : u;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** One copy's fold: the copy statement's index, the local and its declaration. */
|
|
138
|
+
interface Fold {
|
|
139
|
+
at: number;
|
|
140
|
+
local: string;
|
|
141
|
+
type: IrType;
|
|
142
|
+
init: (number | InitAddress | null)[];
|
|
143
|
+
string: boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Why `image` cannot fold, as the tail of a sentence naming it, or the fold of each copy of it. */
|
|
147
|
+
function planFolds(sfn: SFn, image: SImage): Fold[] | string {
|
|
148
|
+
if (image.type.kind !== 'array' || image.type.elem.kind !== 'int') {
|
|
149
|
+
return 'whose image writes no one element width — a struct, or padding alone';
|
|
150
|
+
}
|
|
151
|
+
const copies: number[] = [];
|
|
152
|
+
sfn.body.forEach((s, i) => {
|
|
153
|
+
if (
|
|
154
|
+
s.k === 'exprstmt' &&
|
|
155
|
+
s.value.k === 'call' &&
|
|
156
|
+
wholeObject(s.value.args[blockCopyRoles(s.value.fn)?.src ?? -1] ?? { k: 'const', value: 0 }) === image.name
|
|
157
|
+
) {
|
|
158
|
+
copies.push(i);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
if (copies.length === 0) {
|
|
162
|
+
return (
|
|
163
|
+
'which no top-level block-copy call (memcpy) takes as its source — the one copy folded into an ' +
|
|
164
|
+
'initializer; an inline, element-by-element copy of the image, and any other read of it, is not'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (mentions(sfn.body, image.name) !== copies.length) {
|
|
168
|
+
return 'which the body reads other than as the source of a block copy at the top level';
|
|
169
|
+
}
|
|
170
|
+
const folds: Fold[] = [];
|
|
171
|
+
for (const at of copies) {
|
|
172
|
+
const fold = planCopy(sfn, image, at);
|
|
173
|
+
if (typeof fold === 'string') {
|
|
174
|
+
return fold;
|
|
175
|
+
}
|
|
176
|
+
folds.push(fold);
|
|
177
|
+
}
|
|
178
|
+
return folds;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Why the block copy of `image` at body index `at` cannot fold, or its fold. */
|
|
182
|
+
function planCopy(sfn: SFn, image: SImage, at: number): Fold | string {
|
|
183
|
+
const elem = (image.type as Extract<IrType, { kind: 'array' }>).elem as Extract<IrType, { kind: 'int' }>;
|
|
184
|
+
const size = byteSize(image.type)!;
|
|
185
|
+
const call = (sfn.body[at] as Extract<Stmt, { k: 'exprstmt' }>).value as Extract<Expr, { k: 'call' }>;
|
|
186
|
+
const roles = blockCopyRoles(call.fn)!;
|
|
187
|
+
if (constantValue(call.args[roles.n] ?? { k: 'var', name: '' }) !== size) {
|
|
188
|
+
return `whose copy does not move its whole ${size} bytes`;
|
|
189
|
+
}
|
|
190
|
+
const local = wholeObject(call.args[roles.dst] ?? { k: 'const', value: 0 });
|
|
191
|
+
const decl = sfn.locals.find((l) => l.name === local);
|
|
192
|
+
if (local === null || decl === undefined) {
|
|
193
|
+
return 'whose copy does not fill a local of this function';
|
|
194
|
+
}
|
|
195
|
+
if (decl.init !== undefined) {
|
|
196
|
+
return `whose copy fills '${local}', which another image already initializes`;
|
|
197
|
+
}
|
|
198
|
+
if (decl.volatile || decl.pointeeVolatile) {
|
|
199
|
+
return `whose copy fills '${local}', which is volatile`;
|
|
200
|
+
}
|
|
201
|
+
const declElem = decl.type.kind === 'array' ? decl.type.elem : null;
|
|
202
|
+
const declSize = byteSize(decl.type);
|
|
203
|
+
// The copy moves the whole object — a brace initializer's image carries its zero tail — so the
|
|
204
|
+
// count is the object's size, and a reservation the frame audit measured wider by less than a
|
|
205
|
+
// word is the frame's own rounding (agbcc rounds a 6-byte array's frame to 8).
|
|
206
|
+
if (declElem === null || declElem.kind !== 'int' || declSize === null || declSize < size || declSize - size >= 4) {
|
|
207
|
+
return `whose copy fills '${local}', a ${typeToString(decl.type)}, which is not an integer array of its ${size} bytes`;
|
|
208
|
+
}
|
|
209
|
+
// A local that holds nothing but the local's address — `p0 = (u8 *)&sp0;`, the base the
|
|
210
|
+
// readability hoist (l3/basecse.ts) places at the head of the body, ahead of the copy — reads no
|
|
211
|
+
// element where it is assigned, so the copy may move past it; its uses are the local's.
|
|
212
|
+
const aliasAt = aliasesOf(sfn, local);
|
|
213
|
+
const reach = new Set([local, ...aliasAt.keys()]);
|
|
214
|
+
const aliasing = new Set(aliasAt.values());
|
|
215
|
+
const kept = sfn.body.filter((_, i) => i !== at && !aliasing.has(i));
|
|
216
|
+
if (
|
|
217
|
+
mentionsAnyLocal(
|
|
218
|
+
sfn.body.slice(0, at).filter((_, i) => !aliasing.has(i)),
|
|
219
|
+
reach,
|
|
220
|
+
)
|
|
221
|
+
) {
|
|
222
|
+
return `whose copy into '${local}' is not the first thing the body does with it`;
|
|
223
|
+
}
|
|
224
|
+
const use = uses(kept, reach);
|
|
225
|
+
const count = size / (elem.width / 8);
|
|
226
|
+
// Reached as anything but elements, the local may be reached past the bytes copied — the tail a
|
|
227
|
+
// fill writes after a short string's copy — so the reservation must be the copy's exactly.
|
|
228
|
+
if (use.other && byteSize(decl.type) !== size) {
|
|
229
|
+
return `whose copy fills '${local}', a ${typeToString(decl.type)} the body reaches past the ${size} bytes copied`;
|
|
230
|
+
}
|
|
231
|
+
if (use.extents.some(([from, to]) => from < 0 || to > size)) {
|
|
232
|
+
return `whose copy fills '${local}', which the body indexes past the ${size} bytes copied`;
|
|
233
|
+
}
|
|
234
|
+
// An offset or a cast was spelled against the local's own element, so the image's must match and
|
|
235
|
+
// the local keeps its type. Element accesses print their own width, so where only they and the
|
|
236
|
+
// whole address reach it, the declaration (the frame audit may type it by bytes alone) witnesses
|
|
237
|
+
// nothing: the image says the element, and its signedness where it writes a negative (agbcc does
|
|
238
|
+
// only for a signed element), else the reads of that width do.
|
|
239
|
+
let own: Extract<IrType, { kind: 'int' }>;
|
|
240
|
+
if (use.other) {
|
|
241
|
+
if (declElem.width !== elem.width) {
|
|
242
|
+
return `whose ${elem.width / 8}-byte elements are not the ${declElem.width / 8}-byte elements the body reaches '${local}' by`;
|
|
243
|
+
}
|
|
244
|
+
own = declElem;
|
|
245
|
+
} else {
|
|
246
|
+
const signs = new Set(use.reads.filter((r) => r.width * 8 === elem.width).map((r) => r.signed));
|
|
247
|
+
const signed =
|
|
248
|
+
elem.signed ||
|
|
249
|
+
(signs.size === 1 ? [...signs][0] : declElem.width === elem.width && use.indexed > 0 && declElem.signed);
|
|
250
|
+
own = { ...elem, signed };
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
at,
|
|
254
|
+
local,
|
|
255
|
+
type: { kind: 'array', elem: own, count },
|
|
256
|
+
// an address is the address whatever the element's signedness: its cast spells that
|
|
257
|
+
init: image.init.map((v) => (typeof v === 'number' ? resign(v, elem.width / 8, own.signed) : v)),
|
|
258
|
+
string: image.string === true && elem.width === 8,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** The locals of `sfn` that hold `local`'s address and nothing else — assigned it once, at the top
|
|
263
|
+
* level of the body, and never written through `&` — each with the index of that assignment. */
|
|
264
|
+
function aliasesOf(sfn: SFn, local: string): Map<string, number> {
|
|
265
|
+
const counts = localMentions(sfn);
|
|
266
|
+
const out = new Map<string, number>();
|
|
267
|
+
sfn.body.forEach((s, i) => {
|
|
268
|
+
const m = s.k === 'assign' ? counts.get(s.name) : undefined;
|
|
269
|
+
if (
|
|
270
|
+
s.k === 'assign' &&
|
|
271
|
+
s.name !== local &&
|
|
272
|
+
m?.assigns === 1 &&
|
|
273
|
+
m.addrTaken === 0 &&
|
|
274
|
+
wholeObject(s.value) === local
|
|
275
|
+
) {
|
|
276
|
+
out.set(s.name, i);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** How `body` reaches the object the names in `reach` all denote whole: the byte extent of each
|
|
283
|
+
* direct element access with a constant index (`sp0[k]`, `((u8 *)sp0)[k]`, `p0[k]` — the index
|
|
284
|
+
* node carries its own width), how many of those there are in all, and whether anything reaches
|
|
285
|
+
* it other than as a whole call argument or a direct element access. */
|
|
286
|
+
function uses(
|
|
287
|
+
body: readonly Stmt[],
|
|
288
|
+
reach: ReadonlySet<string>,
|
|
289
|
+
): { indexed: number; extents: [number, number][]; reads: { width: number; signed: boolean }[]; other: boolean } {
|
|
290
|
+
let total = 0;
|
|
291
|
+
let accounted = 0;
|
|
292
|
+
let indexed = 0;
|
|
293
|
+
const extents: [number, number][] = [];
|
|
294
|
+
const reads: { width: number; signed: boolean }[] = [];
|
|
295
|
+
const reaches = (e: Expr): boolean => {
|
|
296
|
+
const n = wholeObject(e);
|
|
297
|
+
return n !== null && reach.has(n);
|
|
298
|
+
};
|
|
299
|
+
for (const e of walkExprs(body as Stmt[])) {
|
|
300
|
+
const n = mentionedName(e);
|
|
301
|
+
if (n !== undefined && reach.has(n)) {
|
|
302
|
+
total++;
|
|
303
|
+
}
|
|
304
|
+
if (e.k === 'call') {
|
|
305
|
+
accounted += e.args.filter(reaches).length;
|
|
306
|
+
}
|
|
307
|
+
if (e.k === 'index' && reaches(e.base)) {
|
|
308
|
+
accounted++;
|
|
309
|
+
indexed++;
|
|
310
|
+
reads.push({ width: e.width, signed: e.signed });
|
|
311
|
+
const k = bare(e.idx);
|
|
312
|
+
if (k.k === 'const') {
|
|
313
|
+
extents.push([k.value * e.width, (k.value + 1) * e.width]);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return { indexed, extents, reads, other: total !== accounted };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Whether every anonymous image `sfn` holds folds into a local's initializer: null when it does,
|
|
321
|
+
* else the image's name and the tail of the sentence that refuses it. */
|
|
322
|
+
export function constantImageRefusal(sfn: SFn): { image: string; refused: string } | null {
|
|
323
|
+
for (const image of sfn.images ?? []) {
|
|
324
|
+
const plan = planFolds(sfn, image);
|
|
325
|
+
if (typeof plan === 'string') {
|
|
326
|
+
return { image: image.name, refused: plan };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** `e` with every access to a folded local that reads one of its elements AS that element spelled
|
|
333
|
+
* `a[k]` on the local itself. The structurer spelled the access against the type the frame audit
|
|
334
|
+
* gave the local — bytes, unsigned — so a read of the element type this pass gives it (`s8`, from
|
|
335
|
+
* the image) came out through the local's address and a cast, `((s8 *)a)[5]`. The two are one C
|
|
336
|
+
* value and not one compile: agbcc re-forms `sp + 0x15` for the cast where `b[5]` reads through
|
|
337
|
+
* the base it already holds for `b`'s copy (`ldrsb r0, [r5, r0]`). An access of another width or
|
|
338
|
+
* signedness keeps its cast, which is the element type it spells. */
|
|
339
|
+
function asElement(e: Expr, by: ReadonlyMap<string, Fold>): Expr {
|
|
340
|
+
const inner = mapExprChildren(e, (c) => asElement(c, by));
|
|
341
|
+
if (inner.k !== 'index' || (inner.lead?.length ?? 0) > 0 || inner.baseElem !== undefined) {
|
|
342
|
+
return inner;
|
|
343
|
+
}
|
|
344
|
+
// the object the base names whole, as the fold's own census of element accesses reads it (`uses`)
|
|
345
|
+
const name = wholeObject(inner.base);
|
|
346
|
+
const fold = name === null ? undefined : by.get(name);
|
|
347
|
+
const elem = fold?.type.kind === 'array' ? fold.type.elem : undefined;
|
|
348
|
+
if (elem?.kind !== 'int' || elem.width !== inner.width * 8 || elem.signed !== inner.signed) {
|
|
349
|
+
return inner;
|
|
350
|
+
}
|
|
351
|
+
return { ...inner, base: { k: 'var', name: name! }, subscript: true };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** `sfn` with every anonymous image folded into the initializer of each local its copies fill,
|
|
355
|
+
* the copies gone and the image with them. Throws where one does not fold: structure() refused
|
|
356
|
+
* that function already, so a tree that reaches here unfoldable was made so by a later pass. */
|
|
357
|
+
export function foldConstantImages(sfn: SFn): SFn {
|
|
358
|
+
if (sfn.images === undefined) {
|
|
359
|
+
return sfn;
|
|
360
|
+
}
|
|
361
|
+
let out: SFn = { ...sfn };
|
|
362
|
+
delete out.images;
|
|
363
|
+
for (const image of sfn.images) {
|
|
364
|
+
const plan = planFolds(out, image);
|
|
365
|
+
if (typeof plan === 'string') {
|
|
366
|
+
throw new Error(`cannot spell '${sfn.name}': the compiler's anonymous constant '${image.name}' ${plan}`);
|
|
367
|
+
}
|
|
368
|
+
const copies = new Set(plan.map((f) => f.at));
|
|
369
|
+
const by = new Map(plan.map((f) => [f.local, f]));
|
|
370
|
+
out = {
|
|
371
|
+
...out,
|
|
372
|
+
body: out.body.filter((_, i) => !copies.has(i)).map((st) => mapStmtExprs(st, (e) => asElement(e, by))),
|
|
373
|
+
locals: out.locals.map((l) => {
|
|
374
|
+
const f = by.get(l.name);
|
|
375
|
+
return f === undefined
|
|
376
|
+
? l
|
|
377
|
+
: { ...l, type: f.type, init: f.init, ...(f.string ? { initString: true as const } : {}) };
|
|
378
|
+
}),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return out;
|
|
382
|
+
}
|
package/src/l3/dce.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// unresolved `?` value trips the contract first and never reaches DCE; `mustKeep` treating `?` as
|
|
26
26
|
// keep is defense-in-depth for any future caller that skips that check.
|
|
27
27
|
import type { Expr, SFn, Stmt } from './ast';
|
|
28
|
-
import { exprChildren, negateCond, stmtChildren, stmtExprs } from './ast';
|
|
28
|
+
import { exprChildren, keepsHelperRel, negateCond, stmtChildren, stmtExprs } from './ast';
|
|
29
29
|
|
|
30
30
|
/** Accumulate every LOCAL-eligible `var` name read anywhere in `e` (recurses all sub-exprs). An
|
|
31
31
|
* `addr` name counts too: taking a local's address makes every store to it observable through the
|
|
@@ -196,7 +196,7 @@ function dceBlock(
|
|
|
196
196
|
if (mustKeep(s.cond, volatiles)) {
|
|
197
197
|
rev.push({ k: 'exprstmt', value: s.cond });
|
|
198
198
|
}
|
|
199
|
-
} else if (t.out.length === 0) {
|
|
199
|
+
} else if (t.out.length === 0 && !keepsHelperRel(s.cond)) {
|
|
200
200
|
rev.push({ k: 'if', cond: negateCond(s.cond), then: e.out, else: [] });
|
|
201
201
|
} else {
|
|
202
202
|
rev.push({ k: 'if', cond: s.cond, then: t.out, else: e.out });
|
package/src/l3/initfirst.ts
CHANGED
|
@@ -43,12 +43,19 @@ import {
|
|
|
43
43
|
exprChildren,
|
|
44
44
|
exprEquals,
|
|
45
45
|
exprHasEffect,
|
|
46
|
+
keepsHelperRel,
|
|
46
47
|
mentionedName,
|
|
47
48
|
stmtChildren,
|
|
48
49
|
stmtExprs,
|
|
49
50
|
walkExprs,
|
|
50
51
|
} from './ast';
|
|
51
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
arithConversionSignedness,
|
|
54
|
+
declaredTypes,
|
|
55
|
+
exprIntWidth,
|
|
56
|
+
provablyNonNegative,
|
|
57
|
+
rendersSignedOverUnsigned,
|
|
58
|
+
} from './typing';
|
|
52
59
|
|
|
53
60
|
const readsVar = (e: Expr, name: string): boolean =>
|
|
54
61
|
mentionedName(e) === name || exprChildren(e).some((c) => readsVar(c, name));
|
|
@@ -95,8 +102,9 @@ const hoistableRead = (e: Expr, ownNames: ReadonlySet<string>, volatileLocals: R
|
|
|
95
102
|
};
|
|
96
103
|
|
|
97
104
|
/** A compare operand and the init's value denote the same 32-bit value under different SPELLINGS
|
|
98
|
-
* when a width-32 cast is all that separates them:
|
|
99
|
-
* the branch unsigned, and on a zero-trip guard that side is the very const the
|
|
105
|
+
* when a width-32 cast is all that separates them: the unsigned-compare spelling wraps one side in
|
|
106
|
+
* `(u32)` to make the branch unsigned, and on a zero-trip guard that side is the very const the
|
|
107
|
+
* init assigns.
|
|
100
108
|
* The swap is still exact — `v = X` stores X's 32 bits and `v` is 32-bit-declared
|
|
101
109
|
* (meaningPreserved refuses otherwise), so `v` and `(u32)X` carry the same bit pattern and only
|
|
102
110
|
* the compare's rendered signedness can differ, which meaningPreserved checks separately. A
|
|
@@ -137,6 +145,25 @@ const meaningPreserved = (
|
|
|
137
145
|
return before !== undefined && before === after;
|
|
138
146
|
};
|
|
139
147
|
|
|
148
|
+
/** `cond` with one side read through `v`, AFTER `meaningPreserved` said yes. That answer is a
|
|
149
|
+
* value proof the finished tree cannot repeat: `v` holds X, but `v`'s declaration does not say
|
|
150
|
+
* what range X had, so rank.ts's judgment of the tree (typing.ts `signedOverUnsignedCompare`)
|
|
151
|
+
* would call an unsigned compare whose ORIGINAL operands were both non-negative signed, and
|
|
152
|
+
* withhold a spelling that compares the same values. So a compare that was value-faithful before
|
|
153
|
+
* the swap is marked proved: its `unsignedCmp` evidence is dropped. One that was not keeps it. */
|
|
154
|
+
const swapSide = (
|
|
155
|
+
cond: Extract<Expr, { k: 'bin' }>,
|
|
156
|
+
side: 'l' | 'r',
|
|
157
|
+
v: string,
|
|
158
|
+
env: ReturnType<typeof declaredTypes>,
|
|
159
|
+
): Expr => {
|
|
160
|
+
const out: Extract<Expr, { k: 'bin' }> = { ...cond, [side]: { k: 'var', name: v } };
|
|
161
|
+
if (cond.unsignedCmp === true && !rendersSignedOverUnsigned(cond.l, cond.r, env)) {
|
|
162
|
+
delete out.unsignedCmp;
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
};
|
|
166
|
+
|
|
140
167
|
/** TOTAL (reads and pure writes) — see the note on stmtTouches. Runs against the pre-rewrite
|
|
141
168
|
* tree (`skip` is an original-tree statement, found by identity); the rewrites never change a
|
|
142
169
|
* name's presence in a subtree, so the verdict carries over to the rewritten one. */
|
|
@@ -223,7 +250,7 @@ export function initFirstGuards(sfn: SFn): SFn | null {
|
|
|
223
250
|
then = then.slice(1);
|
|
224
251
|
els = els.slice(1);
|
|
225
252
|
}
|
|
226
|
-
if (then.length === 0 && els.length > 0) {
|
|
253
|
+
if (then.length === 0 && els.length > 0 && !keepsHelperRel(cond)) {
|
|
227
254
|
const neg = cond.k === 'bin' ? NEGATE_REL[cond.op] : undefined;
|
|
228
255
|
if (neg !== undefined && cond.k === 'bin') {
|
|
229
256
|
cond = { ...cond, op: neg };
|
|
@@ -234,16 +261,20 @@ export function initFirstGuards(sfn: SFn): SFn | null {
|
|
|
234
261
|
// a COMMON-hoisted variable holds its const on both paths, so the condition's matching
|
|
235
262
|
// const operand reads through it unconditionally — no tail gate needed.
|
|
236
263
|
// A BARE const only, where the guard re-spelling below uses the cast-tolerant `sameValue`:
|
|
237
|
-
//
|
|
238
|
-
// type
|
|
239
|
-
//
|
|
240
|
-
//
|
|
264
|
+
// peeling a `(u32)` here would put a re-rendered signedness on the table with no cast left to
|
|
265
|
+
// say which. The name's DECLARED type can re-render it too — `if (a1 > 0)` read through a
|
|
266
|
+
// `u32 v` that holds the 0 is `a1 != 0` — so the swap asks `meaningPreserved` as the guard
|
|
267
|
+
// re-spelling does.
|
|
241
268
|
for (const hv of hoisted) {
|
|
242
269
|
if (cond.k === 'bin' && NEGATE_REL[cond.op]) {
|
|
243
270
|
if (cond.l.k === 'const' && cond.l.value === hv.value) {
|
|
244
|
-
cond
|
|
271
|
+
if (meaningPreserved(cond.l, cond.r, 'l', hv.name, env)) {
|
|
272
|
+
cond = swapSide(cond, 'l', hv.name, env);
|
|
273
|
+
}
|
|
245
274
|
} else if (cond.r.k === 'const' && cond.r.value === hv.value) {
|
|
246
|
-
|
|
275
|
+
if (meaningPreserved(cond.l, cond.r, 'r', hv.name, env)) {
|
|
276
|
+
cond = swapSide(cond, 'r', hv.name, env);
|
|
277
|
+
}
|
|
247
278
|
}
|
|
248
279
|
}
|
|
249
280
|
}
|
|
@@ -283,7 +314,7 @@ export function initFirstGuards(sfn: SFn): SFn | null {
|
|
|
283
314
|
) {
|
|
284
315
|
out.push(init);
|
|
285
316
|
moved.add(init);
|
|
286
|
-
cond =
|
|
317
|
+
cond = swapSide(cond, side, init.name, env);
|
|
287
318
|
then = then.slice(1);
|
|
288
319
|
changed = true;
|
|
289
320
|
}
|
package/src/l3/intcell.ts
CHANGED
|
@@ -38,7 +38,8 @@ const isCell = (x: Expr, name: string): boolean =>
|
|
|
38
38
|
* a bare value as it stands, anything else converted. */
|
|
39
39
|
function integerValue(value: Expr, name: string): Expr {
|
|
40
40
|
const stored = value.k === 'cast' ? value.e : value;
|
|
41
|
-
// `(u8 *)((u32)g + r)`: the integer sum the
|
|
41
|
+
// `(u8 *)((u32)g + r)`: the integer sum the arithmetic table restores to a byte pointer
|
|
42
|
+
// (structure/pointer-spelling.ts ARITH_ROWS, `restore: 'bytes'`)
|
|
42
43
|
const sum = stored.k === 'cast' && isPtrTo(stored.to, 'u8') ? stored.e : stored;
|
|
43
44
|
if (sum.k === 'bin' && (sum.op === '+' || sum.op === '-') && isCell(sum.l, name)) {
|
|
44
45
|
return { ...sum, l: { k: 'var', name } };
|