@asmlift/core 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- 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 +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -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 +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- 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 +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- 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/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- 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 +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- package/src/variation-tokens.ts +68 -3
|
@@ -0,0 +1,410 @@
|
|
|
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
|
+
// …and a read of the element's BYTES at the other signedness, converted to the element's own
|
|
342
|
+
// type, IS the element: `(s8)((u8 *)a)[i]` is `a[i]` on `s8 a[16]`. agbcc has no `ldrsb` with an
|
|
343
|
+
// immediate offset, so a signed byte read through the frame base is `ldrb` and `lsl #24; asr #24`,
|
|
344
|
+
// which the lift reads as the unsigned byte sign-extended; C extends the element itself the same
|
|
345
|
+
// way. Only a conversion to exactly the element type: a wider or a narrower one is another value.
|
|
346
|
+
// A DECLARED array's byte (a map global's, a proven frame part's) is read signed before this, by
|
|
347
|
+
// the structurer (structure.ts `declaredExtendedRead`); the type this pass gives a local exists
|
|
348
|
+
// only here, so for those the identity is applied here.
|
|
349
|
+
if (inner.k === 'cast' && inner.to.kind === 'int' && inner.volatile !== true && inner.e.k === 'index') {
|
|
350
|
+
const read = inner.e;
|
|
351
|
+
const elem = elementOf(read, by);
|
|
352
|
+
if (elem !== undefined && elem.width === inner.to.width && elem.signed === inner.to.signed) {
|
|
353
|
+
return asElement({ ...read, signed: elem.signed }, by);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (inner.k !== 'index') {
|
|
357
|
+
return inner;
|
|
358
|
+
}
|
|
359
|
+
const elem = elementOf(inner, by);
|
|
360
|
+
if (elem === undefined || elem.signed !== inner.signed) {
|
|
361
|
+
return inner;
|
|
362
|
+
}
|
|
363
|
+
return { ...inner, base: { k: 'var', name: wholeObject(inner.base)! }, subscript: true };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** The element type of the folded local `read` reads one element of — at the element's width,
|
|
367
|
+
* whatever its signedness — or undefined for any other access. */
|
|
368
|
+
function elementOf(
|
|
369
|
+
read: Extract<Expr, { k: 'index' }>,
|
|
370
|
+
by: ReadonlyMap<string, Fold>,
|
|
371
|
+
): Extract<IrType, { kind: 'int' }> | undefined {
|
|
372
|
+
if ((read.lead?.length ?? 0) > 0 || read.baseElem !== undefined) {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
// the object the base names whole, as the fold's own census of element accesses reads it (`uses`)
|
|
376
|
+
const name = wholeObject(read.base);
|
|
377
|
+
const fold = name === null ? undefined : by.get(name);
|
|
378
|
+
const elem = fold?.type.kind === 'array' ? fold.type.elem : undefined;
|
|
379
|
+
return elem?.kind === 'int' && elem.width === read.width * 8 ? elem : undefined;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** `sfn` with every anonymous image folded into the initializer of each local its copies fill,
|
|
383
|
+
* the copies gone and the image with them. Throws where one does not fold: structure() refused
|
|
384
|
+
* that function already, so a tree that reaches here unfoldable was made so by a later pass. */
|
|
385
|
+
export function foldConstantImages(sfn: SFn): SFn {
|
|
386
|
+
if (sfn.images === undefined) {
|
|
387
|
+
return sfn;
|
|
388
|
+
}
|
|
389
|
+
let out: SFn = { ...sfn };
|
|
390
|
+
delete out.images;
|
|
391
|
+
for (const image of sfn.images) {
|
|
392
|
+
const plan = planFolds(out, image);
|
|
393
|
+
if (typeof plan === 'string') {
|
|
394
|
+
throw new Error(`cannot spell '${sfn.name}': the compiler's anonymous constant '${image.name}' ${plan}`);
|
|
395
|
+
}
|
|
396
|
+
const copies = new Set(plan.map((f) => f.at));
|
|
397
|
+
const by = new Map(plan.map((f) => [f.local, f]));
|
|
398
|
+
out = {
|
|
399
|
+
...out,
|
|
400
|
+
body: out.body.filter((_, i) => !copies.has(i)).map((st) => mapStmtExprs(st, (e) => asElement(e, by))),
|
|
401
|
+
locals: out.locals.map((l) => {
|
|
402
|
+
const f = by.get(l.name);
|
|
403
|
+
return f === undefined
|
|
404
|
+
? l
|
|
405
|
+
: { ...l, type: f.type, init: f.init, ...(f.string ? { initString: true as const } : {}) };
|
|
406
|
+
}),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
return out;
|
|
410
|
+
}
|
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 { caseEndGotoLeaves, exprChildren, keepsHelperRel, negateCond, stmtChildren, stmtExprs } from './ast';
|
|
29
29
|
|
|
30
30
|
/** Accumulate every LOCAL-eligible `var` name read anywhere in `e` (recurses all sub-exprs). An
|
|
31
31
|
* `addr` name counts too: taking a local's address makes every store to it observable through the
|
|
@@ -106,6 +106,24 @@ function allAddrNamesInto(stmts: Stmt[], out: Set<string>): void {
|
|
|
106
106
|
interface JumpLive {
|
|
107
107
|
brk: ReadonlySet<string>;
|
|
108
108
|
cont?: ReadonlySet<string>; // absent in a switch no loop encloses
|
|
109
|
+
/** live at a case label of the innermost switch: what its `goto` (l3/ast.ts) lands on — absent
|
|
110
|
+
* where no switch encloses the statement, or a loop stands between it and the switch */
|
|
111
|
+
caseLabel?: ReadonlySet<string>;
|
|
112
|
+
/** live at the end of the innermost switch arm around the statement, loops between included: what
|
|
113
|
+
* a case-end `goto` lands on — absent where no switch encloses it */
|
|
114
|
+
caseEnd?: ReadonlySet<string>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The conservative set live throughout a loop: what is live after it and every read in it, and,
|
|
118
|
+
* where a case-end `goto` in its body leaves it, what is live at the arm's end that goto lands on —
|
|
119
|
+
* a later iteration may take it. */
|
|
120
|
+
function loopLiveOf(s: Stmt & { body: Stmt[] }, live: ReadonlySet<string>, jumps: JumpLive | null): Set<string> {
|
|
121
|
+
const loopLive = new Set(live);
|
|
122
|
+
allReadsInto([s], loopLive);
|
|
123
|
+
if (jumps?.caseEnd && caseEndGotoLeaves(s.body)) {
|
|
124
|
+
jumps.caseEnd.forEach((n) => loopLive.add(n));
|
|
125
|
+
}
|
|
126
|
+
return loopLive;
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
/** Backward live-variable walk over one block. `liveOut` is the set of locals live on exit;
|
|
@@ -165,6 +183,26 @@ function dceBlock(
|
|
|
165
183
|
rev.push(s);
|
|
166
184
|
break;
|
|
167
185
|
}
|
|
186
|
+
// The jump lands on the `break;` that ends its arm (l3/ast.ts, contracts.ts
|
|
187
|
+
// `assertCaseGotosNarrow`), whose every read the switch below already counts live.
|
|
188
|
+
case 'caseEndGoto': {
|
|
189
|
+
if (!jumps?.caseEnd) {
|
|
190
|
+
throw new Error('dce: a case-end goto outside a switch arm');
|
|
191
|
+
}
|
|
192
|
+
setLive(new Set(jumps.caseEnd));
|
|
193
|
+
rev.push(s);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case 'goto': {
|
|
197
|
+
// The jump lands on a case label of the switch it sits in (l3/ast.ts, contracts.ts
|
|
198
|
+
// `assertCaseGotosNarrow`), whose every read the switch below already counts live.
|
|
199
|
+
if (!jumps?.caseLabel) {
|
|
200
|
+
throw new Error('dce: a goto outside the switch whose case it names');
|
|
201
|
+
}
|
|
202
|
+
setLive(new Set(jumps.caseLabel));
|
|
203
|
+
rev.push(s);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
168
206
|
case 'break':
|
|
169
207
|
case 'continue': {
|
|
170
208
|
// A mid-body `if (c) { v = x; break; }` carries the loop's exit value in `v`, while the
|
|
@@ -196,7 +234,7 @@ function dceBlock(
|
|
|
196
234
|
if (mustKeep(s.cond, volatiles)) {
|
|
197
235
|
rev.push({ k: 'exprstmt', value: s.cond });
|
|
198
236
|
}
|
|
199
|
-
} else if (t.out.length === 0) {
|
|
237
|
+
} else if (t.out.length === 0 && !keepsHelperRel(s.cond)) {
|
|
200
238
|
rev.push({ k: 'if', cond: negateCond(s.cond), then: e.out, else: [] });
|
|
201
239
|
} else {
|
|
202
240
|
rev.push({ k: 'if', cond: s.cond, then: t.out, else: e.out });
|
|
@@ -207,9 +245,12 @@ function dceBlock(
|
|
|
207
245
|
case 'dowhile': {
|
|
208
246
|
// Conservative: any local read anywhere in the loop is live throughout it, so a
|
|
209
247
|
// loop-carried store is never cut. Body DCE removes only what is dead on EVERY path.
|
|
210
|
-
const loopLive =
|
|
211
|
-
|
|
212
|
-
|
|
248
|
+
const loopLive = loopLiveOf(s, live, jumps);
|
|
249
|
+
const b = dceBlock(s.body, loopLive, locals, volatiles, {
|
|
250
|
+
brk: loopLive,
|
|
251
|
+
cont: loopLive,
|
|
252
|
+
caseEnd: jumps?.caseEnd,
|
|
253
|
+
});
|
|
213
254
|
const nlive = new Set(loopLive);
|
|
214
255
|
for (const r of b.liveIn) {
|
|
215
256
|
nlive.add(r);
|
|
@@ -219,9 +260,12 @@ function dceBlock(
|
|
|
219
260
|
break;
|
|
220
261
|
}
|
|
221
262
|
case 'for': {
|
|
222
|
-
const loopLive =
|
|
223
|
-
|
|
224
|
-
|
|
263
|
+
const loopLive = loopLiveOf(s, live, jumps);
|
|
264
|
+
const b = dceBlock(s.body, loopLive, locals, volatiles, {
|
|
265
|
+
brk: loopLive,
|
|
266
|
+
cont: loopLive,
|
|
267
|
+
caseEnd: jumps?.caseEnd,
|
|
268
|
+
});
|
|
225
269
|
const nlive = new Set(loopLive);
|
|
226
270
|
for (const r of b.liveIn) {
|
|
227
271
|
nlive.add(r);
|
|
@@ -235,7 +279,7 @@ function dceBlock(
|
|
|
235
279
|
// read anywhere in the switch as live throughout — no case-body store is ever cut.
|
|
236
280
|
const swLive = new Set(live);
|
|
237
281
|
allReadsInto([s], swLive);
|
|
238
|
-
const inSwitch: JumpLive = { brk: swLive, cont: jumps?.cont };
|
|
282
|
+
const inSwitch: JumpLive = { brk: swLive, cont: jumps?.cont, caseLabel: swLive, caseEnd: swLive };
|
|
239
283
|
const cases = s.cases.map((c) => ({ ...c, body: dceBlock(c.body, swLive, locals, volatiles, inSwitch).out }));
|
|
240
284
|
const def = s.default ? dceBlock(s.default, swLive, locals, volatiles, inSwitch).out : s.default;
|
|
241
285
|
const nlive = new Set(swLive);
|
package/src/l3/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 } };
|