@asmlift/core 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +165 -50
- package/src/backend/cpp.ts +2 -1
- package/src/backend/pascal.ts +31 -12
- package/src/contracts.ts +173 -16
- package/src/declare.ts +69 -20
- package/src/frontend/asmdata.ts +7 -1
- package/src/frontend/call-plan.ts +964 -60
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +23 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1232 -192
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2076 -215
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2711 -232
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1460 -44
- package/src/frontend/stackargs.ts +106 -57
- package/src/frontend/thumb.ts +2044 -318
- package/src/ir/alias.ts +42 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +100 -5
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +159 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +109 -9
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +80 -11
- package/src/l3/ast.ts +290 -18
- package/src/l3/basecse.ts +33 -5
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +2 -2
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +31 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +9 -4
- package/src/l3/reindex.ts +283 -96
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +4 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +541 -24
- package/src/proto-context.ts +905 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +10 -26
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +102 -19
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +694 -0
- package/src/raise/shortcircuit.ts +26 -12
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +89 -31
- package/src/rank.ts +307 -54
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +165 -2
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +2180 -1054
- package/src/structure/switch-recover.ts +244 -100
- package/src/symbols.ts +6 -0
- package/src/target.ts +765 -52
- package/src/trace.ts +26 -8
- package/src/variation-definitions.ts +126 -14
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +5 -1
package/src/raise/structs.ts
CHANGED
|
@@ -47,12 +47,15 @@
|
|
|
47
47
|
// at an offset natural C alignment could not place it at) is rejected LOUD. An overlap is a union
|
|
48
48
|
// member on a base whose layout only this function's accesses describe, and no struct at all on
|
|
49
49
|
// one whose address is declared elsewhere (`recognizeStructs`).
|
|
50
|
-
import { constAddressOf, globalCellOf } from '../ir/alias';
|
|
50
|
+
import { constAddressOf, constDisplacementOf, globalCellOf } from '../ir/alias';
|
|
51
51
|
import { Fn, Op, Value, defOpMap } from '../ir/core';
|
|
52
|
+
import { placedAt } from '../ir/discipline';
|
|
52
53
|
import { nextStructIndex } from '../ir/struct-names';
|
|
53
|
-
import { IrType, StructField, T, scalarTypeForAccess } from '../ir/types';
|
|
54
|
+
import { IrType, StructField, T, UNION_VIEW_NAMES, intWidth, scalarTypeForAccess, unionViewSigns } from '../ir/types';
|
|
54
55
|
import type { StructType } from '../l3/ast';
|
|
56
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
55
57
|
import { RaiseUnsupportedError, StructOverlapError } from './errors';
|
|
58
|
+
import { storeSigned } from './storesign';
|
|
56
59
|
|
|
57
60
|
/** A single observed constant-offset access to a base: the op itself and its program point, its
|
|
58
61
|
* byte offset, its access width (bytes), and the signedness a recovered field would take from it.
|
|
@@ -62,10 +65,11 @@ import { RaiseUnsupportedError, StructOverlapError } from './errors';
|
|
|
62
65
|
* like" — and a second copy of the walk would let them come to disagree about the answer while
|
|
63
66
|
* each looked right on its own.
|
|
64
67
|
*
|
|
65
|
-
* SIGNEDNESS ON THE STORE HALF IS A POLICY, not a reading
|
|
66
|
-
*
|
|
67
|
-
* LOAD's own signedness at the same offset,
|
|
68
|
-
*
|
|
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. */
|
|
69
73
|
export interface Access {
|
|
70
74
|
op: Op;
|
|
71
75
|
/** index into `fn.blocks`, for the dominance question raise/truncload.ts asks. */
|
|
@@ -78,6 +82,9 @@ export interface Access {
|
|
|
78
82
|
isLoad: boolean;
|
|
79
83
|
}
|
|
80
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
|
+
|
|
81
88
|
/** What one walk of a function yields about its constant-offset memory accesses. */
|
|
82
89
|
export interface ConstOffsetAccesses {
|
|
83
90
|
/** every base's constant-offset accesses, in program order. */
|
|
@@ -124,7 +131,7 @@ export function constOffsetAccesses(fn: Fn): ConstOffsetAccesses {
|
|
|
124
131
|
at,
|
|
125
132
|
off: op.attrs.off as number,
|
|
126
133
|
width: op.attrs.width as number,
|
|
127
|
-
signed: (op.attrs.width as number)
|
|
134
|
+
signed: storeSigned(op, op.attrs.width as number),
|
|
128
135
|
isLoad: false,
|
|
129
136
|
});
|
|
130
137
|
break;
|
|
@@ -169,25 +176,35 @@ function isArray(accesses: Access[]): boolean {
|
|
|
169
176
|
* not settle — a narrow STORE, a read above the low-order end, a cell at a constant address, and a
|
|
170
177
|
* cover on a path the narrow read does not run — and every one of them throws here.
|
|
171
178
|
*
|
|
172
|
-
* A THROW IS NOT THE FUNCTION'S VERDICT, AND WHICH THROW IT IS DECIDES THAT.
|
|
179
|
+
* A THROW IS NOT THE FUNCTION'S VERDICT, AND WHICH THROW IT IS DECIDES THAT. Four throw sites
|
|
173
180
|
* below reject a layout and they fall into two CLASSES, which is the split a caller reads:
|
|
174
|
-
* • `StructOverlapError` — two accesses whose byte ranges collide.
|
|
175
|
-
* it,
|
|
176
|
-
* at an offset already taken with a different width,
|
|
177
|
-
*
|
|
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}`).
|
|
178
187
|
* Each access alone is spellable at its own offset and width, so a base whose ADDRESS is
|
|
179
188
|
* declared outside this function's access set can keep its untyped spelling and lift
|
|
180
189
|
* (`recognizeStructs` below). There is no struct to synthesize, and nothing is lost by not
|
|
181
|
-
* synthesizing one. Any other base is declared with a union member
|
|
190
|
+
* synthesizing one. Any other base is declared with a union member, or a `castViews` field
|
|
191
|
+
* where the union would not fit (`buildUnionStruct`).
|
|
182
192
|
* • `RaiseUnsupportedError` — a field at an offset its own natural alignment could not place it
|
|
183
193
|
* at. This one has NO per-access spelling downstream: an access at `off` under a wider access's
|
|
184
194
|
* width folds to the element index `off / width` (structure/structure.ts), which for a
|
|
185
195
|
* misaligned pair is a FRACTION and not C. It declines whatever the base is. */
|
|
186
|
-
function buildStruct(name: string, accesses: Access[]): IrType {
|
|
196
|
+
function buildStruct(name: string, accesses: Access[], extensionViews = true): IrType {
|
|
187
197
|
// One field per distinct offset; a load's signedness wins over a store's (more information).
|
|
188
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>();
|
|
189
202
|
for (const a of accesses) {
|
|
190
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
|
+
}
|
|
191
208
|
if (!prev) {
|
|
192
209
|
byOff.set(a.off, a);
|
|
193
210
|
continue;
|
|
@@ -197,9 +214,23 @@ function buildStruct(name: string, accesses: Access[]): IrType {
|
|
|
197
214
|
`cannot recover struct '${name}': overlapping fields at offset ${a.off} (widths ${prev.width} and ${a.width}) — unions not modelled`,
|
|
198
215
|
);
|
|
199
216
|
}
|
|
200
|
-
|
|
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) {
|
|
201
232
|
byOff.set(a.off, a);
|
|
202
|
-
}
|
|
233
|
+
}
|
|
203
234
|
}
|
|
204
235
|
const dataFields: StructField[] = [...byOff.values()]
|
|
205
236
|
.sort((x, y) => x.off - y.off)
|
|
@@ -253,10 +284,6 @@ function placeFields(name: string, dataFields: StructField[]): IrType {
|
|
|
253
284
|
return T.struct(name, fields);
|
|
254
285
|
}
|
|
255
286
|
|
|
256
|
-
/** The member name of each view a union holds, by access width. A width with no entry has no view
|
|
257
|
-
* (the deref contract admits no other scalar width either), and its overlap still declines. */
|
|
258
|
-
const VIEW_NAMES: Readonly<Record<number, string>> = { 1: 'byte', 2: 'half', 4: 'word' };
|
|
259
|
-
|
|
260
287
|
/** The struct for a base whose accesses OVERLAP — `buildStruct` refused it with
|
|
261
288
|
* `StructOverlapError` — with each overlap declared as a UNION member: the same bytes read or
|
|
262
289
|
* written at more than one width are views of one cell, and a union is the C that says so.
|
|
@@ -281,14 +308,15 @@ const VIEW_NAMES: Readonly<Record<number, string>> = { 1: 'byte', 2: 'half', 4:
|
|
|
281
308
|
* one element at the cell's start is a scalar; any other is an array reaching the furthest
|
|
282
309
|
* element accessed (`u16 half[2]` for the halfword at +2). A cell with ONE view is a plain field;
|
|
283
310
|
* any other is a member `field_<off>` holding a union of its views — including a single-width cell
|
|
284
|
-
* loaded both ways
|
|
285
|
-
* and that same cell there is one field typed by its signed load, so its unsigned read is spelled sign-extended.
|
|
311
|
+
* loaded both ways, which `buildStruct` hands over as an overlap even on a base with no other one.
|
|
286
312
|
*
|
|
287
313
|
* THE COMPILER DECIDES THE UNION'S SIZE, not its widest view. `aggregateBoundary` is the size it
|
|
288
314
|
* aligns and rounds every struct and union to (target.ts `compilerBehaviors.aggregateBoundary`):
|
|
289
315
|
* agbcc makes `union { u16 h; u8 b; }` four bytes, four-aligned, where the other compilers make
|
|
290
316
|
* it two. A union that boundary would move — seated off it, or with the next field inside its
|
|
291
|
-
* rounded size —
|
|
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
|
|
292
320
|
* compiler (undefined) declines every union, since no boundary is safe to assume: one too small
|
|
293
321
|
* mislays the fields after the union on agbcc, one too large drops the pad in front of them
|
|
294
322
|
* everywhere else.
|
|
@@ -311,17 +339,16 @@ function buildUnionStruct(structName: string, accesses: Access[], aggregateBound
|
|
|
311
339
|
const start = c[0].off;
|
|
312
340
|
const members = [...new Set(c.map((a) => a.width))].flatMap((width): StructField[] => {
|
|
313
341
|
const group = c.filter((a) => a.width === width);
|
|
314
|
-
const name =
|
|
342
|
+
const name = UNION_VIEW_NAMES[width];
|
|
315
343
|
if (name === undefined) {
|
|
316
344
|
throw new RaiseUnsupportedError(
|
|
317
345
|
`cannot recover struct '${structName}': no union view for a ${width}-byte access at offset ${group[0].off}`,
|
|
318
346
|
);
|
|
319
347
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const views = signs.length === 0 ? [false] : signs;
|
|
348
|
+
const views = unionViewSigns(
|
|
349
|
+
width,
|
|
350
|
+
group.map((a) => ({ isLoad: a.isLoad, signed: a.signed, stamped: isStamped(a) })),
|
|
351
|
+
);
|
|
325
352
|
const viewOf = (a: Access): boolean => (views.length === 1 ? views[0] : a.isLoad ? a.signed : false);
|
|
326
353
|
return views.map((signed): StructField => {
|
|
327
354
|
const reads = group.filter((a) => viewOf(a) === signed);
|
|
@@ -341,18 +368,46 @@ function buildUnionStruct(structName: string, accesses: Access[], aggregateBound
|
|
|
341
368
|
}
|
|
342
369
|
return { off: start, type: T.union(members, aggregateBoundary), name: `field_${start}` };
|
|
343
370
|
});
|
|
344
|
-
dataFields.
|
|
371
|
+
const fitted = dataFields.map((f, i): StructField => {
|
|
345
372
|
if (f.type.kind !== 'union') {
|
|
346
|
-
return;
|
|
373
|
+
return f;
|
|
347
374
|
}
|
|
348
375
|
const next = dataFields[i + 1];
|
|
349
|
-
if (f.off % f.type.size
|
|
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) {
|
|
350
381
|
throw new RaiseUnsupportedError(
|
|
351
382
|
`cannot recover struct '${structName}': the union at offset ${f.off} does not fit this compiler's ${aggregateBoundary}-byte aggregate boundary`,
|
|
352
383
|
);
|
|
353
384
|
}
|
|
385
|
+
return { off: f.off, type: widest, name: f.name, castViews: true };
|
|
354
386
|
});
|
|
355
|
-
return placeFields(structName,
|
|
387
|
+
return placeFields(structName, fitted);
|
|
388
|
+
}
|
|
389
|
+
|
|
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;
|
|
356
411
|
}
|
|
357
412
|
|
|
358
413
|
/** Size in bytes of a recovered field's type (int width/8; pointer is word-sized 4; a union the
|
|
@@ -370,6 +425,305 @@ function fieldSize(t: IrType): number {
|
|
|
370
425
|
}
|
|
371
426
|
}
|
|
372
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);
|
|
616
|
+
}
|
|
617
|
+
l.push({ op, at });
|
|
618
|
+
};
|
|
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
|
+
}
|
|
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 });
|
|
698
|
+
}
|
|
699
|
+
}
|
|
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?.();
|
|
716
|
+
}
|
|
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
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return folded;
|
|
725
|
+
}
|
|
726
|
+
|
|
373
727
|
/** Recover struct-pointer types from access-pattern evidence. Runs after array legalization and
|
|
374
728
|
* before type recovery, so `recoverTypes` sees the base already typed and does not flatten it to a
|
|
375
729
|
* plain pointer. Returns the number of bases recovered as structs. `aggregateBoundary` is the
|
|
@@ -399,6 +753,15 @@ export function recognizeStructs(fn: Fn, aggregateBoundary: number | undefined):
|
|
|
399
753
|
if (base.type.kind !== 'unknown') {
|
|
400
754
|
continue;
|
|
401
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
|
+
}
|
|
402
765
|
|
|
403
766
|
const accesses = accessesOf.get(base)!;
|
|
404
767
|
if (isArray(accesses)) {
|
package/src/raise/truncload.ts
CHANGED
|
@@ -161,9 +161,10 @@ export const TRUNC_LOAD_GATES: readonly Gate<TruncatedLoad>[] = [
|
|
|
161
161
|
// spellings a source has for one land here: the bare cast of a number (`*(vu16 *)0x4000004`)
|
|
162
162
|
// through `constAddressOf`, and the named register (`REG_DISPSTAT`) through `globalCellOf`,
|
|
163
163
|
// whose `volatile` is a declaration in the project's headers that no raise pass can read.
|
|
164
|
-
// The two helpers are ir/alias.ts's
|
|
165
|
-
//
|
|
166
|
-
//
|
|
164
|
+
// The two helpers are ir/alias.ts's. A dead read this rule and `qualified-access` admit is one
|
|
165
|
+
// nobody spells: a read the raw lift never used is stamped by the dead-read pin
|
|
166
|
+
// (frontend/dead-reads.ts) before this pass runs, `qualified-access` refuses it, and
|
|
167
|
+
// structure.ts `volatileQualifiable` spells only a qualified read.
|
|
167
168
|
//
|
|
168
169
|
// NOT `capabilities.deviceRegisters`. That window is documented as a question about SPELLING
|
|
169
170
|
// and may be approximate (target.ts), so a soundness rule may not rest on it; asking whether
|