@asmlift/core 0.8.0 → 0.9.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 +34 -30
- package/package.json +1 -1
- package/src/aggregate.ts +248 -0
- package/src/backend/c.ts +5 -2
- package/src/backend/cfamily.ts +236 -37
- package/src/backend/cpp.ts +114 -9
- package/src/backend/pascal.ts +39 -0
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +615 -79
- package/src/declare.ts +75 -5
- package/src/frontend/asmdata.ts +19 -5
- package/src/frontend/call-plan.ts +686 -0
- package/src/frontend/device-pins.ts +277 -0
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +167 -0
- package/src/frontend/frame-objects.ts +2131 -0
- package/src/frontend/high-half.ts +2 -1
- package/src/frontend/local-object.ts +490 -0
- package/src/frontend/mips.ts +363 -40
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/ppc.ts +520 -129
- package/src/frontend/reloc-symbol.ts +94 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +669 -76
- package/src/frontend/stackargs.ts +120 -39
- package/src/frontend/thumb.ts +2764 -2018
- package/src/ir/alias.ts +29 -0
- package/src/ir/core.ts +101 -3
- package/src/ir/discipline.ts +248 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/ir/opcodes.ts +84 -112
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +118 -2
- package/src/ir/verify.ts +150 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +186 -14
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/dce.ts +32 -10
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +27 -9
- package/src/l3/intcell.ts +97 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +218 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +3 -2
- package/src/l3/ptrcell.ts +357 -0
- package/src/l3/regspell.ts +17 -3
- package/src/l3/reindex.ts +10 -7
- package/src/l3/symbol-refs.ts +104 -23
- package/src/l3/tailmerge.ts +6 -19
- package/src/l3/typing.ts +137 -25
- package/src/l3/unreduce.ts +4 -4
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +11 -1
- package/src/pattern/engine.ts +19 -16
- package/src/pipeline.ts +57 -12
- package/src/proto-context.ts +788 -0
- package/src/proto.ts +689 -50
- package/src/raise/arrays.ts +4 -3
- package/src/raise/declared-volatile.ts +131 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +243 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +26 -5
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +510 -0
- package/src/raise/pairparams.ts +81 -0
- package/src/raise/paramwidth.ts +7 -0
- package/src/raise/pre-recovery.ts +78 -4
- package/src/raise/recover.ts +14 -10
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +41 -19
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/struct-arrays.ts +5 -4
- package/src/raise/structs.ts +311 -98
- package/src/raise/truncload.ts +366 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +30 -5
- package/src/rank-variations.ts +36 -8
- package/src/rank.ts +276 -80
- package/src/runtime-helpers.ts +287 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1285 -448
- package/src/structure/hazards.ts +633 -58
- package/src/structure/local-statics.ts +381 -0
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +1772 -395
- package/src/structure/switch-recover.ts +159 -78
- package/src/symbols.ts +38 -3
- package/src/target.ts +662 -59
- package/src/trace.ts +44 -24
- package/src/variation-definitions.ts +119 -1
- package/src/variation-tokens.ts +20 -6
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// asmlift — the C DEFINITION of each function-scope static a function names (ir/core.ts
|
|
2
|
+
// `LocalObjects`), typed from the target's own definition and the accesses the function makes.
|
|
3
|
+
//
|
|
4
|
+
// The frontend hands over what the target shows: section, size and bytes, and where the target is
|
|
5
|
+
// an assembler listing (agbcc's `.s`) the definition's directives too. Those say the element: the
|
|
6
|
+
// compiler writes each scalar of the initializer with the directive of its width, so a table of
|
|
7
|
+
// `.short`s is one of 16-bit elements whatever the function reads out of it, and a narrow negative
|
|
8
|
+
// value is only ever written for a signed type. An object file (mwcc) shows no directives, and
|
|
9
|
+
// there the element is the one width every access agrees on. The loads of the element's width then
|
|
10
|
+
// settle its signedness — they are what the function computes, and a byte holds the same bits
|
|
11
|
+
// declared either way, so they outrank a negative the source wrote through another type (a struct's
|
|
12
|
+
// signed field, a cast) — and the definition settles it where no load does. An object nothing says
|
|
13
|
+
// anything about is bytes.
|
|
14
|
+
//
|
|
15
|
+
// The target also shows the ALIGNMENT — the listing's `.align`, mwcc's `.comment` record — which
|
|
16
|
+
// decides where the object lands after the statics declared before it, and whether a DMA or GX
|
|
17
|
+
// buffer is where the hardware needs it. The definition this pass picks gets its compiler's own
|
|
18
|
+
// alignment, which the target declares ({@link StaticLayout}): a scalar to its width, an array to
|
|
19
|
+
// its element or the compiler's aggregate floor, a string literal to the compiler's string
|
|
20
|
+
// alignment. A target aligned wider — a struct, an `s64`, an `ALIGNED(32)` — keeps that alignment
|
|
21
|
+
// in an attribute, because a table one element off is misread by the machine and scored as a
|
|
22
|
+
// MATCH. A byte array the listing wrote as strings and placed at the string alignment is spelled
|
|
23
|
+
// with a string literal, which is what gives it that alignment.
|
|
24
|
+
//
|
|
25
|
+
// The definition is a SymbolInfo as well as a declaration, because the structurer spells an access
|
|
26
|
+
// through a symbol's declared shape (`tide[i]` for an array, the bare `q` for a scalar): the shape
|
|
27
|
+
// it spells against and the object the backend defines are one reading of the same facts. Both are
|
|
28
|
+
// keyed by the LINKER name the IR carries (`tide.3`), and the finished tree is renamed to the
|
|
29
|
+
// source's (`tide`) in one step, `nameLocalStatics`, which is where a clash between that name and
|
|
30
|
+
// anything else the function names can first be seen whole.
|
|
31
|
+
//
|
|
32
|
+
// REFUSES, naming the static, when the target's compiler declares no layout rules, when nothing
|
|
33
|
+
// in the definition settles the width and the accesses disagree on it, when the width is no
|
|
34
|
+
// integer type's or does not divide the size, when the loads disagree on the signedness, when the
|
|
35
|
+
// definition is aligned narrower than its declaration here would be, when a bss static sits where
|
|
36
|
+
// only a scalar initialized to zero is sure to land and it is no scalar (or the input does not
|
|
37
|
+
// show where it sits);
|
|
38
|
+
// and, at the rename, when two statics share a source name (one block cannot declare both) or
|
|
39
|
+
// when the function names anything else by it — a global, a callee, a parameter, a local, itself —
|
|
40
|
+
// which the block-scope static would hide.
|
|
41
|
+
import { type Fn, type LocalObject, type LocalObjects, type Op, type Value, defOpMap } from '../ir/core';
|
|
42
|
+
import { type IrType, T } from '../ir/types';
|
|
43
|
+
import {
|
|
44
|
+
type Expr,
|
|
45
|
+
type SFn,
|
|
46
|
+
type SStatic,
|
|
47
|
+
type Stmt,
|
|
48
|
+
mapExprChildren,
|
|
49
|
+
mapStmtExprs,
|
|
50
|
+
mapStmtLists,
|
|
51
|
+
walkExprs,
|
|
52
|
+
} from '../l3/ast';
|
|
53
|
+
import { takenNames } from '../l3/hoist';
|
|
54
|
+
import { type SymbolInfo, accessSignedness } from '../symbols';
|
|
55
|
+
|
|
56
|
+
/** How a compiler lays out a function-scope static — facts about the COMPILER, declared per target
|
|
57
|
+
* (target.ts `compilerBehaviors.staticLayout`), from which the definition that puts a static where
|
|
58
|
+
* the target has it follows. */
|
|
59
|
+
export interface StaticLayout {
|
|
60
|
+
/** the least alignment it gives an array or a struct, whatever the element */
|
|
61
|
+
aggregateAlign: number;
|
|
62
|
+
/** the alignment it gives a declaration initialized by a string literal */
|
|
63
|
+
stringAlign: number;
|
|
64
|
+
/** where a scalar initialized to zero goes: kept in `.data`, or moved to bss — where its
|
|
65
|
+
* initializer is gone, and one element of zero in `.data` must then have been an aggregate */
|
|
66
|
+
zeroScalar: 'data' | 'bss';
|
|
67
|
+
/** The bss sections of such a compiler, each laying out the scalars it moved there first, in
|
|
68
|
+
* declaration order, and then the statics with no initializer: in reverse declaration order
|
|
69
|
+
* (mwcc's `.sbss`: `a = 0; b; c = 0;` at +0, +8, +4), or in the order its code first uses them
|
|
70
|
+
* (mwcc's `.bss`: `a; b = 0;` used a first at +4, +0). Either way the offsets say which statics
|
|
71
|
+
* sit where only one with the `= 0` is sure to land. */
|
|
72
|
+
uninitOrder?: Readonly<Record<string, 'reversed' | 'first-use'>>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The bss statics of `objs` to write `= 0` ({@link StaticLayout.uninitOrder}), or
|
|
76
|
+
* the symbol of one whose placement the input does not show. In a section of either order a
|
|
77
|
+
* function's zero-initialized statics come first, scalars with their counters rising.
|
|
78
|
+
*
|
|
79
|
+
* Where the others follow reversed, a static with a later-declared one at a higher offset had the
|
|
80
|
+
* initializer. The last of the rising run cannot be told from the first of the falling one, and
|
|
81
|
+
* both declarations put it in the same place: it is left without.
|
|
82
|
+
*
|
|
83
|
+
* Where the others follow by first use, the leading run of `scalarShaped` statics with rising
|
|
84
|
+
* counters had it, save that a run that is the whole section leaves its last without, which lands
|
|
85
|
+
* after the rest either way. Any of that run may have had none and been used first, and the `= 0`
|
|
86
|
+
* puts it where it was, which the candidate's code would decide otherwise. */
|
|
87
|
+
function zeroInitialized(
|
|
88
|
+
objs: readonly LocalObject[],
|
|
89
|
+
uninitOrder: NonNullable<StaticLayout['uninitOrder']>,
|
|
90
|
+
scalarShaped: (o: LocalObject) => boolean,
|
|
91
|
+
): Set<string> | { symbol: string } {
|
|
92
|
+
const out = new Set<string>();
|
|
93
|
+
const bss = objs.filter((o) => o.section === 'bss');
|
|
94
|
+
for (const o of bss) {
|
|
95
|
+
if (o.placement === undefined) {
|
|
96
|
+
return { symbol: o.symbol };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const [section, order] of Object.entries(uninitOrder)) {
|
|
100
|
+
const run = bss
|
|
101
|
+
.filter((o) => o.placement!.section === section)
|
|
102
|
+
.sort((a, b) => a.placement!.offset - b.placement!.offset);
|
|
103
|
+
if (order === 'reversed') {
|
|
104
|
+
for (const o of run) {
|
|
105
|
+
if (run.some((p) => p.placement!.offset > o.placement!.offset && p.order > o.order)) {
|
|
106
|
+
out.add(o.symbol);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
let lead = 0;
|
|
112
|
+
while (lead < run.length && scalarShaped(run[lead]) && (lead === 0 || run[lead].order > run[lead - 1].order)) {
|
|
113
|
+
lead++;
|
|
114
|
+
}
|
|
115
|
+
for (const o of run.slice(0, lead === run.length ? lead - 1 : lead)) {
|
|
116
|
+
out.add(o.symbol);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Each static's shape for the structurer, and its definition for the backend. */
|
|
123
|
+
export interface LocalStaticShapes {
|
|
124
|
+
infos: Map<string, SymbolInfo>;
|
|
125
|
+
statics: SStatic[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** One access through a static's address: the width it reads or writes, and a load's extension. */
|
|
129
|
+
interface Access {
|
|
130
|
+
width: number;
|
|
131
|
+
signed?: boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The width an `aload`/`astore` reads or writes. Its `elemSize` is the element's, which for an
|
|
135
|
+
* element of a struct array (raise/struct-arrays.ts, `fieldOff`) is the STRIDE: the access itself
|
|
136
|
+
* is the field's, whose type the base's struct carries. Null for a field that type does not hold. */
|
|
137
|
+
function indexedWidth(op: Op): number | null {
|
|
138
|
+
const fieldOff = op.attrs.fieldOff as number | undefined;
|
|
139
|
+
if (fieldOff === undefined) {
|
|
140
|
+
return op.attrs.elemSize as number;
|
|
141
|
+
}
|
|
142
|
+
const bt = op.operands[0].type;
|
|
143
|
+
const field = bt.kind === 'ptr' && bt.to.kind === 'struct' ? bt.to.fields.find((f) => f.off === fieldOff) : undefined;
|
|
144
|
+
return field !== undefined && field.type.kind === 'int' ? field.type.width / 8 : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Every access through each static's address. */
|
|
148
|
+
function accessesOf(fn: Fn, names: ReadonlySet<string>): Map<string, Access[]> {
|
|
149
|
+
const defs = defOpMap(fn);
|
|
150
|
+
/** the static an address is computed from — its `gaddr`, through adds of anything else */
|
|
151
|
+
const baseOf = (v: Value, seen = new Set<Value>()): string | null => {
|
|
152
|
+
const d = defs.get(v);
|
|
153
|
+
if (d === undefined || seen.has(v)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
seen.add(v);
|
|
157
|
+
if (d.opcode === 'gaddr') {
|
|
158
|
+
return names.has(d.attrs.sym as string) ? (d.attrs.sym as string) : null;
|
|
159
|
+
}
|
|
160
|
+
if (d.opcode === 'add' || d.opcode === 'sub') {
|
|
161
|
+
const found = d.operands.slice(0, d.opcode === 'sub' ? 1 : undefined).map((o) => baseOf(o, seen));
|
|
162
|
+
const hits = found.filter((f): f is string => f !== null);
|
|
163
|
+
return hits.length === 1 ? hits[0] : null;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
};
|
|
167
|
+
const out = new Map<string, Access[]>();
|
|
168
|
+
for (const b of fn.blocks) {
|
|
169
|
+
for (const op of b.ops) {
|
|
170
|
+
const plain = op.opcode === 'load' || op.opcode === 'store';
|
|
171
|
+
const indexed = op.opcode === 'aload' || op.opcode === 'astore';
|
|
172
|
+
if (!plain && !indexed) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const sym = baseOf(op.operands[0]);
|
|
176
|
+
if (sym === null) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const width = plain ? (op.attrs.width as number) : indexedWidth(op);
|
|
180
|
+
if (width === null) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const isLoad = op.opcode === 'load' || op.opcode === 'aload';
|
|
184
|
+
const list = out.get(sym) ?? out.set(sym, []).get(sym)!;
|
|
185
|
+
list.push({
|
|
186
|
+
width,
|
|
187
|
+
...(isLoad ? { signed: accessSignedness(width, op.attrs.signed as boolean | undefined) } : {}),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The elements of `obj`'s initial bytes read `width` bytes at a time in its byte order. */
|
|
195
|
+
function elements(obj: LocalObject, width: number, signed: boolean): number[] {
|
|
196
|
+
const b = obj.bytes!;
|
|
197
|
+
const out: number[] = [];
|
|
198
|
+
for (let at = 0; at < obj.size; at += width) {
|
|
199
|
+
let v = 0;
|
|
200
|
+
for (let i = 0; i < width; i++) {
|
|
201
|
+
v = v * 256 + b[obj.bigEndian ? at + i : at + width - 1 - i];
|
|
202
|
+
}
|
|
203
|
+
out.push(signed && v >= 2 ** (8 * width - 1) ? v - 2 ** (8 * width) : v);
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The width of `obj`'s element: the one its definition's directives write, else the one every
|
|
209
|
+
* access agrees on; with no access either, the whole object where it is aligned below the
|
|
210
|
+
* compiler's aggregate floor, as only a scalar is, and a byte where it is not. */
|
|
211
|
+
function elementWidth(
|
|
212
|
+
obj: LocalObject,
|
|
213
|
+
accesses: readonly Access[],
|
|
214
|
+
layout: StaticLayout,
|
|
215
|
+
): number | { refused: string } {
|
|
216
|
+
if (obj.directives?.unit !== undefined) {
|
|
217
|
+
return obj.directives.unit;
|
|
218
|
+
}
|
|
219
|
+
const widths = [...new Set(accesses.map((x) => x.width))];
|
|
220
|
+
if (widths.length > 1) {
|
|
221
|
+
return { refused: `whose accesses disagree on its element width (${widths.sort().join(' and ')} bytes)` };
|
|
222
|
+
}
|
|
223
|
+
const shown = obj.directives?.align ?? obj.placement?.align;
|
|
224
|
+
const scalarOnly = shown !== undefined && shown < layout.aggregateAlign;
|
|
225
|
+
return widths[0] ?? (scalarOnly && [1, 2, 4].includes(obj.size) ? obj.size : 1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** The shape and definition of every static `fn` defines, or the refusal: the static's linker name
|
|
229
|
+
* and the tail of the sentence. */
|
|
230
|
+
export function localStaticShapes(
|
|
231
|
+
fn: Fn,
|
|
232
|
+
layout: StaticLayout | undefined,
|
|
233
|
+
): LocalStaticShapes | { symbol: string; refused: string } {
|
|
234
|
+
const objs = fn.localObjects;
|
|
235
|
+
const infos = new Map<string, SymbolInfo>();
|
|
236
|
+
const statics: SStatic[] = [];
|
|
237
|
+
if (objs === undefined) {
|
|
238
|
+
return { infos, statics };
|
|
239
|
+
}
|
|
240
|
+
if (layout === undefined) {
|
|
241
|
+
const [first] = objs.keys();
|
|
242
|
+
return { symbol: first, refused: "whose layout rules this target's compiler does not declare" };
|
|
243
|
+
}
|
|
244
|
+
const access = accessesOf(fn, new Set(objs.keys()));
|
|
245
|
+
const widthOf = (o: LocalObject) => elementWidth(o, access.get(o.symbol) ?? [], layout);
|
|
246
|
+
/** Declared here as a scalar, or sized and aligned as one — the compiler aligns a scalar to at
|
|
247
|
+
* least its width. One declared as an array where only a zero scalar sits is refused below: an
|
|
248
|
+
* `s32 = 0` read a byte at a time, or only through its address, lands there as well as four
|
|
249
|
+
* bytes used first. */
|
|
250
|
+
const scalarShaped = (o: LocalObject): boolean =>
|
|
251
|
+
widthOf(o) === o.size || ([1, 2, 4, 8].includes(o.size) && (o.placement?.align ?? 0) >= o.size);
|
|
252
|
+
const zeroed =
|
|
253
|
+
layout.uninitOrder !== undefined
|
|
254
|
+
? zeroInitialized([...objs.values()], layout.uninitOrder, scalarShaped)
|
|
255
|
+
: new Set<string>();
|
|
256
|
+
if (!(zeroed instanceof Set)) {
|
|
257
|
+
return { symbol: zeroed.symbol, refused: 'in bss at an offset this input does not show' };
|
|
258
|
+
}
|
|
259
|
+
// Declared in the target's declaration order, which the compiler lays the objects out by (mwcc
|
|
260
|
+
// reverses it in `.sbss` for statics with no initializer), so that the candidate's compiler
|
|
261
|
+
// places each where the target has it — except mwcc's `.bss` statics with no initializer, which
|
|
262
|
+
// it places in the order it first uses them: the candidate's code decides that, no declaration.
|
|
263
|
+
for (const obj of [...objs.values()].sort((a, b) => a.order - b.order)) {
|
|
264
|
+
const say = (why: string) => ({ symbol: obj.symbol, refused: why });
|
|
265
|
+
const accesses = access.get(obj.symbol) ?? [];
|
|
266
|
+
const dir = obj.directives;
|
|
267
|
+
// the alignment the target shows
|
|
268
|
+
const shown = dir?.align ?? obj.placement?.align;
|
|
269
|
+
const width = widthOf(obj);
|
|
270
|
+
if (typeof width !== 'number') {
|
|
271
|
+
return say(width.refused);
|
|
272
|
+
}
|
|
273
|
+
if (![1, 2, 4].includes(width)) {
|
|
274
|
+
return say(`whose ${width}-byte elements are no integer type`);
|
|
275
|
+
}
|
|
276
|
+
if (obj.size % width !== 0) {
|
|
277
|
+
return say(`whose ${width}-byte elements do not divide its ${obj.size} bytes`);
|
|
278
|
+
}
|
|
279
|
+
// an access of another width reads through a cast, and says nothing about these elements
|
|
280
|
+
const signs = new Set(accesses.filter((x) => x.width === width && x.signed !== undefined).map((x) => x.signed));
|
|
281
|
+
if (signs.size > 1) {
|
|
282
|
+
return say('whose loads disagree on whether its elements are signed');
|
|
283
|
+
}
|
|
284
|
+
const signed = signs.size > 0 ? signs.has(true) : dir?.negative === true;
|
|
285
|
+
const count = obj.size / width;
|
|
286
|
+
const elem = T.int(width * 8, signed);
|
|
287
|
+
const init = zeroed.has(obj.symbol) ? [0] : obj.bytes === undefined ? undefined : elements(obj, width, signed);
|
|
288
|
+
if (zeroed.has(obj.symbol) && count !== 1) {
|
|
289
|
+
return say(`laid out where only a scalar initialized to zero is, but of ${count} elements`);
|
|
290
|
+
}
|
|
291
|
+
// One element is a scalar — unless it is initialized data holding zero on a compiler that
|
|
292
|
+
// moves a zero scalar to bss, where it can only have been an aggregate, which the compiler
|
|
293
|
+
// keeps in .data.
|
|
294
|
+
const scalar =
|
|
295
|
+
count === 1 && !(layout.zeroScalar === 'bss' && obj.section === 'data' && init!.every((v) => v === 0));
|
|
296
|
+
const type: IrType = scalar ? elem : T.array(elem, count);
|
|
297
|
+
const string = dir?.string === true && width === 1 && !scalar && shown !== undefined && shown >= layout.stringAlign;
|
|
298
|
+
// The alignment the target shows against the one this definition gets from its compiler.
|
|
299
|
+
const own = scalar ? width : Math.max(width, layout.aggregateAlign, string ? layout.stringAlign : 1);
|
|
300
|
+
if (shown !== undefined && shown < own) {
|
|
301
|
+
return say(`whose definition is aligned to ${shown} bytes, less than the ${own} its declaration here would get`);
|
|
302
|
+
}
|
|
303
|
+
const align = shown !== undefined && shown > own ? shown : undefined;
|
|
304
|
+
const isConst = obj.section === 'rodata';
|
|
305
|
+
infos.set(obj.symbol, {
|
|
306
|
+
name: obj.symbol,
|
|
307
|
+
kind: 'data',
|
|
308
|
+
size: obj.size,
|
|
309
|
+
...(scalar
|
|
310
|
+
? { shape: 'scalar' as const, signed }
|
|
311
|
+
: { shape: 'array' as const, elemSize: width, elemSigned: signed, dims: [count] }),
|
|
312
|
+
...(isConst ? { const: true } : {}),
|
|
313
|
+
});
|
|
314
|
+
statics.push({
|
|
315
|
+
name: obj.name,
|
|
316
|
+
type,
|
|
317
|
+
...(isConst ? { const: true as const } : {}),
|
|
318
|
+
...(align !== undefined ? { align } : {}),
|
|
319
|
+
...(init ? { init } : {}),
|
|
320
|
+
...(string ? { string: true as const } : {}),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return { infos, statics };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** `sfn` with each static's linker name replaced by its source name — every reference, assignment
|
|
327
|
+
* targets and the typed-global list included — or the refusal. The tree is final here, so what the
|
|
328
|
+
* source name would collide with is all in it: every name its body mentions, callees included,
|
|
329
|
+
* plus its parameters, locals and own name. */
|
|
330
|
+
export function nameLocalStatics(sfn: SFn, objs: LocalObjects): SFn | { symbol: string; refused: string } {
|
|
331
|
+
const taken = takenNames(sfn);
|
|
332
|
+
for (const g of sfn.globals ?? []) {
|
|
333
|
+
taken.add(g.name);
|
|
334
|
+
}
|
|
335
|
+
const bound = new Set([...sfn.params, ...sfn.locals].map((x) => x.name));
|
|
336
|
+
const callees = new Set<string>([sfn.name]);
|
|
337
|
+
for (const e of walkExprs(sfn.body)) {
|
|
338
|
+
if (e.k === 'call' && typeof e.fn === 'string') {
|
|
339
|
+
callees.add(e.fn);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const rename = new Map<string, string>();
|
|
343
|
+
const bySource = new Map<string, string>();
|
|
344
|
+
for (const obj of objs.values()) {
|
|
345
|
+
const say = (why: string) => ({ symbol: obj.symbol, refused: `whose source name '${obj.name}' ${why}` });
|
|
346
|
+
const other = bySource.get(obj.name);
|
|
347
|
+
if (other !== undefined) {
|
|
348
|
+
return say(`another static here ('${other}') also has — one block cannot declare both`);
|
|
349
|
+
}
|
|
350
|
+
if (callees.has(obj.name) || taken.has(obj.name)) {
|
|
351
|
+
const what = callees.has(obj.name) ? 'a function' : bound.has(obj.name) ? 'a parameter or local' : 'a global';
|
|
352
|
+
return say(`is also ${what} this one names — the static would hide it`);
|
|
353
|
+
}
|
|
354
|
+
bySource.set(obj.name, obj.symbol);
|
|
355
|
+
rename.set(obj.symbol, obj.name);
|
|
356
|
+
}
|
|
357
|
+
const expr = (e: Expr): Expr => {
|
|
358
|
+
const r = mapExprChildren(e, expr);
|
|
359
|
+
return (r.k === 'var' || r.k === 'addr' || r.k === 'postincr') && rename.has(r.name)
|
|
360
|
+
? { ...r, name: rename.get(r.name)! }
|
|
361
|
+
: r;
|
|
362
|
+
};
|
|
363
|
+
const assigns = (s: Stmt): Stmt => {
|
|
364
|
+
const r = mapStmtLists(s, (list) => list.map(assigns));
|
|
365
|
+
if (r.k === 'for') {
|
|
366
|
+
return { ...r, init: assigns(r.init), inc: assigns(r.inc) };
|
|
367
|
+
}
|
|
368
|
+
return r.k === 'assign' && rename.has(r.name) ? { ...r, name: rename.get(r.name)! } : r;
|
|
369
|
+
};
|
|
370
|
+
return {
|
|
371
|
+
...sfn,
|
|
372
|
+
body: sfn.body.map((s) => assigns(mapStmtExprs(s, expr))),
|
|
373
|
+
...(sfn.globals
|
|
374
|
+
? {
|
|
375
|
+
globals: sfn.globals
|
|
376
|
+
.map((g) => ({ ...g, name: rename.get(g.name) ?? g.name }))
|
|
377
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
378
|
+
}
|
|
379
|
+
: {}),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
// • `sitesOf` widens where a loop emitter may MOVE a copy, but the read side does not follow. For
|
|
87
87
|
// a block parameter `clobbers` falls to `liveIn` of the widened site, and a parameter's SSA def
|
|
88
88
|
// point is its own block's entry, not the position the emitter actually writes it at — so a
|
|
89
|
-
// header definition is not seen to clobber a value the sink copies
|
|
89
|
+
// header definition is not seen to clobber a value the sink copies into the body. The
|
|
90
90
|
// slot that is actually sunk is unreachable (its exit arg is a header parameter, which
|
|
91
91
|
// `loop-escape` rejects); reaching it needs a second, non-loop predecessor of the exit block
|
|
92
92
|
// passing the header's definition into the same slot, and no input has been built that does.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// asmlift structurer — whether a test that READS MEMORY, and whose two arms spell the SAME
|
|
2
|
+
// statements, can be left out of the C. It decides nothing (both outcomes run the same code next —
|
|
3
|
+
// usually none, or the same edge copy into a join), and leaving it out must drop no access the
|
|
4
|
+
// machine made on that path. Without this the `if` reaches `l3/tailmerge.ts` and `l3/dce.ts`,
|
|
5
|
+
// which hoist the common statements and keep a condition that reads memory as a bare statement
|
|
6
|
+
// (`a1[14] >= 1;`), because they cannot tell a read the machine made there from one it made
|
|
7
|
+
// elsewhere.
|
|
8
|
+
//
|
|
9
|
+
// The IR can. The test is left out when every memory read in its operand cone is a load whose
|
|
10
|
+
// value something OUTSIDE the cone also reads, on the test's own path: a reader in a block that
|
|
11
|
+
// dominates the test's, or one that post-dominates it, and inside exactly the loops the test is
|
|
12
|
+
// in. The C then performs that read as often as the test runs, so the bare statement would be a
|
|
13
|
+
// second read of a value the machine loaded once. A reader on a SIBLING path is not one:
|
|
14
|
+
// `if (a1 != 0) *a2 = *a0; else *a0 < 0;` reads `*a0` on both paths, and without its test the
|
|
15
|
+
// `a1 == 0` path reads nothing. Nor is a reader after the loop the test is in: it post-dominates
|
|
16
|
+
// the test and runs once, where the test and its load run on every iteration, so a volatile
|
|
17
|
+
// register read N times would be read once. A load only this test
|
|
18
|
+
// reads is the machine's one access there and keeps the statement, as does a call or other effect
|
|
19
|
+
// in the cone. A `volatile` load needs nothing more: an on-path reader already performs the one
|
|
20
|
+
// access the machine made, and keeping the test beside it would add a second.
|
|
21
|
+
//
|
|
22
|
+
// A test that reads NO memory is left to the passes after this one: an empty `if` over registers
|
|
23
|
+
// is one `l3/dce.ts` already deletes, and one whose two arms are the same non-empty statements
|
|
24
|
+
// prints as the lift spelled it. Collapsing those too was measured behaviour-preserving (9 more
|
|
25
|
+
// GameCube functions change, 0 benchmark rows) and is left out because nothing in them is read
|
|
26
|
+
// twice.
|
|
27
|
+
//
|
|
28
|
+
// Where the shape comes from: CodeWarrior folds a `case` label into `default:` and keeps its test,
|
|
29
|
+
// so `case 1: … case 0: default: …` dispatches `beq case1; bge default; b default` (mwcc_242_81;
|
|
30
|
+
// ac-decomp mTG_select_tag_decide_needlework, and aSNMgr_set_appear_info_guest, whose two edges
|
|
31
|
+
// carry the same merge value); pikmin UpdateMgr::removeClient keeps an assert's `blt`, whose other
|
|
32
|
+
// side held only a dead load.
|
|
33
|
+
//
|
|
34
|
+
// RENDERING ONLY, and deliberately not a CFG rewrite ahead of the raising tower: a pre-recovery
|
|
35
|
+
// pass turning the same `cond_br` into a `br` costs `af:adds:ido7.1` 6/41 -> 18/41. ido guards a
|
|
36
|
+
// `div` with `bne v0, at` over the numerator, the MIPS lift keeps that trap check as a branch whose
|
|
37
|
+
// arms meet, and that compare is the out-of-block reader that offers `/escape-home` on the
|
|
38
|
+
// numerator, the spelling that wins.
|
|
39
|
+
import type { Block, Op, Value } from '../ir/core';
|
|
40
|
+
import { effectful } from '../ir/discipline';
|
|
41
|
+
import { opSig } from '../ir/opcodes';
|
|
42
|
+
import type { UseSite } from './analysis';
|
|
43
|
+
import type { NaturalLoop } from './loops';
|
|
44
|
+
|
|
45
|
+
export interface RedundantTestDeps {
|
|
46
|
+
defs: Map<Value, Op>;
|
|
47
|
+
useSitesOf: Map<Value, UseSite[]>;
|
|
48
|
+
dom: Map<Block, Set<Block>>;
|
|
49
|
+
ipdom: Map<Block, Block | null>;
|
|
50
|
+
loops: Iterable<NaturalLoop>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Whether `branch`, the terminator of `at`, only re-reads what the machine read on its path. */
|
|
54
|
+
export function testRereadsOnly(branch: Op, at: Block, deps: RedundantTestDeps): boolean {
|
|
55
|
+
const { defs, useSitesOf, dom, ipdom } = deps;
|
|
56
|
+
const loops = [...deps.loops];
|
|
57
|
+
const cone = new Set<Op>();
|
|
58
|
+
const loads: Op[] = [];
|
|
59
|
+
const work = [...branch.operands];
|
|
60
|
+
for (let v = work.pop(); v !== undefined; v = work.pop()) {
|
|
61
|
+
const op = defs.get(v);
|
|
62
|
+
if (op === undefined || cone.has(op)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (effectful(op)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
cone.add(op);
|
|
69
|
+
if (opSig(op.opcode)?.reads === true) {
|
|
70
|
+
loads.push(op);
|
|
71
|
+
}
|
|
72
|
+
work.push(...op.operands);
|
|
73
|
+
}
|
|
74
|
+
// Dominance and post-dominance say a reader runs whenever the test does, not as often: a loop
|
|
75
|
+
// between them repeats one and not the other.
|
|
76
|
+
const sameLoops = (b: Block): boolean => loops.every((l) => l.body.has(b) === l.body.has(at));
|
|
77
|
+
const onPath = (b: Block): boolean => {
|
|
78
|
+
if (!sameLoops(b)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (dom.get(at)?.has(b)) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
for (let p = ipdom.get(at); p; p = ipdom.get(p)) {
|
|
85
|
+
if (p === b) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
};
|
|
91
|
+
return (
|
|
92
|
+
loads.length > 0 &&
|
|
93
|
+
loads.every((ld) =>
|
|
94
|
+
ld.results.some((r) =>
|
|
95
|
+
(useSitesOf.get(r) ?? []).some((u) => u.op !== branch && !cone.has(u.op) && onPath(u.blk)),
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
}
|