@asmlift/core 0.8.1 → 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 +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- 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 +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +25 -7
package/src/raise/structs.ts
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
// gap fields the compiler had but this function never touched)
|
|
19
19
|
// base @ {off2 w4} -> LOUD decline (a 4-byte field at offset 2 is not 4-aligned —
|
|
20
20
|
// natural C alignment cannot place it there)
|
|
21
|
+
// base @ {off0 w4, off2 w2} -> struct { union { s32 word; s16 half[2]; } field_0; } (the same
|
|
22
|
+
// bytes at two widths — `buildUnionStruct`)
|
|
21
23
|
// base @ {off0 w4, off4 w4} -> array (uniform stride — untouched)
|
|
22
24
|
// base @ {off8 w4} -> array (single aligned access — no struct evidence)
|
|
23
25
|
// base @ aload(index) -> array (variable index — untouched)
|
|
@@ -42,19 +44,105 @@
|
|
|
42
44
|
// struct reproduces the observed offsets byte-for-byte and is self-describing (the same
|
|
43
45
|
// discipline raise/struct-arrays.ts withPadding uses). Each accessed field must still be
|
|
44
46
|
// naturally aligned to ITS OWN width (`off % width === 0`) — a genuinely packed layout (a field
|
|
45
|
-
// at an offset natural C alignment could not place it at) is rejected LOUD
|
|
46
|
-
//
|
|
47
|
-
|
|
47
|
+
// at an offset natural C alignment could not place it at) is rejected LOUD. An overlap is a union
|
|
48
|
+
// member on a base whose layout only this function's accesses describe, and no struct at all on
|
|
49
|
+
// one whose address is declared elsewhere (`recognizeStructs`).
|
|
50
|
+
import { constAddressOf, constDisplacementOf, globalCellOf } from '../ir/alias';
|
|
51
|
+
import { Fn, Op, Value, defOpMap } from '../ir/core';
|
|
52
|
+
import { placedAt } from '../ir/discipline';
|
|
48
53
|
import { nextStructIndex } from '../ir/struct-names';
|
|
49
|
-
import { IrType, StructField, T, scalarTypeForAccess } from '../ir/types';
|
|
54
|
+
import { IrType, StructField, T, UNION_VIEW_NAMES, intWidth, scalarTypeForAccess, unionViewSigns } from '../ir/types';
|
|
50
55
|
import type { StructType } from '../l3/ast';
|
|
51
|
-
import {
|
|
56
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
57
|
+
import { RaiseUnsupportedError, StructOverlapError } from './errors';
|
|
58
|
+
import { storeSigned } from './storesign';
|
|
52
59
|
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
/** A single observed constant-offset access to a base: the op itself and its program point, its
|
|
61
|
+
* byte offset, its access width (bytes), and the signedness a recovered field would take from it.
|
|
62
|
+
*
|
|
63
|
+
* SHARED WITH raise/truncload.ts, which folds a narrow read into a cast of a wider one before this
|
|
64
|
+
* pass runs. The two ask the same question of a function — "what does this base's access set look
|
|
65
|
+
* like" — and a second copy of the walk would let them come to disagree about the answer while
|
|
66
|
+
* each looked right on its own.
|
|
67
|
+
*
|
|
68
|
+
* SIGNEDNESS ON THE STORE HALF IS A POLICY, not a reading, unless raise/storesign.ts stamped the
|
|
69
|
+
* store from the constant it stores: otherwise a word store makes an `s32` field where a narrower
|
|
70
|
+
* one makes an unsigned one. `buildStruct` then prefers a LOAD's own signedness at the same offset,
|
|
71
|
+
* which is the reading, over either. raise/truncload.ts never sees this field — its
|
|
72
|
+
* `covering-store` rule refuses a store cover before the signedness is used. */
|
|
73
|
+
export interface Access {
|
|
74
|
+
op: Op;
|
|
75
|
+
/** index into `fn.blocks`, for the dominance question raise/truncload.ts asks. */
|
|
76
|
+
block: number;
|
|
77
|
+
/** index into that block's `ops`, as they stood when this was collected. */
|
|
78
|
+
at: number;
|
|
55
79
|
off: number;
|
|
56
80
|
width: number;
|
|
57
81
|
signed: boolean;
|
|
82
|
+
isLoad: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A store whose signedness raise/storesign.ts read off the constant it stores. */
|
|
86
|
+
const isStamped = (a: Access): boolean => !a.isLoad && a.op.attrs.signed !== undefined;
|
|
87
|
+
|
|
88
|
+
/** What one walk of a function yields about its constant-offset memory accesses. */
|
|
89
|
+
export interface ConstOffsetAccesses {
|
|
90
|
+
/** every base's constant-offset accesses, in program order. */
|
|
91
|
+
accessesOf: Map<Value, Access[]>;
|
|
92
|
+
/** the bases, in FIRST-APPEARANCE order → deterministic struct names. */
|
|
93
|
+
order: Value[];
|
|
94
|
+
/** bases a VARIABLE-index access also reaches (`aload`/`astore`) — arrays, never struct-recovered. */
|
|
95
|
+
arrayBases: Set<Value>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Collect every base's constant-offset accesses, plus the bases a variable-index access reaches.
|
|
99
|
+
* ONE walk, two consumers: `recognizeStructs` below and raise/truncload.ts. */
|
|
100
|
+
export function constOffsetAccesses(fn: Fn): ConstOffsetAccesses {
|
|
101
|
+
const accessesOf = new Map<Value, Access[]>();
|
|
102
|
+
const arrayBases = new Set<Value>();
|
|
103
|
+
const order: Value[] = [];
|
|
104
|
+
const note = (base: Value, a: Access) => {
|
|
105
|
+
let list = accessesOf.get(base);
|
|
106
|
+
if (!list) {
|
|
107
|
+
list = [];
|
|
108
|
+
accessesOf.set(base, list);
|
|
109
|
+
order.push(base);
|
|
110
|
+
}
|
|
111
|
+
list.push(a);
|
|
112
|
+
};
|
|
113
|
+
fn.blocks.forEach((b, block) => {
|
|
114
|
+
(b.ops as Op[]).forEach((op, at) => {
|
|
115
|
+
switch (op.opcode) {
|
|
116
|
+
case 'load':
|
|
117
|
+
note(op.operands[0], {
|
|
118
|
+
op,
|
|
119
|
+
block,
|
|
120
|
+
at,
|
|
121
|
+
off: op.attrs.off as number,
|
|
122
|
+
width: op.attrs.width as number,
|
|
123
|
+
signed: op.attrs.signed as boolean,
|
|
124
|
+
isLoad: true,
|
|
125
|
+
});
|
|
126
|
+
break;
|
|
127
|
+
case 'store':
|
|
128
|
+
note(op.operands[0], {
|
|
129
|
+
op,
|
|
130
|
+
block,
|
|
131
|
+
at,
|
|
132
|
+
off: op.attrs.off as number,
|
|
133
|
+
width: op.attrs.width as number,
|
|
134
|
+
signed: storeSigned(op, op.attrs.width as number),
|
|
135
|
+
isLoad: false,
|
|
136
|
+
});
|
|
137
|
+
break;
|
|
138
|
+
case 'aload':
|
|
139
|
+
case 'astore':
|
|
140
|
+
arrayBases.add(op.operands[0]);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
return { accessesOf, order, arrayBases };
|
|
58
146
|
}
|
|
59
147
|
|
|
60
148
|
// Natural C size/alignment of a recovered scalar field type (all fields here are int/ptr ≤ 4 bytes,
|
|
@@ -78,27 +166,71 @@ function isArray(accesses: Access[]): boolean {
|
|
|
78
166
|
|
|
79
167
|
/** Build the struct type for a base whose accesses are NOT array-shaped. Unaccessed leading/
|
|
80
168
|
* interior gaps are FILLED with `u8[N]` pads so the declared struct reproduces the observed
|
|
81
|
-
* offsets. Throws LOUD only on a layout
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
|
|
169
|
+
* offsets. Throws LOUD only on a layout no plain struct reproduces: two accesses overlapping in
|
|
170
|
+
* bytes (same offset with differing widths, OR distinct offsets whose ranges collide), or a field
|
|
171
|
+
* at an offset its own natural alignment could not place it at (a PACKED layout).
|
|
172
|
+
*
|
|
173
|
+
* ONE CLASS OF OVERLAP NEVER REACHES HERE: a narrow LOAD covering the low-order end of a wider
|
|
174
|
+
* access that ran before it on every path is a cast of that access rather than a second field, and
|
|
175
|
+
* raise/truncload.ts folds it into one before this pass runs. What is left is what the asm does
|
|
176
|
+
* not settle — a narrow STORE, a read above the low-order end, a cell at a constant address, and a
|
|
177
|
+
* cover on a path the narrow read does not run — and every one of them throws here.
|
|
178
|
+
*
|
|
179
|
+
* A THROW IS NOT THE FUNCTION'S VERDICT, AND WHICH THROW IT IS DECIDES THAT. Four throw sites
|
|
180
|
+
* below reject a layout and they fall into two CLASSES, which is the split a caller reads:
|
|
181
|
+
* • `StructOverlapError` — two accesses whose byte ranges collide. THREE of the four sites raise
|
|
182
|
+
* it, each message ending "unions not modelled": a second access
|
|
183
|
+
* at an offset already taken with a different width, a second LOAD of a narrow cell with the
|
|
184
|
+
* other extension (one field would spell one of the two reads wrong), and one whose range
|
|
185
|
+
* straddles the field before it (the `aligned > f.off` arm, which is the only one that can see
|
|
186
|
+
* `{s32@0, s16@2}`).
|
|
187
|
+
* Each access alone is spellable at its own offset and width, so a base whose ADDRESS is
|
|
188
|
+
* declared outside this function's access set can keep its untyped spelling and lift
|
|
189
|
+
* (`recognizeStructs` below). There is no struct to synthesize, and nothing is lost by not
|
|
190
|
+
* synthesizing one. Any other base is declared with a union member, or a `castViews` field
|
|
191
|
+
* where the union would not fit (`buildUnionStruct`).
|
|
192
|
+
* • `RaiseUnsupportedError` — a field at an offset its own natural alignment could not place it
|
|
193
|
+
* at. This one has NO per-access spelling downstream: an access at `off` under a wider access's
|
|
194
|
+
* width folds to the element index `off / width` (structure/structure.ts), which for a
|
|
195
|
+
* misaligned pair is a FRACTION and not C. It declines whatever the base is. */
|
|
196
|
+
function buildStruct(name: string, accesses: Access[], extensionViews = true): IrType {
|
|
86
197
|
// One field per distinct offset; a load's signedness wins over a store's (more information).
|
|
87
198
|
const byOff = new Map<number, Access>();
|
|
199
|
+
// The extension of every LOAD seen at an offset, kept apart from `byOff`, whose access can be a
|
|
200
|
+
// store (kept over an unsigned load): the verdict must not hang on the order of the accesses.
|
|
201
|
+
const loadedSigned = new Map<number, boolean>();
|
|
88
202
|
for (const a of accesses) {
|
|
89
203
|
const prev = byOff.get(a.off);
|
|
204
|
+
const seen = a.isLoad ? loadedSigned.get(a.off) : undefined;
|
|
205
|
+
if (a.isLoad && seen === undefined) {
|
|
206
|
+
loadedSigned.set(a.off, a.signed);
|
|
207
|
+
}
|
|
90
208
|
if (!prev) {
|
|
91
209
|
byOff.set(a.off, a);
|
|
92
210
|
continue;
|
|
93
211
|
}
|
|
94
212
|
if (prev.width !== a.width) {
|
|
95
|
-
throw new
|
|
213
|
+
throw new StructOverlapError(
|
|
96
214
|
`cannot recover struct '${name}': overlapping fields at offset ${a.off} (widths ${prev.width} and ${a.width}) — unions not modelled`,
|
|
97
215
|
);
|
|
98
216
|
}
|
|
99
|
-
|
|
217
|
+
// Two LOADS of one narrow cell with different extensions (`ldrh` and `ldrsh`) are two views of
|
|
218
|
+
// it, and one field types only one of them: the other read would be spelled through the wrong
|
|
219
|
+
// extension, a silent wrong (`u16` read as `s16` sign-extends). It is the same overlap class as
|
|
220
|
+
// two widths at one offset, so it goes the same way: `buildUnionStruct` declares the cell's two
|
|
221
|
+
// views, a declared address keeps its per-access casts. A word has no extension to disagree on.
|
|
222
|
+
// `extensionViews` false switches off this overlap class alone (the no-layout gate says why).
|
|
223
|
+
if (extensionViews && seen !== undefined && a.signed !== seen && a.width < 4) {
|
|
224
|
+
throw new StructOverlapError(
|
|
225
|
+
`cannot recover struct '${name}': the ${a.width}-byte cell at offset ${a.off} is loaded both sign- and zero-extended — unions not modelled`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
// A load reads its signedness, and a stamped store only stands in for one: a load replaces a
|
|
229
|
+
// stamped store, and otherwise the signed view wins, but never a stamped store's over a load's.
|
|
230
|
+
const replace = isStamped(prev) && a.isLoad ? true : a.signed && !prev.signed && !(isStamped(a) && prev.isLoad);
|
|
231
|
+
if (replace) {
|
|
100
232
|
byOff.set(a.off, a);
|
|
101
|
-
}
|
|
233
|
+
}
|
|
102
234
|
}
|
|
103
235
|
const dataFields: StructField[] = [...byOff.values()]
|
|
104
236
|
.sort((x, y) => x.off - y.off)
|
|
@@ -108,31 +240,38 @@ function buildStruct(name: string, accesses: Access[]): IrType {
|
|
|
108
240
|
// cannot reproduce, so it is rejected LOUD (never a silently-wrong struct). The GAP before a
|
|
109
241
|
// field (an unaccessed leading/interior member) is legal: it is filled with a `u8[N]` pad below.
|
|
110
242
|
for (const f of dataFields) {
|
|
111
|
-
|
|
112
|
-
if (f.off % sizeAlign(sz) !== 0) {
|
|
113
|
-
throw new RaiseUnsupportedError(
|
|
114
|
-
`cannot recover struct '${name}': field at offset ${f.off} (width ${sz}) is not naturally aligned — packed layout not modelled`,
|
|
115
|
-
);
|
|
116
|
-
}
|
|
243
|
+
refusePacked(name, f.off, fieldSize(f.type));
|
|
117
244
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
245
|
+
return placeFields(name, dataFields);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function refusePacked(name: string, off: number, size: number): void {
|
|
249
|
+
if (off % sizeAlign(size) !== 0) {
|
|
250
|
+
throw new RaiseUnsupportedError(
|
|
251
|
+
`cannot recover struct '${name}': field at offset ${off} (width ${size}) is not naturally aligned — packed layout not modelled`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Seat `dataFields` (sorted by offset) under natural C alignment, inserting an explicit `u8[N]`
|
|
257
|
+
* PAD only for a gap the alignment itself does NOT already cover (the same self-describing
|
|
258
|
+
* discipline as raise/struct-arrays.ts withPadding). For each field, `aligned` = where natural C
|
|
259
|
+
* alignment would put it after the running cursor:
|
|
260
|
+
* • aligned === off — natural padding lands it exactly (`{s8@0, s32@4}`): no explicit pad,
|
|
261
|
+
* C's own inter-field alignment reproduces the layout.
|
|
262
|
+
* • aligned < off — a leading/interior gap alignment can't fill (`{s16@2, s32@4}`, byte 0–1
|
|
263
|
+
* never read): insert a `u8[off - cursor]` pad so the field lands exactly.
|
|
264
|
+
* • aligned > off — the field's offset precedes where alignment would force it: it OVERLAPS
|
|
265
|
+
* the prior field (`{s32@0, s16@2}` — a union view the same-offset byOff check cannot see):
|
|
266
|
+
* reject LOUD, never a silently-mislaid field. */
|
|
267
|
+
function placeFields(name: string, dataFields: StructField[]): IrType {
|
|
129
268
|
const fields: StructField[] = [];
|
|
130
269
|
let cursor = 0;
|
|
131
270
|
let pad = 0;
|
|
132
271
|
for (const f of dataFields) {
|
|
133
|
-
const aligned = roundUp(cursor, sizeAlign(
|
|
272
|
+
const aligned = roundUp(cursor, sizeAlign(fieldSize(f.type)));
|
|
134
273
|
if (aligned > f.off) {
|
|
135
|
-
throw new
|
|
274
|
+
throw new StructOverlapError(
|
|
136
275
|
`cannot recover struct '${name}': field at offset ${f.off} overlaps the prior field (aligned to ${aligned}) — unions not modelled`,
|
|
137
276
|
);
|
|
138
277
|
}
|
|
@@ -140,69 +279,467 @@ function buildStruct(name: string, accesses: Access[]): IrType {
|
|
|
140
279
|
fields.push({ off: cursor, type: T.array(T.u(8), f.off - cursor), name: `_pad${pad++}` });
|
|
141
280
|
}
|
|
142
281
|
fields.push(f);
|
|
143
|
-
cursor = f.off +
|
|
282
|
+
cursor = f.off + fieldSize(f.type);
|
|
144
283
|
}
|
|
145
284
|
return T.struct(name, fields);
|
|
146
285
|
}
|
|
147
286
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
287
|
+
/** The struct for a base whose accesses OVERLAP — `buildStruct` refused it with
|
|
288
|
+
* `StructOverlapError` — with each overlap declared as a UNION member: the same bytes read or
|
|
289
|
+
* written at more than one width are views of one cell, and a union is the C that says so.
|
|
290
|
+
*
|
|
291
|
+
* NOT A CAST, and the compiler is why. `((u16 *)p)[k]` beside `*p` spells the same two accesses,
|
|
292
|
+
* but a cast-punned access is outside C's aliasing rules and agbcc (-O2) applies them: a narrow
|
|
293
|
+
* read, a word store and the same narrow read again compiles, through casts, to ONE `ldrh` whose
|
|
294
|
+
* value is reused, where the union spelling reloads after the `str` — as the asm the union was
|
|
295
|
+
* compiled from does (`synthetic:ureread`). gcc2.7.2kmc, ido7.1 and mwcc_242_81 reload for both.
|
|
296
|
+
* The direction is one-way: a union access aliases every view of its cell, so the union spelling
|
|
297
|
+
* keeps every access the asm performed and the cast cannot be the better candidate — which is why
|
|
298
|
+
* this is the default and not a variation.
|
|
299
|
+
*
|
|
300
|
+
* THE LAYOUT. Every access must be naturally aligned (a packed one declines, as in
|
|
301
|
+
* `buildStruct`), and access widths are powers of two, so an aligned access never straddles the
|
|
302
|
+
* boundary of a wider aligned one: two accesses overlap only when the wider CONTAINS the narrower.
|
|
303
|
+
* A cell is therefore one widest access and every access inside it, and it gets a VIEW per width —
|
|
304
|
+
* `word`, `half`, `byte` — and, for a narrow width LOADED with both extensions, per extension —
|
|
305
|
+
* `uhalf` and `shalf` — because a view's type IS its extension and one view for both reads one of
|
|
306
|
+
* them wrong. A store carries no extension and writes through its width's only view, or the
|
|
307
|
+
* unsigned one where there are two. A view that holds
|
|
308
|
+
* one element at the cell's start is a scalar; any other is an array reaching the furthest
|
|
309
|
+
* element accessed (`u16 half[2]` for the halfword at +2). A cell with ONE view is a plain field;
|
|
310
|
+
* any other is a member `field_<off>` holding a union of its views — including a single-width cell
|
|
311
|
+
* loaded both ways, which `buildStruct` hands over as an overlap even on a base with no other one.
|
|
312
|
+
*
|
|
313
|
+
* THE COMPILER DECIDES THE UNION'S SIZE, not its widest view. `aggregateBoundary` is the size it
|
|
314
|
+
* aligns and rounds every struct and union to (target.ts `compilerBehaviors.aggregateBoundary`):
|
|
315
|
+
* agbcc makes `union { u16 h; u8 b; }` four bytes, four-aligned, where the other compilers make
|
|
316
|
+
* it two. A union that boundary would move — seated off it, or with the next field inside its
|
|
317
|
+
* rounded size — is never declared, since it would mislay every access at or after it. Its cell
|
|
318
|
+
* is a plain field of its widest view instead, read at its other views through casts, where those
|
|
319
|
+
* casts alias it as the union would, and otherwise it declines (`castViewCell`). An UNMEASURED
|
|
320
|
+
* compiler (undefined) declines every union, since no boundary is safe to assume: one too small
|
|
321
|
+
* mislays the fields after the union on agbcc, one too large drops the pad in front of them
|
|
322
|
+
* everywhere else.
|
|
323
|
+
*
|
|
324
|
+
* A cell at a width no view is named for declines; the frontends emit none. */
|
|
325
|
+
function buildUnionStruct(structName: string, accesses: Access[], aggregateBoundary: number | undefined): IrType {
|
|
326
|
+
for (const a of accesses) {
|
|
327
|
+
refusePacked(structName, a.off, a.width);
|
|
328
|
+
}
|
|
329
|
+
const clusters: Access[][] = [];
|
|
330
|
+
for (const a of [...accesses].sort((x, y) => x.off - y.off || y.width - x.width)) {
|
|
331
|
+
const cur = clusters.at(-1);
|
|
332
|
+
if (cur !== undefined && a.off < cur[0].off + cur[0].width) {
|
|
333
|
+
cur.push(a);
|
|
334
|
+
} else {
|
|
335
|
+
clusters.push([a]);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const dataFields = clusters.map((c): StructField => {
|
|
339
|
+
const start = c[0].off;
|
|
340
|
+
const members = [...new Set(c.map((a) => a.width))].flatMap((width): StructField[] => {
|
|
341
|
+
const group = c.filter((a) => a.width === width);
|
|
342
|
+
const name = UNION_VIEW_NAMES[width];
|
|
343
|
+
if (name === undefined) {
|
|
344
|
+
throw new RaiseUnsupportedError(
|
|
345
|
+
`cannot recover struct '${structName}': no union view for a ${width}-byte access at offset ${group[0].off}`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
const views = unionViewSigns(
|
|
349
|
+
width,
|
|
350
|
+
group.map((a) => ({ isLoad: a.isLoad, signed: a.signed, stamped: isStamped(a) })),
|
|
351
|
+
);
|
|
352
|
+
const viewOf = (a: Access): boolean => (views.length === 1 ? views[0] : a.isLoad ? a.signed : false);
|
|
353
|
+
return views.map((signed): StructField => {
|
|
354
|
+
const reads = group.filter((a) => viewOf(a) === signed);
|
|
355
|
+
const count = Math.max(...reads.map((a) => a.off - start)) / width + 1;
|
|
356
|
+
const elem = scalarTypeForAccess(width, signed);
|
|
357
|
+
const viewName = views.length > 1 ? `${signed ? 's' : 'u'}${name}` : name;
|
|
358
|
+
return { off: 0, type: count === 1 ? elem : T.array(elem, count), name: viewName };
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
if (members.length === 1) {
|
|
362
|
+
return { off: start, type: members[0].type, name: `field_${start}` };
|
|
363
|
+
}
|
|
364
|
+
if (aggregateBoundary === undefined) {
|
|
365
|
+
throw new RaiseUnsupportedError(
|
|
366
|
+
`cannot recover struct '${structName}': the union at offset ${start} needs this compiler's aggregate boundary, which is unmeasured`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
return { off: start, type: T.union(members, aggregateBoundary), name: `field_${start}` };
|
|
370
|
+
});
|
|
371
|
+
const fitted = dataFields.map((f, i): StructField => {
|
|
372
|
+
if (f.type.kind !== 'union') {
|
|
373
|
+
return f;
|
|
374
|
+
}
|
|
375
|
+
const next = dataFields[i + 1];
|
|
376
|
+
if (f.off % f.type.size === 0 && (next === undefined || next.off >= f.off + f.type.size)) {
|
|
377
|
+
return f;
|
|
378
|
+
}
|
|
379
|
+
const widest = castViewCell(f.type.members);
|
|
380
|
+
if (widest === undefined) {
|
|
381
|
+
throw new RaiseUnsupportedError(
|
|
382
|
+
`cannot recover struct '${structName}': the union at offset ${f.off} does not fit this compiler's ${aggregateBoundary}-byte aggregate boundary`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
return { off: f.off, type: widest, name: f.name, castViews: true };
|
|
386
|
+
});
|
|
387
|
+
return placeFields(structName, fitted);
|
|
151
388
|
}
|
|
152
389
|
|
|
153
|
-
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
390
|
+
/** The type a union's cell is declared at when the union itself cannot be laid out, or undefined
|
|
391
|
+
* when its views cannot be spelled as casts of that one field. The field is the widest view, the
|
|
392
|
+
* unsigned one where that width has two; every other view must be ALIAS-TRANSPARENT to it, so a cast access aliases the field exactly as a
|
|
393
|
+
* union member would: a 1-byte view (a character type, alias set 0 — c-common.c
|
|
394
|
+
* `c_get_alias_set`) or the field's own width with the other extension (s16 and u16 share one
|
|
395
|
+
* set). Any other view is refused: a `u16` read of a word cell through a cast is reused across a
|
|
396
|
+
* word store on agbcc, where the union reloads (`synthetic:ureread`).
|
|
397
|
+
*
|
|
398
|
+
* NO MEASURED COMPILER REACHES THE REFUSAL. Every target but agbcc has a 1-byte boundary, where a
|
|
399
|
+
* union of aligned views is never larger than its widest one and always fits. On agbcc a cell
|
|
400
|
+
* the size of the boundary is aligned and fits, a misaligned one is `refusePacked`'s, so only a
|
|
401
|
+
* cell of 2 bytes or fewer gets here — and its views are bytes or its own width. The refusal is
|
|
402
|
+
* the guard for a compiler whose boundary is wider, which the test with a boundary of 8 stands in
|
|
403
|
+
* for. */
|
|
404
|
+
function castViewCell(members: readonly StructField[]): IrType | undefined {
|
|
405
|
+
const width = (m: StructField): number => intWidth(m.type.kind === 'array' ? m.type.elem : m.type) ?? 0;
|
|
406
|
+
const widest = Math.max(...members.map(width));
|
|
407
|
+
const cells = members.filter((m) => width(m) === widest && m.type.kind === 'int');
|
|
408
|
+
const cell = cells.find((m) => m.type.kind === 'int' && !m.type.signed) ?? cells[0];
|
|
409
|
+
const transparent = members.every((m) => width(m) === 8 || width(m) === widest);
|
|
410
|
+
return transparent && cell !== undefined ? cell.type : undefined;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Size in bytes of a recovered field's type (int width/8; pointer is word-sized 4; a union the
|
|
414
|
+
* size its compiler gives it). */
|
|
415
|
+
function fieldSize(t: IrType): number {
|
|
416
|
+
switch (t.kind) {
|
|
417
|
+
case 'int':
|
|
418
|
+
return t.width / 8;
|
|
419
|
+
case 'array':
|
|
420
|
+
return t.count * fieldSize(t.elem);
|
|
421
|
+
case 'union':
|
|
422
|
+
return t.size;
|
|
423
|
+
default:
|
|
424
|
+
return 4;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export interface DisplacedOptions {
|
|
429
|
+
/** leave every STORE on its materialised address, folding only the loads (`/far-cast`) */
|
|
430
|
+
keepStores?: boolean;
|
|
431
|
+
/** called when a lift without `keepStores` folds a store, which is what offers `/far-cast` */
|
|
432
|
+
onFoldedStore?: () => void;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** The target facts the fold reads (target.ts `compilerBehaviors`). */
|
|
436
|
+
export interface DisplacedBehaviors {
|
|
437
|
+
farMemberStoreOrder?: boolean;
|
|
438
|
+
farMemberBasePerAccess?: { past: number };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** One materialised base, as `DISPLACED_BASE_GATES` judges it. */
|
|
442
|
+
export interface DisplacedBase {
|
|
443
|
+
base: Value;
|
|
444
|
+
accesses: readonly Access[];
|
|
445
|
+
disp: { root: Value; k: number; chain: Value[] };
|
|
446
|
+
defs: Map<Value, Op>;
|
|
447
|
+
arrayBases: Set<Value>;
|
|
448
|
+
/** does every use of `v` go to `next` (a link of the chain), or, at the chain's head, to the
|
|
449
|
+
* BASE operand of a constant-offset load/store? */
|
|
450
|
+
onlyFeeds: (v: Value, next: Value | undefined) => boolean;
|
|
451
|
+
behaviors: DisplacedBehaviors;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** One root and every access the fold would move onto it, as `DISPLACED_ROOT_GATES` judges it. */
|
|
455
|
+
export interface DisplacedRoot {
|
|
456
|
+
root: Value;
|
|
457
|
+
/** the root's own accesses plus the moved ones at their folded offsets */
|
|
458
|
+
merged: readonly Access[];
|
|
459
|
+
/** some moved store's value is computed BEFORE its address — the member's order on a target
|
|
460
|
+
* with `farMemberStoreOrder`, which no cast or held pointer builds */
|
|
461
|
+
memberStoreOrder: boolean;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** The smallest displacement `address-constant` reads as an address rather than a member offset. */
|
|
465
|
+
const ADDRESS_CONSTANT_FLOOR = 1 << 24;
|
|
466
|
+
|
|
467
|
+
export const DISPLACED_BASE_GATES: readonly Gate<DisplacedBase>[] = [
|
|
468
|
+
{
|
|
469
|
+
id: 'not-recovery-root',
|
|
470
|
+
why: 'the root is typed already, a frame, global or literal address, or a variable-index array',
|
|
471
|
+
sound: false,
|
|
472
|
+
rejects: ({ disp: { root: p }, defs, arrayBases }) => {
|
|
473
|
+
const d = defs.get(p)?.opcode;
|
|
474
|
+
return (
|
|
475
|
+
p.type.kind !== 'unknown' ||
|
|
476
|
+
d === 'laddr' ||
|
|
477
|
+
d === 'gaddr' ||
|
|
478
|
+
d === 'const' ||
|
|
479
|
+
arrayBases.has(p) ||
|
|
480
|
+
globalCellOf(defs, p, 0) !== null ||
|
|
481
|
+
constAddressOf(defs, p, 0) !== null
|
|
482
|
+
);
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
// `*(u16 *)(x + 0x6000000)`, `*(u8 *)(0x6000000 + i * 2)`: a hardware or segment address added
|
|
487
|
+
// to an index. Read as a member it retypes the integer as a pointer to a struct with a 96 MiB
|
|
488
|
+
// pad. No struct on these machines comes near 16 MiB (GBA EWRAM is 256 KiB, N64 RDRAM 8 MiB,
|
|
489
|
+
// GameCube main RAM 24 MiB in all), and every address region a constant names starts past it.
|
|
490
|
+
id: 'address-constant',
|
|
491
|
+
why: 'a displacement of 16 MiB or more is an address constant added to an index, not a member offset',
|
|
492
|
+
sound: false,
|
|
493
|
+
rejects: ({ disp: { k } }) => Math.abs(k) >= ADDRESS_CONSTANT_FLOOR,
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
id: 'claimed-chain',
|
|
497
|
+
why: 'another struct pass already typed a value of the chain, or an indexed access reaches it',
|
|
498
|
+
sound: false,
|
|
499
|
+
rejects: ({ disp: { chain }, arrayBases }) => chain.some((v) => v.type.kind !== 'unknown' || arrayBases.has(v)),
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
id: 'held-address',
|
|
503
|
+
why: 'the address is used as a value too, so the source held it',
|
|
504
|
+
sound: false,
|
|
505
|
+
rejects: ({ disp: { chain }, onlyFeeds }) =>
|
|
506
|
+
!chain.every((v, i) => onlyFeeds(v, i === 0 ? undefined : chain[i - 1])),
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
id: 'placed-access',
|
|
510
|
+
why: 'a volatile or device-pinned access keeps the address it was written through',
|
|
511
|
+
sound: false,
|
|
512
|
+
rejects: ({ accesses }) => accesses.some((a) => placedAt(a.op) !== null),
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
id: 'negative-offset',
|
|
516
|
+
why: 'a folded offset below the root is no member',
|
|
517
|
+
sound: false,
|
|
518
|
+
rejects: ({ accesses, disp: { k } }) => accesses.some((a) => a.off + k < 0),
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
id: 'held-pointer-offsets',
|
|
522
|
+
why: 'one materialised base at two offsets is a held pointer where every member access gets its own',
|
|
523
|
+
sound: false,
|
|
524
|
+
rejects: ({ accesses, disp: { k }, behaviors: { farMemberBasePerAccess: perAccess } }) =>
|
|
525
|
+
perAccess !== undefined && Math.abs(k) > perAccess.past && new Set(accesses.map((a) => a.off)).size > 1,
|
|
526
|
+
},
|
|
527
|
+
];
|
|
528
|
+
|
|
529
|
+
export const DISPLACED_ROOT_GATES: readonly Gate<DisplacedRoot>[] = [
|
|
530
|
+
{
|
|
531
|
+
id: 'array-shaped',
|
|
532
|
+
why: 'uniform-stride or single accesses are no struct evidence, unless a store shows the member order',
|
|
533
|
+
sound: false,
|
|
534
|
+
rejects: ({ merged, memberStoreOrder }) => isArray([...merged]) && !memberStoreOrder,
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
id: 'no-layout',
|
|
538
|
+
why: 'the accesses overlap or misalign, so no plain struct lays them out',
|
|
539
|
+
sound: false,
|
|
540
|
+
// A cell loaded with both extensions lays out: `recognizeStructs` gives it a view per extension
|
|
541
|
+
// once the fold has put its reads on one base, and leaving the root unfolded would type the
|
|
542
|
+
// cell by its unfolded `ldrh` alone (agbcc's `ldrsh` has no immediate form, so every signed
|
|
543
|
+
// halfword read is a displaced one). Not for an array-shaped set, which the fold types itself,
|
|
544
|
+
// as one plain struct.
|
|
545
|
+
// KNOWN GAP (correct C): a WIDTH overlap still rejects the root, though `recognizeStructs`
|
|
546
|
+
// would lay it out too. `ldrb [r0]` beside agbcc's `mov r3, #0; ldrsh [r0, r3]` keeps the
|
|
547
|
+
// halfword on its address, `*(s16 *)((u8 *)a0 + 0)`: the right bytes, the right extension.
|
|
548
|
+
rejects: ({ merged }) => {
|
|
549
|
+
try {
|
|
550
|
+
buildStruct('Struct', [...merged], isArray([...merged]));
|
|
551
|
+
return false;
|
|
552
|
+
} catch {
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
];
|
|
558
|
+
|
|
559
|
+
/** FOLD A MATERIALISED DISPLACEMENT INTO THE ACCESS: a load/store whose base is `add(p, K)` (or a
|
|
560
|
+
* chain of constant adds/subs off `p`) becomes an access of `p` at `off + K`, so `recognizeStructs`
|
|
561
|
+
* sees it in `p`'s access set and spells it as the member it is.
|
|
562
|
+
*
|
|
563
|
+
* WHY. Every ISA has a displacement range, and a member past it is reached through an address the
|
|
564
|
+
* machine materialises first: Thumb's `adds rX, #38; strb [rX]` (a byte past 31, a half past 62,
|
|
565
|
+
* a word past 124), ido's `addiu at, a0, 32767; sb a1, 7232(at)`, KMC's and GCC 2.7.2's
|
|
566
|
+
* `lui at, 1; addu at, at, a0; sb a1, -25537(at)`, mwcc's `addis r5, r3, 1; stb r4, -25537(r5)`.
|
|
567
|
+
* Unfolded, such an access is keyed on the materialised address, never joins its struct, and is
|
|
568
|
+
* spelled as a cast `*((u8 *)p + 38)`. On agbcc that spelling is a different program: for a
|
|
569
|
+
* COMPONENT_REF target expand_assignment (gcc/expr.c :2535) expands only the base and store_field
|
|
570
|
+
* (:2624) evaluates the right-hand side first, while a cast (INDIRECT_REF) target is expanded whole
|
|
571
|
+
* (:2689) before the right-hand side (:2747). A member store of a loaded value therefore loads it
|
|
572
|
+
* before the `adds #K`, and the cast store after (`sa3:sub_800F0E8`'s offset-38 byte store), and
|
|
573
|
+
* with a constant or a parameter on the right the two still allocate registers differently. The
|
|
574
|
+
* cast is a source spelling too: a hoisted `u8 *d = &p->f38; *d = v;` builds the cast's object. So
|
|
575
|
+
* the fold is the default and `opts.keepStores` (the `/far-cast` lift) keeps every such store on
|
|
576
|
+
* its address for the differ to referee.
|
|
577
|
+
* A LOAD through a cast and through a member compile alike, and fold in both lifts — but a load
|
|
578
|
+
* through a HELD pointer does not: agbcc materialises one `add` per member access, even for
|
|
579
|
+
* adjacent members (`p->f38 + p->f39` is `add #0x26` and `add #0x27`), so one materialised base
|
|
580
|
+
* read or written at two distinct offsets (`add r1, #0x26; ldrb [r1]; ldrb [r1, #1]`) is the
|
|
581
|
+
* pointer the source held, and the member spelling cannot rebuild it (`held-pointer-offsets`).
|
|
582
|
+
* ido does the same past its `addiu` reach, where the assembler gives each member access its own
|
|
583
|
+
* `lui`/`addu` (target.ts `farMemberBasePerAccess` states the reach per compiler). On ido, KMC,
|
|
584
|
+
* GCC 2.7.2 and mwcc one access through a member and through a cast compile alike, a store of a
|
|
585
|
+
* loaded value included, and there the fold buys the type; what it can cost is the neighbours'
|
|
586
|
+
* spelling — a folded store no longer aliases the loads beside it, and a held mwcc pointer is
|
|
587
|
+
* respelled as members — which is why `/far-cast` is offered there too.
|
|
588
|
+
*
|
|
589
|
+
* THE REFUSALS ARE TWO GATE TABLES, so `pnpm bench gates --pass displaced` can count which one
|
|
590
|
+
* keeps a row on the cast spelling: `DISPLACED_BASE_GATES`, asked once per materialised base, and
|
|
591
|
+
* `DISPLACED_ROOT_GATES`, asked once per root over the bases that passed. A base the first
|
|
592
|
+
* refuses keeps its own accesses on its materialised address while the root's other bases are
|
|
593
|
+
* still asked; a root the second refuses keeps every access off it on its address.
|
|
594
|
+
* Every gate is a spelling choice, not a soundness rule: the fold rewrites address arithmetic
|
|
595
|
+
* into the same address, so no candidate it admits reads different memory, and the differ
|
|
596
|
+
* referees. NOT TABLED: `/far-cast`'s `keepStores` skip, which is the variation itself
|
|
597
|
+
* rather than a refusal. Runs right before `recognizeStructs`, after raise/truncload.ts, which
|
|
598
|
+
* therefore never sees a folded access. Returns the number of accesses folded. */
|
|
599
|
+
export function foldDisplacedAccesses(
|
|
600
|
+
fn: Fn,
|
|
601
|
+
opts: DisplacedOptions = {},
|
|
602
|
+
behaviors: DisplacedBehaviors = {},
|
|
603
|
+
voidReturn = false,
|
|
604
|
+
baseGates: readonly Gate<DisplacedBase>[] = DISPLACED_BASE_GATES,
|
|
605
|
+
rootGates: readonly Gate<DisplacedRoot>[] = DISPLACED_ROOT_GATES,
|
|
606
|
+
): number {
|
|
607
|
+
const defs = defOpMap(fn);
|
|
608
|
+
const { accessesOf, arrayBases } = constOffsetAccesses(fn);
|
|
609
|
+
// Every use of each value: the op and operand index, or null for an edge argument.
|
|
610
|
+
const uses = new Map<Value, { op: Op | null; at: number }[]>();
|
|
611
|
+
const use = (v: Value, op: Op | null, at: number) => {
|
|
612
|
+
let l = uses.get(v);
|
|
613
|
+
if (!l) {
|
|
614
|
+
l = [];
|
|
615
|
+
uses.set(v, l);
|
|
168
616
|
}
|
|
169
|
-
|
|
617
|
+
l.push({ op, at });
|
|
170
618
|
};
|
|
171
|
-
for
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
case 'store':
|
|
182
|
-
note(op.operands[0], {
|
|
183
|
-
off: op.attrs.off as number,
|
|
184
|
-
width: op.attrs.width as number,
|
|
185
|
-
signed: (op.attrs.width as number) === 4,
|
|
186
|
-
});
|
|
187
|
-
break;
|
|
188
|
-
case 'aload':
|
|
189
|
-
case 'astore':
|
|
190
|
-
arrayBases.add(op.operands[0]);
|
|
191
|
-
break;
|
|
619
|
+
// Where each op stands, for the store-order reading.
|
|
620
|
+
const position = new Map<Op, { block: number; at: number }>();
|
|
621
|
+
fn.blocks.forEach((b, block) => {
|
|
622
|
+
(b.ops as Op[]).forEach((op, at) => {
|
|
623
|
+
position.set(op, { block, at });
|
|
624
|
+
op.operands.forEach((v, i) => use(v, op, i));
|
|
625
|
+
for (const s of op.successors) {
|
|
626
|
+
for (const v of s.args) {
|
|
627
|
+
use(v, null, -1);
|
|
628
|
+
}
|
|
192
629
|
}
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
// A `ret` of a function declared void reads no value: the address merely sits in the return
|
|
633
|
+
// register when the function ends (`add r0, #0x27; strb r1, [r0]; bx lr`).
|
|
634
|
+
const onlyFeeds = (v: Value, next: Value | undefined): boolean =>
|
|
635
|
+
(uses.get(v) ?? []).every(({ op, at }) =>
|
|
636
|
+
op === null
|
|
637
|
+
? false
|
|
638
|
+
: voidReturn && op.opcode === 'ret'
|
|
639
|
+
? true
|
|
640
|
+
: next === undefined
|
|
641
|
+
? (op.opcode === 'load' || op.opcode === 'store') && at === 0
|
|
642
|
+
: op.results[0] === next && op.opcode !== 'load' && op.opcode !== 'store',
|
|
643
|
+
);
|
|
644
|
+
// THE MEMBER'S ORDER (target.ts `farMemberStoreOrder`): the stored value is computed by a
|
|
645
|
+
// non-constant op in the same block before the chain's first link, AS THIS STORE'S RIGHT-HAND
|
|
646
|
+
// SIDE: this store is its only reader, and no memory access or call stands between it and the
|
|
647
|
+
// chain. A constant or a parameter on the right leaves no order to read, so it is no evidence
|
|
648
|
+
// either way; neither is a value an earlier statement computed (`u8 v = *q; *r = 0; p[38] = v;`),
|
|
649
|
+
// which the member and the cast spellings both load before the address, byte for byte. Nor is a
|
|
650
|
+
// value that IS a call: agbcc expands every call of a right-hand side before either target
|
|
651
|
+
// (MODIFY_EXPR's preexpand_calls, gcc/expr.c:6777), so `p->f60 = k()` and `p[60] = k()` are one
|
|
652
|
+
// object. What the right-hand side computes from a call's result still orders (`p->f60 = k() + 1`
|
|
653
|
+
// adds before the address, `p[60] = k() + 1` after it).
|
|
654
|
+
const effect = (op: Op): boolean =>
|
|
655
|
+
op.opcode === 'store' ||
|
|
656
|
+
op.opcode === 'astore' ||
|
|
657
|
+
op.opcode === 'load' ||
|
|
658
|
+
op.opcode === 'aload' ||
|
|
659
|
+
op.opcode === 'call' ||
|
|
660
|
+
op.opcode === 'opaque';
|
|
661
|
+
const memberOrdered = (a: Access, chain: Value[]): boolean => {
|
|
662
|
+
const value = a.op.operands[1];
|
|
663
|
+
const vdef = defs.get(value);
|
|
664
|
+
const first = defs.get(chain[chain.length - 1]);
|
|
665
|
+
const v = vdef && vdef.opcode !== 'const' && vdef.opcode !== 'call' ? position.get(vdef) : undefined;
|
|
666
|
+
const f = first ? position.get(first) : undefined;
|
|
667
|
+
return (
|
|
668
|
+
v !== undefined &&
|
|
669
|
+
f !== undefined &&
|
|
670
|
+
v.block === f.block &&
|
|
671
|
+
v.at < f.at &&
|
|
672
|
+
(uses.get(value) ?? []).length === 1 &&
|
|
673
|
+
!(fn.blocks[v.block].ops as Op[]).slice(v.at + 1, f.at).some(effect)
|
|
674
|
+
);
|
|
675
|
+
};
|
|
676
|
+
const byRoot = new Map<Value, { access: Access; k: number; memberOrder: boolean }[]>();
|
|
677
|
+
for (const [base, accesses] of accessesOf) {
|
|
678
|
+
const disp = constDisplacementOf(defs, base);
|
|
679
|
+
if (!disp) {
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
// `/far-cast`: a store kept on its address keeps the chain's value used by something other
|
|
683
|
+
// than a folded access, so no load sharing that address folds either.
|
|
684
|
+
if (opts.keepStores === true && accesses.some((a) => a.op.opcode === 'store')) {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
// A refused base stays on its address and its root's other bases are still asked: a held
|
|
688
|
+
// `u16 *d = &p->f68` beside a member store `p->f38 = *q` keeps `d` and folds the store.
|
|
689
|
+
if (firstRejection(baseGates, { base, accesses, disp, defs, arrayBases, onlyFeeds, behaviors }) !== null) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const list = byRoot.get(disp.root) ?? [];
|
|
693
|
+
byRoot.set(disp.root, list);
|
|
694
|
+
for (const access of accesses) {
|
|
695
|
+
const memberOrder =
|
|
696
|
+
behaviors.farMemberStoreOrder === true && access.op.opcode === 'store' && memberOrdered(access, disp.chain);
|
|
697
|
+
list.push({ access, k: disp.k, memberOrder });
|
|
193
698
|
}
|
|
194
699
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
700
|
+
let folded = 0;
|
|
701
|
+
for (const [root, moved] of byRoot) {
|
|
702
|
+
const merged = [
|
|
703
|
+
...(accessesOf.get(root) ?? []),
|
|
704
|
+
...moved.map(({ access, k }) => ({ ...access, off: access.off + k })),
|
|
705
|
+
];
|
|
706
|
+
const memberStoreOrder = moved.some((m) => m.memberOrder);
|
|
707
|
+
if (firstRejection(rootGates, { root, merged, memberStoreOrder }) !== null) {
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
for (const { access, k } of moved) {
|
|
711
|
+
access.op.operands[0] = root;
|
|
712
|
+
access.op.attrs.off = access.off + k;
|
|
713
|
+
folded++;
|
|
714
|
+
if (access.op.opcode === 'store') {
|
|
715
|
+
opts.onFoldedStore?.();
|
|
203
716
|
}
|
|
204
717
|
}
|
|
718
|
+
// An array-shaped set the member's store order admitted: `recognizeStructs` would read it as
|
|
719
|
+
// an array, so the struct its evidence names is synthesized here.
|
|
720
|
+
if (isArray(merged)) {
|
|
721
|
+
root.type = T.ptr(buildStruct(`Struct${firstFreeStructIndex(fn)}`, merged));
|
|
722
|
+
}
|
|
205
723
|
}
|
|
724
|
+
return folded;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** Recover struct-pointer types from access-pattern evidence. Runs after array legalization and
|
|
728
|
+
* before type recovery, so `recoverTypes` sees the base already typed and does not flatten it to a
|
|
729
|
+
* plain pointer. Returns the number of bases recovered as structs. `aggregateBoundary` is the
|
|
730
|
+
* compiler's struct/union size boundary, undefined where nobody measured it (`buildUnionStruct`). */
|
|
731
|
+
export function recognizeStructs(fn: Fn, aggregateBoundary: number | undefined): number {
|
|
732
|
+
const { accessesOf, order, arrayBases } = constOffsetAccesses(fn);
|
|
733
|
+
|
|
734
|
+
// Does this base's address have a source of truth OUTSIDE this function's access set? Consulted
|
|
735
|
+
// only when synthesis refuses an OVERLAP: see the caller below. Two kinds qualify, and both are
|
|
736
|
+
// read with the helpers raise/truncload.ts asks the same question with — one concept, one
|
|
737
|
+
// reading, and `globalCellOf` is the whole of that reading: it answers for `&gSym + K` as well as
|
|
738
|
+
// for the bare `&gSym`, and an interior address is its own base here (`constOffsetAccesses` keys
|
|
739
|
+
// on the operand Value), so a narrower opcode test would decline the shape `fixed-cell` admits.
|
|
740
|
+
const defs = defOpMap(fn);
|
|
741
|
+
const addressDeclaredElsewhere = (base: Value): boolean =>
|
|
742
|
+
globalCellOf(defs, base, 0) !== null || constAddressOf(defs, base, 0) !== null;
|
|
206
743
|
|
|
207
744
|
// The NAME counter is seeded from the names already in the graph, not from this pass's own
|
|
208
745
|
// success count: raise/memberarrays.ts runs first and mints `Struct<N>` types of its own, and two
|
|
@@ -216,6 +753,15 @@ export function recognizeStructs(fn: Fn): number {
|
|
|
216
753
|
if (base.type.kind !== 'unknown') {
|
|
217
754
|
continue;
|
|
218
755
|
} // already typed (not a bare recovery target)
|
|
756
|
+
// A FRAME OBJECT'S ADDRESS is never a recovery target: its declaration is the frame audit's
|
|
757
|
+
// stamp (structure.ts `laddrType` — a scalar, a `u8` array, a struct-return temp, or the union
|
|
758
|
+
// of the views its accesses use), and every access to it is spelled from that stamp, never
|
|
759
|
+
// through a type recovered here. Recovered anyway, the one-object area's accesses at several
|
|
760
|
+
// offsets and widths make a second, per-word union of the same bytes that nothing references
|
|
761
|
+
// and every candidate prints.
|
|
762
|
+
if (defs.get(base)?.opcode === 'laddr') {
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
219
765
|
|
|
220
766
|
const accesses = accessesOf.get(base)!;
|
|
221
767
|
if (isArray(accesses)) {
|
|
@@ -224,19 +770,47 @@ export function recognizeStructs(fn: Fn): number {
|
|
|
224
770
|
try {
|
|
225
771
|
base.type = T.ptr(buildStruct(`Struct${name}`, accesses));
|
|
226
772
|
} catch (e) {
|
|
227
|
-
// A
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
773
|
+
// A base whose ADDRESS is already declared somewhere other than this function's access set is
|
|
774
|
+
// not a struct this pass has to synthesize, so failing to synthesize one for it is not a
|
|
775
|
+
// reason to decline the function — PROVIDED the refusal is the OVERLAP one (its own class,
|
|
776
|
+
// never a substring of the message), whose residue is per-access spellings that stand on their
|
|
777
|
+
// own. Two kinds of base qualify:
|
|
778
|
+
// • a NAMED global (`globalCellOf`) — its declaration belongs to the project's own headers,
|
|
779
|
+
// and its constant-offset accesses render at L3 through the symbol context (member
|
|
780
|
+
// spelling when the map knows the layout, the honest cast spelling when it does not). The
|
|
781
|
+
// inhabitant is agbcc FUSING two adjacent u8 compares into one ldrh — `s.level == 8 &&
|
|
782
|
+
// s.world == 6` reads offset 12 at widths 1 AND 2, which is not a union, just two
|
|
783
|
+
// spellings of declared bytes.
|
|
784
|
+
// • a LITERAL ADDRESS (`constAddressOf`) — a cell the hardware placed, which L3 renders as
|
|
785
|
+
// a cast at each access's OWN width (`*(u16 *)0x4000004`). There is no layout to
|
|
786
|
+
// reconcile: two widths at one literal address are two casts of one address, which is
|
|
787
|
+
// what the source wrote. The inhabitant is a memory-mapped I/O register the source writes
|
|
788
|
+
// as a halfword and as a word (`sa3:Sio32MultiLoadMain`, `synthetic:unidev`).
|
|
789
|
+
// This is the DUAL of raise/truncload.ts's `fixed-cell` gate, not a relaxation of it: that
|
|
790
|
+
// gate refuses to FOLD the two accesses into one cast of a wider load, because a device read
|
|
791
|
+
// is not a read of the bytes around it. Both readings agree that the two accesses stay two.
|
|
792
|
+
//
|
|
793
|
+
// THE PACKED REFUSAL IS NOT FORGIVEN FOR ANY BASE. Its residue is not two spellings: a
|
|
794
|
+
// misaligned access renders through the element index `off / width`, which is a fraction and
|
|
795
|
+
// not C, so forgiving it would trade a loud decline for an uncompilable candidate. THIS layer
|
|
796
|
+
// is the one that can name the layout, which is why the refusal a reader sees is this one.
|
|
797
|
+
// structure/structure.ts `displacementIndex` refuses the fraction a second time where it
|
|
798
|
+
// would be computed, for the bases that never reach this pass at all (already typed, or a
|
|
799
|
+
// forgiven overlap); that message can only describe the access, not the layout.
|
|
800
|
+
//
|
|
801
|
+
// An ANONYMOUS base (a loaded pointer, a parameter) has no other source of truth: its layout
|
|
802
|
+
// is what this function's accesses say, and an overlap there says UNION. It is declared as
|
|
803
|
+
// one (`buildUnionStruct`), which declines in turn on what it cannot lay out. The two
|
|
804
|
+
// declared kinds above keep their casts although `buildUnionStruct`'s aliasing argument
|
|
805
|
+
// applies to them too: no row re-reads across a store there, and a union is the wrong
|
|
806
|
+
// declaration for both inhabitants (docs/level-tower.md, "A union").
|
|
807
|
+
if (!(e instanceof StructOverlapError)) {
|
|
808
|
+
throw e;
|
|
809
|
+
}
|
|
810
|
+
if (addressDeclaredElsewhere(base)) {
|
|
237
811
|
continue;
|
|
238
812
|
}
|
|
239
|
-
|
|
813
|
+
base.type = T.ptr(buildUnionStruct(`Struct${name}`, accesses, aggregateBoundary));
|
|
240
814
|
}
|
|
241
815
|
name++;
|
|
242
816
|
count++;
|
|
@@ -249,13 +823,15 @@ export function recognizeStructs(fn: Fn): number {
|
|
|
249
823
|
* `ir/struct-names.ts`, which the three struct minters share; this names the prefix. */
|
|
250
824
|
export function firstFreeStructIndex(fn: Fn): number {
|
|
251
825
|
return nextStructIndex(
|
|
252
|
-
[...collectStructs(fn)].map((s) => s.name),
|
|
826
|
+
[...collectStructs(fn, true)].map((s) => s.name),
|
|
253
827
|
'Struct',
|
|
254
828
|
);
|
|
255
829
|
}
|
|
256
830
|
|
|
257
831
|
/** The distinct struct types this function's L2 GRAPH mentions (unwrapping struct pointers on every
|
|
258
|
-
* value), deduped by name and sorted, for the backend to declare above the function.
|
|
832
|
+
* value), deduped by name and sorted, for the backend to declare above the function. A struct the
|
|
833
|
+
* headers own (`declared`, ir/types.ts) is left out — the declarations block defines it — unless
|
|
834
|
+
* `withDeclared` asks for every name the graph uses, which is what a name allocator needs.
|
|
259
835
|
*
|
|
260
836
|
* "Mentions", not "references": this walks `fn.blocks` at the moment structuring runs, and the
|
|
261
837
|
* result is CACHED on the SFn (`structure.ts`) and carried by every later `{...sfn}` pass, so a
|
|
@@ -266,11 +842,11 @@ export function firstFreeStructIndex(fn: Fn): number {
|
|
|
266
842
|
* is recorded here rather than fixed speculatively. The fix, if a pass ever makes it reachable, is
|
|
267
843
|
* the one l3/symbol-refs.ts already applies to symbol references: derive at the consumption point
|
|
268
844
|
* (`backend/cfamily.ts` structDecls) instead of caching. */
|
|
269
|
-
export function collectStructs(fn: Fn): StructType[] {
|
|
845
|
+
export function collectStructs(fn: Fn, withDeclared = false): StructType[] {
|
|
270
846
|
const seen = new Map<string, StructType>();
|
|
271
847
|
const consider = (t: IrType) => {
|
|
272
848
|
const s = t.kind === 'ptr' && t.to.kind === 'struct' ? t.to : t.kind === 'struct' ? t : null;
|
|
273
|
-
if (s && s.kind === 'struct' && !seen.has(s.name)) {
|
|
849
|
+
if (s && s.kind === 'struct' && (withDeclared || s.declared === undefined) && !seen.has(s.name)) {
|
|
274
850
|
seen.set(s.name, { name: s.name, fields: s.fields, size: s.size });
|
|
275
851
|
}
|
|
276
852
|
};
|