@asmlift/core 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- package/src/variation-tokens.ts +68 -3
|
@@ -9,11 +9,19 @@
|
|
|
9
9
|
* every ISA — the level-tower split for this frame model: the frontend supplies the PARTITION,
|
|
10
10
|
* the shared pass applies the rule. Messages name the frame base `sp`.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
* Three frontends call it: Thumb; MIPS, which lowers `addiu rD,sp,k` and `addu rD,sp,rX` to an
|
|
13
|
+
* `laddr` and hands over the O32 local area as both ranges (`frontend/mips.ts` `measureO32Frame`);
|
|
14
|
+
* and PPC, which lowers `addi rD,r1,k` and every r1 access inside mwcc's local area the same way
|
|
15
|
+
* (`frontend/ppc.ts` `measurePpcFrame`).
|
|
16
|
+
* The worked examples below are agbcc's, because agbcc is the compiler every rule was first
|
|
17
|
+
* measured against; IDO, gcc 2.7.2 and mwcc reload past every store of another type (strict
|
|
18
|
+
* aliasing does not reorder there), so the cast and union refusals below are only more cautious on
|
|
19
|
+
* MIPS and PPC. */
|
|
20
|
+
import { type StridedRange, linearFactor } from '../ir/bits';
|
|
21
|
+
import { type Block, type Op, type Value, byValueOperands, calleeName, mkOp, mkValue, successorsOf } from '../ir/core';
|
|
22
|
+
import { type IrType, T, structSpelling, typeEquals, typeToString, unionViewSigns } from '../ir/types';
|
|
23
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
24
|
+
import { blockCopyRoles } from '../proto';
|
|
17
25
|
import type { SymbolMap } from '../symbols';
|
|
18
26
|
import { type TargetDescription, blockTransferRead, sourceControlRead, sourceReach } from '../target';
|
|
19
27
|
import type { CallDeclarations } from './call-plan';
|
|
@@ -31,9 +39,25 @@ export interface FrameObjectAudit {
|
|
|
31
39
|
* stack-argument block staged at its bottom, which is where an untyped object claims to start */
|
|
32
40
|
declaredLocals: NonNullable<LiveInModel['declaredLocals']>;
|
|
33
41
|
usedSlotOffsets: ReadonlySet<number>;
|
|
42
|
+
/** THE FRONTEND'S LOADED-WORD BOUND, present where the target states `outgoingAreaNeverLoadedFrom`:
|
|
43
|
+
* `from` is that offset and `bound` the lowest frame word, in this audit's offsets, the frontend
|
|
44
|
+
* saw the function load from `from` up, or `Infinity`. The frontend reads it ONCE off its
|
|
45
|
+
* instructions — slot reloads, the epilogue's restores and plain loads alike, which the IR does
|
|
46
|
+
* not all show — and its own stack-argument refusals take the same number, so one lift answers
|
|
47
|
+
* the premise once (`storedNeverRead` adds only the loads it alone sees, through a captured
|
|
48
|
+
* address). Absent where the target states no such offset. */
|
|
49
|
+
loadedWords?: { readonly from: number; readonly bound: number };
|
|
34
50
|
capturedObjectIsTheWholeFrame: boolean;
|
|
51
|
+
/** the frame offsets whose escape the frontend's outgoing-argument verdict leans on — each one
|
|
52
|
+
* re-proven here (`recheckEscapesReliedOn`). Absent or empty where it leans on none. */
|
|
53
|
+
escapesReliedOn?: ReadonlySet<number>;
|
|
35
54
|
/** every capture the add arm re-minted at a constant offset from it */
|
|
36
55
|
movedCaptures: ReadonlySet<Value>;
|
|
56
|
+
/** the `laddr`s the frontend minted for a DIRECT frame access (`off(sp)` inside its local area)
|
|
57
|
+
* rather than for a captured address: a word reached only through them is one its slot model
|
|
58
|
+
* lifts again outside a relift's bytes (`reliftRange`). Thumb, which lifts a direct word access
|
|
59
|
+
* as a slot, has none. */
|
|
60
|
+
directAccesses?: ReadonlySet<Value>;
|
|
37
61
|
/** whether a callee's declaration rules out a struct returned through a hidden pointer at
|
|
38
62
|
* argument 0 (aggregate.ts `returnsWithoutHiddenPointer`), asked of the frontend's call
|
|
39
63
|
* declarations */
|
|
@@ -43,6 +67,17 @@ export interface FrameObjectAudit {
|
|
|
43
67
|
/** the retraction rules an escape is judged by; `FRAME_ESCAPE_GATES` when absent, and a
|
|
44
68
|
* census or an ablation hands in its own (`FRAME_OBJECT_AUDIT`) */
|
|
45
69
|
gates?: readonly Gate<FrameEscape>[];
|
|
70
|
+
/** the rules that keep a frame's end word in the one object (`reliftRange`);
|
|
71
|
+
* `RELIFT_HAND_BACK_GATES` when absent, and a census or an ablation hands in its own */
|
|
72
|
+
reliftGates?: readonly Gate<HandBack>[];
|
|
73
|
+
/** THE ARGUMENT HOMES THE FRONTEND KEPT FROM THE SLOT MODEL FOR THE ONE-OBJECT ANSWER ALONE: frame
|
|
74
|
+
* words an incoming argument register is stored to on entry, whose narrow reloads the frontend
|
|
75
|
+
* left as captures rather than reading them as the word narrowed, because a by-value struct
|
|
76
|
+
* parameter's home recompiles as that one object (Thumb `argumentHomes`). Where that answer is
|
|
77
|
+
* withheld and the audit refuses, it asks for a relift with them released
|
|
78
|
+
* (`FrameObjectRelift.homesReleased`) rather than refusing, and a relift that declines reports
|
|
79
|
+
* this refusal first. Absent or empty where none was kept. */
|
|
80
|
+
releasableHomes?: ReadonlySet<number>;
|
|
46
81
|
/** the bytes a first audit asked to have kept as ONE object in memory (`FrameObjectRelift`):
|
|
47
82
|
* the frontend routes every access inside them through an `laddr`, and the audit declares
|
|
48
83
|
* them one object rather than judging each offset on its own */
|
|
@@ -54,12 +89,113 @@ export interface FrameRange {
|
|
|
54
89
|
readonly to: number;
|
|
55
90
|
}
|
|
56
91
|
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
92
|
+
/** THE LOCAL AREA ABOVE A FRAME'S OUTGOING AREA, `[from, to)` in post-push stack-pointer offsets:
|
|
93
|
+
* from where the outgoing words end (`outgoingEnd`, rounded up to `align`) to the lowest register
|
|
94
|
+
* save at or above that, or the frame's top. The rule O32's `above-outgoing-area` compilers
|
|
95
|
+
* (frontend/mips.ts `o32LocalArea`) and mwcc (frontend/ppc.ts, `compilerBehaviors.linkageLocalArea`)
|
|
96
|
+
* both keep; each frontend measures the numbers it is applied to. Null where nothing is left. */
|
|
97
|
+
export function areaAboveOutgoing({
|
|
98
|
+
frameSize,
|
|
99
|
+
outgoingEnd,
|
|
100
|
+
saves,
|
|
101
|
+
align,
|
|
102
|
+
}: {
|
|
103
|
+
frameSize: number;
|
|
104
|
+
outgoingEnd: number;
|
|
105
|
+
saves: readonly number[];
|
|
106
|
+
align: number;
|
|
107
|
+
}): FrameRange | null {
|
|
108
|
+
const from = Math.ceil(outgoingEnd / align) * align;
|
|
109
|
+
const area = { from, to: Math.min(frameSize, ...saves.filter((o) => o >= from)) };
|
|
110
|
+
return area.from < area.to ? area : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Whether a `width`-byte frame access at post-push offset `at` lands on a byte of `area`. A
|
|
114
|
+
* frontend that routes accesses into the area by their post-push offset asks it of every access
|
|
115
|
+
* made at ANOTHER depth, which that routing would misname, and draws no area where one does. */
|
|
116
|
+
export const landsOnArea = (area: FrameRange, at: number, width: number): boolean =>
|
|
117
|
+
at < area.to && at + width > area.from;
|
|
118
|
+
|
|
119
|
+
/** Whether a `width`-byte access at `at` inside a frontend's local area is MEMORY in a relift that
|
|
120
|
+
* keeps `kept` as one object: inside it, yes; wholly outside, no — a word the first audit handed
|
|
121
|
+
* back to the slot model (`reliftRange`); across its edge, refused, half memory and half slot. */
|
|
122
|
+
export function keptInRelift(kept: FrameRange, at: number, width: number, what: () => string, name: string): boolean {
|
|
123
|
+
if (at >= kept.from && at + width <= kept.to) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (at >= kept.to || at + width <= kept.from) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
throw new FrontendUnsupportedError(
|
|
130
|
+
`cannot lift '${name}': address-taken stack local — ${what()} reaches across an edge of the bytes ` +
|
|
131
|
+
`[sp,#${kept.from}) to [sp,#${kept.to}) kept as one object — part memory, part slot`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** What an audit answers instead of a refusal. Where a device may read the frame, or a callee write
|
|
136
|
+
* it, without bound: lift again with `oneObject` set to these bytes. Where the one-object answer is
|
|
137
|
+
* withheld and the audit refuses a frame with an argument home the frontend kept out of the slot
|
|
138
|
+
* model for that answer alone (`FrameObjectAudit.releasableHomes`): lift again with `oneObject`
|
|
139
|
+
* unset and `homesReleased`, so the frontend reads those homes as the slots they are. */
|
|
59
140
|
export interface FrameObjectRelift {
|
|
60
|
-
readonly oneObject: FrameRange;
|
|
141
|
+
readonly oneObject: FrameRange | undefined;
|
|
142
|
+
/** with `homesReleased`, the first audit's refusal, which a relift that declines again reports
|
|
143
|
+
* first: the frame as the first lift read it, followed by the second lift's own refusal where it
|
|
144
|
+
* differs (`bothRefusals`) */
|
|
145
|
+
readonly homesReleased?: { readonly refusal: FrontendUnsupportedError };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** THE RELIFT PROTOCOL, AT MOST TWICE, the one copy every frontend that runs the audit calls: lift,
|
|
149
|
+
* and where the audit answers with a relift (`FrameObjectRelift`), lift once more as it asks —
|
|
150
|
+
* bytes to keep as one object routed through an `laddr`, or argument homes released to the slot
|
|
151
|
+
* model. The second audit judges the frame as asked and refuses rather than asking again, so a
|
|
152
|
+
* second request is an internal error, not a third lift. A second lift with homes released that
|
|
153
|
+
* declines reports the first audit's refusal (`homesReleased.refusal`), then its own where that
|
|
154
|
+
* differs. A frontend that hands the audit no `releasableHomes` is never asked to release one. */
|
|
155
|
+
export function liftWithRelift<F>(
|
|
156
|
+
name: string,
|
|
157
|
+
liftOnce: (oneObject: FrameRange | undefined, homesReleased: boolean) => F | FrameObjectRelift,
|
|
158
|
+
): F {
|
|
159
|
+
const first = liftOnce(undefined, false);
|
|
160
|
+
if (!isRelift(first)) {
|
|
161
|
+
return first;
|
|
162
|
+
}
|
|
163
|
+
const released = first.homesReleased;
|
|
164
|
+
let again: F | FrameObjectRelift;
|
|
165
|
+
try {
|
|
166
|
+
again = liftOnce(first.oneObject, released !== undefined);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
if (released !== undefined && e instanceof FrontendUnsupportedError) {
|
|
169
|
+
throw bothRefusals(name, released.refusal, e);
|
|
170
|
+
}
|
|
171
|
+
throw e;
|
|
172
|
+
}
|
|
173
|
+
if (isRelift(again)) {
|
|
174
|
+
throw new Error(`internal: the frame-object audit of '${name}' asked for a second relift`);
|
|
175
|
+
}
|
|
176
|
+
return again;
|
|
61
177
|
}
|
|
62
178
|
|
|
179
|
+
/** A released relift's decline, worded as both lifts read the frame: the first audit's refusal of
|
|
180
|
+
* the frame with the homes held, then the second's with them released, which may name another
|
|
181
|
+
* obstacle (an escape the first refusal stopped short of). The first leads, so a message that
|
|
182
|
+
* only the first lift's refusal explains reads as it would with no release; the second is
|
|
183
|
+
* appended only where it says something else. */
|
|
184
|
+
function bothRefusals(
|
|
185
|
+
name: string,
|
|
186
|
+
first: FrontendUnsupportedError,
|
|
187
|
+
second: FrontendUnsupportedError,
|
|
188
|
+
): FrontendUnsupportedError {
|
|
189
|
+
const lead = `cannot lift '${name}': `;
|
|
190
|
+
const reason = second.message.startsWith(lead) ? second.message.slice(lead.length) : second.message;
|
|
191
|
+
return first.message.endsWith(reason)
|
|
192
|
+
? first
|
|
193
|
+
: new FrontendUnsupportedError(`${first.message}; with the argument home released: ${reason}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const isRelift = <F>(x: F | FrameObjectRelift): x is FrameObjectRelift =>
|
|
197
|
+
typeof x === 'object' && x !== null && 'oneObject' in x;
|
|
198
|
+
|
|
63
199
|
/** What an audit answers: lift again (`FrameObjectRelift`), the device pin the accepted function
|
|
64
200
|
* owes (`DevicePins`, applied by `pinDeviceAccesses`), or nothing, when no frame address was
|
|
65
201
|
* taken. */
|
|
@@ -247,13 +383,20 @@ export const FRAME_ESCAPE_GATES: readonly Gate<FrameEscape>[] = [
|
|
|
247
383
|
* anywhere as a VALUE — into an MMIO register (the DMA-fill idiom), a call, a phi — and it is
|
|
248
384
|
* judged in one of two models, chosen before any shape is:
|
|
249
385
|
* - PER OBJECT, the default: every MEMORY access through it must be at offset 0, with one agreed
|
|
250
|
-
* width and one agreed extension, or a
|
|
251
|
-
*
|
|
386
|
+
* width and one agreed extension, or a read or write through a runtime index into storage
|
|
387
|
+
* nothing else types — bytes, or ONE wider width, read through a cast on the bytes
|
|
388
|
+
* (`indexedWidth`) — and its bytes must belong to nothing else in the frame;
|
|
252
389
|
* - ONE OBJECT, where a device reads or a callee writes without bound and every writer is one
|
|
253
|
-
* that answer can hold (`writerNotKept
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
390
|
+
* that answer can hold (`writerNotKept`), or where no address escapes at all and the
|
|
391
|
+
* per-object model refuses (`oneObject`, which this answers and the frontend lifts again
|
|
392
|
+
* with): the local area is one `u8` array in memory, every fixed-offset access in it is a
|
|
393
|
+
* member at its own offset, a runtime index may reach it at a byte or at ONE wider
|
|
394
|
+
* width (`indexedWidth`), and two widths at one byte are the views of one union
|
|
395
|
+
* (`unionViews`), or refuse where no view names them (`unionRefusal`).
|
|
396
|
+
* In either model, a store through a capture that reaches a call unread on every path, beneath
|
|
397
|
+
* every word a load proves a local's, declines: it may be that call's stack argument
|
|
398
|
+
* (`storedNeverRead`). Which load proves it is the compiler's premise: one after a call under
|
|
399
|
+
* `localsAboveOutgoingArea`, and any load from the offset up under `outgoingAreaNeverLoadedFrom`.
|
|
257
400
|
* Any use the audit cannot vouch for declines the whole function loudly. Nothing here guesses: a
|
|
258
401
|
* scalar's declared type is exactly the access type the machine used, and an object NO access
|
|
259
402
|
* reaches is sized by the frame reservation and left untyped.
|
|
@@ -273,41 +416,92 @@ export const FRAME_ESCAPE_GATES: readonly Gate<FrameEscape>[] = [
|
|
|
273
416
|
* the fold, the split, the one-object answer and the stamp; `foldedHere`, which the split adds to
|
|
274
417
|
* and the slot-overlap refusal reads; and `ModelChoice.refused`, which the model choice and the
|
|
275
418
|
* shape stage set and the relift test reads. */
|
|
276
|
-
export function auditFrameObjects({
|
|
419
|
+
export function auditFrameObjects(audit: FrameObjectAudit): FrameObjectVerdict {
|
|
420
|
+
// AN ARGUMENT HOME HELD FOR AN ANSWER THAT DID NOT COME. The frontend keeps a by-value struct
|
|
421
|
+
// parameter's home out of the slot model (`releasableHomes`) so the one-object answer can take it
|
|
422
|
+
// whole. Where this audit declines instead, that answer withheld (an address escapes to a writer
|
|
423
|
+
// it cannot hold, such as a struct-return temp), the decline becomes a relift with the homes
|
|
424
|
+
// released. The hold is for byte-exactness, not soundness: a home's narrow reload read as the
|
|
425
|
+
// word narrowed is the value the entry stored, and the second audit judges the frame whole, so
|
|
426
|
+
// the release is value-sound whichever refusal fired. Asked of the first lift alone: a relift
|
|
427
|
+
// that keeps one object has already had the answer the homes were held for.
|
|
428
|
+
const { oneObject, releasableHomes } = audit;
|
|
429
|
+
if (oneObject !== undefined || releasableHomes === undefined || releasableHomes.size === 0) {
|
|
430
|
+
return auditFrame(audit);
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
return auditFrame(audit);
|
|
434
|
+
} catch (e) {
|
|
435
|
+
if (e instanceof FrontendUnsupportedError) {
|
|
436
|
+
return { oneObject: undefined, homesReleased: { refusal: e } };
|
|
437
|
+
}
|
|
438
|
+
throw e;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function auditFrame({
|
|
277
443
|
name,
|
|
278
444
|
irBlocks,
|
|
279
445
|
ownedLocals: owned,
|
|
280
446
|
declaredLocals: declared,
|
|
281
447
|
usedSlotOffsets,
|
|
448
|
+
loadedWords,
|
|
282
449
|
capturedObjectIsTheWholeFrame,
|
|
450
|
+
escapesReliedOn = new Set(),
|
|
283
451
|
movedCaptures,
|
|
452
|
+
directAccesses = new Set(),
|
|
284
453
|
returnsWithoutHiddenPointer,
|
|
285
454
|
symbols,
|
|
286
455
|
target,
|
|
287
456
|
gates = FRAME_ESCAPE_GATES,
|
|
457
|
+
reliftGates = RELIFT_HAND_BACK_GATES,
|
|
288
458
|
oneObject,
|
|
289
459
|
}: FrameObjectAudit): FrameObjectVerdict {
|
|
290
460
|
const { laddrs: captures, foldedHere } = foldMovedCaptures(irBlocks, movedCaptures);
|
|
291
461
|
// …and the audit runs for a licensed acceptance with no object at all, so the premise re-check
|
|
292
462
|
// is total rather than resting on "the capture always survives into the IR".
|
|
293
|
-
if (captures.length === 0 && !capturedObjectIsTheWholeFrame) {
|
|
463
|
+
if (captures.length === 0 && !capturedObjectIsTheWholeFrame && escapesReliedOn.size === 0) {
|
|
294
464
|
return undefined;
|
|
295
465
|
}
|
|
296
466
|
const fail = frameRefusal(name);
|
|
467
|
+
if (captures.length === 0 && escapesReliedOn.size > 0) {
|
|
468
|
+
recheckEscapesReliedOn(escapesReliedOn, undefined, fail);
|
|
469
|
+
}
|
|
297
470
|
const facts = frameAddressFacts(irBlocks, symbols, target);
|
|
298
|
-
const { laddrs, splitRefusal } = splitAddressingCopies(irBlocks, captures, foldedHere);
|
|
471
|
+
const { laddrs, splitRefusal } = splitAddressingCopies(irBlocks, captures, foldedHere, declaredStructs(irBlocks));
|
|
299
472
|
const objects = frameObjects(laddrs, owned, fail);
|
|
300
473
|
const flow = addressFlow(irBlocks, objects, facts, fail);
|
|
301
|
-
const uses = classifyFrameUses({
|
|
474
|
+
const uses = classifyFrameUses({
|
|
475
|
+
irBlocks,
|
|
476
|
+
objects,
|
|
477
|
+
flow,
|
|
478
|
+
facts,
|
|
479
|
+
splitRefusal,
|
|
480
|
+
oneObject,
|
|
481
|
+
target,
|
|
482
|
+
fail,
|
|
483
|
+
});
|
|
302
484
|
const windows = escapeWindows(irBlocks, uses, flow, facts, target);
|
|
303
|
-
const model = chooseFrameModel(oneObject, uses, windows, returnsWithoutHiddenPointer, fail);
|
|
485
|
+
const model = chooseFrameModel({ oneObject, uses, windows, returnsWithoutHiddenPointer, fail });
|
|
304
486
|
if (capturedObjectIsTheWholeFrame) {
|
|
305
487
|
recheckWholeFramePremise({ uses, returnsWithoutHiddenPointer, fail });
|
|
306
488
|
}
|
|
489
|
+
recheckEscapesReliedOn(escapesReliedOn, uses, fail);
|
|
490
|
+
storedNeverRead(irBlocks, uses, loadedWords, target, fail);
|
|
307
491
|
// The device pin's trigger: the `readOnlyAddressSinks` registers a frame address was stored to.
|
|
308
492
|
const sinks = [...new Set([...uses.sourceStores.values()].flat().map((s) => s.sink))];
|
|
309
493
|
if (oneObject !== undefined) {
|
|
310
|
-
keepAsOneObject({
|
|
494
|
+
keepAsOneObject({
|
|
495
|
+
oneObject,
|
|
496
|
+
irBlocks,
|
|
497
|
+
objects,
|
|
498
|
+
uses,
|
|
499
|
+
usedSlotOffsets,
|
|
500
|
+
returnsWithoutHiddenPointer,
|
|
501
|
+
arrayOrder: target.compilerBehaviors.addressTakenArrayOrder,
|
|
502
|
+
arrayAlignment: target.compilerBehaviors.addressTakenArrayAlignment,
|
|
503
|
+
fail,
|
|
504
|
+
});
|
|
311
505
|
return { policy: 'one-object', sinks };
|
|
312
506
|
}
|
|
313
507
|
const shapes = objectShapes({
|
|
@@ -321,20 +515,32 @@ export function auditFrameObjects({
|
|
|
321
515
|
fail,
|
|
322
516
|
});
|
|
323
517
|
const escapes = frameEscapes({ irBlocks, uses, windows, shapes, owned, declared, usedSlotOffsets });
|
|
324
|
-
// THE ONE-OBJECT ANSWER, where it is on offer (one escape reaches without
|
|
325
|
-
// writer is one it can hold
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
518
|
+
// THE ONE-OBJECT ANSWER, where it is on offer (`chooseFrameModel`: one escape reaches without
|
|
519
|
+
// bound and every writer is one it can hold, or no address escapes at all) and the per-object
|
|
520
|
+
// model does not describe the frame: it refused a shape, an object sits over a slot, or the
|
|
521
|
+
// unbounded reach meets another object, a slot or a word nothing accounts for. Then what the
|
|
522
|
+
// device may read or the callee may write — or, with no escape, only this function's own
|
|
523
|
+
// accesses — is kept rather than refused: lift again with the local area as one object in memory
|
|
329
524
|
// (`keepAsOneObject`). Below the local area are the outgoing arguments and above it the saved
|
|
330
525
|
// registers, neither of them an object. Where the per-object model does describe the frame —
|
|
331
526
|
// one object and nothing else in reach — it stands, and the object keeps its own type.
|
|
527
|
+
// An escape `misses` bounded is asked as the unbounded read it was without that proof
|
|
528
|
+
// (`ModelChoice.unboundedAt`): the proof may add a per-object lift, never take the one-object one.
|
|
332
529
|
const answered = (e: FrameEscape): boolean =>
|
|
333
|
-
e.
|
|
334
|
-
e.hi === Infinity &&
|
|
530
|
+
model.unboundedAt.has(e.off) &&
|
|
335
531
|
(e.objectReached !== undefined || e.slotReached !== undefined || e.unaccountedWord !== undefined);
|
|
336
532
|
if (model.onOffer && (model.refused || shapes.overSlot.length > 0 || escapes.some(answered))) {
|
|
337
|
-
return {
|
|
533
|
+
return {
|
|
534
|
+
oneObject: reliftRange({
|
|
535
|
+
declared,
|
|
536
|
+
objects,
|
|
537
|
+
uses,
|
|
538
|
+
usedSlotOffsets,
|
|
539
|
+
directAccesses,
|
|
540
|
+
splits: splitsOneObject(target),
|
|
541
|
+
gates: reliftGates,
|
|
542
|
+
}),
|
|
543
|
+
};
|
|
338
544
|
}
|
|
339
545
|
const failWithheld: Refuse = (why) => fail(model.withheld(why));
|
|
340
546
|
for (const [off, width] of shapes.overSlot) {
|
|
@@ -352,6 +558,147 @@ export function auditFrameObjects({
|
|
|
352
558
|
return { policy: 'per-object', sinks };
|
|
353
559
|
}
|
|
354
560
|
|
|
561
|
+
/** THE BYTES THE ONE-OBJECT RELIFT KEEPS: the declared local area, less the words at either end
|
|
562
|
+
* that are a scalar's and nothing else's — a word the slot model keys and no access names, or one
|
|
563
|
+
* the frontend reached only by direct whole-word accesses (`directAccesses`), which escapes
|
|
564
|
+
* nowhere — and, past such a word, words nothing touches at all. The frontend lifts a word
|
|
565
|
+
* outside the range with its slot model (Thumb `isFrameObjectAccess`, MIPS `keptInRelift`; PPC
|
|
566
|
+
* is never handed a narrowed range, `no-split-follows`), so the one object is the bytes its
|
|
567
|
+
* escapes and accesses reach, and `provenParts` can split it where they tile: agbcc's reload puts
|
|
568
|
+
* spill slots ABOVE the arrays (the Thumb frame grows up, thumb.h:569/573, reload1.c:2274),
|
|
569
|
+
* ido7.1 puts them below, and gcc 2.7.2 leaves a pad word between (each array starts at a
|
|
570
|
+
* multiple of 8); kept in the one object, such a word joins the part beside it.
|
|
571
|
+
*
|
|
572
|
+
* A word is handed back only where no rule of `RELIFT_HAND_BACK_GATES` keeps it: a reach through
|
|
573
|
+
* the object, or no split to follow. What makes a word a scalar's (`scalarWord`) is not tabled,
|
|
574
|
+
* and its clauses are not each measured for soundness: a word it gets wrong meets the second
|
|
575
|
+
* audit's refusals where an object or an access lands outside the kept bytes (`keepAsOneObject`).
|
|
576
|
+
*
|
|
577
|
+
* The copies do NOT tell a spill from a struct member only stored (`struct { u8 a[16], b[16];
|
|
578
|
+
* u32 y; }` with `s.y = x` and nothing reading it): that member is handed back too, and its store
|
|
579
|
+
* drops. No value changes, since nothing reads the word through the object (an index or an
|
|
580
|
+
* escape reaching it keeps it); what is lost is one store the target made. */
|
|
581
|
+
function reliftRange({
|
|
582
|
+
declared,
|
|
583
|
+
objects,
|
|
584
|
+
uses,
|
|
585
|
+
usedSlotOffsets,
|
|
586
|
+
directAccesses,
|
|
587
|
+
splits,
|
|
588
|
+
gates,
|
|
589
|
+
}: {
|
|
590
|
+
declared: FrameRange;
|
|
591
|
+
objects: ReadonlyMap<number, readonly Op[]>;
|
|
592
|
+
uses: FrameUses;
|
|
593
|
+
usedSlotOffsets: ReadonlySet<number>;
|
|
594
|
+
directAccesses: ReadonlySet<Value>;
|
|
595
|
+
splits: boolean;
|
|
596
|
+
gates: readonly Gate<HandBack>[];
|
|
597
|
+
}): FrameRange {
|
|
598
|
+
const whole = { from: declared.from, to: declared.to };
|
|
599
|
+
const reaches = frameReaches(uses);
|
|
600
|
+
const fills = copyStarts(reaches);
|
|
601
|
+
const kept = (word: number): boolean => firstRejection(gates, { word, reaches, fills, splits }) !== null;
|
|
602
|
+
const scalarWord = (w: number, pastScalar: boolean): boolean => {
|
|
603
|
+
const touching = uses.fixed.filter((f) => overlaps(f.at, f.width, w, 4));
|
|
604
|
+
const owners = [...objects.keys()].filter((o) => overlaps(o, 1, w, 4));
|
|
605
|
+
if ([...usedSlotOffsets].some((s) => s !== w && overlaps(s, 4, w, 4))) {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
if (usedSlotOffsets.has(w) || touching.length === 0) {
|
|
609
|
+
// …and past one, a word nothing touches: no value lives in it either way
|
|
610
|
+
return owners.length === 0 && touching.length === 0 && (usedSlotOffsets.has(w) || pastScalar);
|
|
611
|
+
}
|
|
612
|
+
return (
|
|
613
|
+
owners.length === 1 &&
|
|
614
|
+
owners[0] === w &&
|
|
615
|
+
!uses.escaped.has(w) &&
|
|
616
|
+
uses.useCount.get(w) === (uses.accesses.get(w)?.length ?? 0) &&
|
|
617
|
+
touching.every((f) => {
|
|
618
|
+
const by = uses.accessOps.get(f.op);
|
|
619
|
+
return f.at === w && f.width === 4 && by?.off === w && directAccesses.has(by.through);
|
|
620
|
+
})
|
|
621
|
+
);
|
|
622
|
+
};
|
|
623
|
+
const handedBack = (w: number, pastScalar: boolean): boolean => !kept(w) && scalarWord(w, pastScalar);
|
|
624
|
+
let { from, to } = whole;
|
|
625
|
+
while (to - from > 4 && handedBack(to - 4, to < whole.to)) {
|
|
626
|
+
to -= 4;
|
|
627
|
+
}
|
|
628
|
+
while (to - from > 4 && handedBack(from, from > whole.from)) {
|
|
629
|
+
from += 4;
|
|
630
|
+
}
|
|
631
|
+
return { from, to };
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** Whether `keepAsOneObject` may split the one object on this target at all: where it places
|
|
635
|
+
* address-taken arrays in a known order and at a known alignment. */
|
|
636
|
+
function splitsOneObject(target: TargetDescription): boolean {
|
|
637
|
+
return (
|
|
638
|
+
target.compilerBehaviors.addressTakenArrayOrder !== undefined &&
|
|
639
|
+
target.compilerBehaviors.addressTakenArrayAlignment !== undefined
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** What `RELIFT_HAND_BACK_GATES` asks of one end word of the local area: the word, and every
|
|
644
|
+
* escape and runtime index of the frame as the bytes each may reach (`frameReaches`). */
|
|
645
|
+
export interface HandBack {
|
|
646
|
+
readonly word: number;
|
|
647
|
+
/** every escape and runtime index, with the bytes each may reach */
|
|
648
|
+
readonly reaches: readonly OwnedReach[];
|
|
649
|
+
/** where a bounded copy writes into the frame */
|
|
650
|
+
readonly fills: ReadonlySet<number>;
|
|
651
|
+
/** the target may split the one object (`splitsOneObject`) */
|
|
652
|
+
readonly splits: boolean;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** WHAT KEEPS AN END WORD IN THE ONE OBJECT (`reliftRange`): a reach through the object that may
|
|
656
|
+
* land on it, or no split to gain by handing it back. A word handed back is a local whose address
|
|
657
|
+
* nothing takes, so the recompile keeps it in a register or drops its store, and whatever reached
|
|
658
|
+
* it through the object in the source reads or writes another byte. */
|
|
659
|
+
export const RELIFT_HAND_BACK_GATES: readonly Gate<HandBack>[] = [
|
|
660
|
+
// `struct S { u8 a[16], b[16]; u32 y; } s; … s.y = x; h(s.a, 16); g(s.b); return s.y + s.a[3];`
|
|
661
|
+
// on agbcc: `g` may write `s.y` through the address it was handed, and the target reads it back
|
|
662
|
+
{
|
|
663
|
+
id: 'escape-without-bound',
|
|
664
|
+
why: 'a callee handed an address nothing bounds may reach any byte of the frame from it',
|
|
665
|
+
sound: true,
|
|
666
|
+
guardedBy: 'frame-audit-stages.test.ts: keeps every word where an escape no literal count bounds may reach it',
|
|
667
|
+
rejects: (c) => c.reaches.some((r) => r.by === 'escape' && r.reach === null),
|
|
668
|
+
},
|
|
669
|
+
// `memcpy(s + 4, src, 8)` over a slot keyed at 8 writes the word the slot model would keep
|
|
670
|
+
{
|
|
671
|
+
id: 'copy-reaches-word',
|
|
672
|
+
why: 'a block copy into or out of the frame names the bytes it moves, and this word is one of them',
|
|
673
|
+
sound: true,
|
|
674
|
+
guardedBy: 'frame-audit-stages.test.ts: keeps a word a bounded copy reaches',
|
|
675
|
+
rejects: (c) => c.reaches.some((r) => r.by === 'escape' && r.reach !== null && mayTouch(r.reach, c.word, 4)),
|
|
676
|
+
},
|
|
677
|
+
// An index nothing bounds reaches every byte of the object it is based in, so no split follows
|
|
678
|
+
// it (`provenParts` keeps the one object whole), and a word handed back would only leave the
|
|
679
|
+
// bytes the index may reach. `*(u32 *)(s + 32) = x; return s[16 + i];` on agbcc is spelled
|
|
680
|
+
// exactly as a spill slot beside an index is. An index whose range is proven (its `reach`:
|
|
681
|
+
// `s[16 + (i & 7) * 2]`) reaches only the bytes it names, so a word outside every such reach is
|
|
682
|
+
// read through no index, and handing it back loses nothing an index could read.
|
|
683
|
+
{
|
|
684
|
+
id: 'runtime-index',
|
|
685
|
+
why: 'a runtime index may reach this word: nothing bounds it, or its bounded reach covers it',
|
|
686
|
+
sound: true,
|
|
687
|
+
guardedBy: 'thumb-frontend.test.ts: a word a runtime index may reach is not handed back to the slot model',
|
|
688
|
+
rejects: (c) => c.reaches.some((r) => r.by === 'index' && mayTouch(r.reach, c.word, 4)),
|
|
689
|
+
},
|
|
690
|
+
// Handing back buys a split and nothing else: two or more copies with a literal count must fill
|
|
691
|
+
// the area, on a target that splits at all (PPC_MWCC places address-taken arrays in no order
|
|
692
|
+
// `target.ts` records). Elsewhere it would only cost a stored-only member's store.
|
|
693
|
+
{
|
|
694
|
+
id: 'no-split-follows',
|
|
695
|
+
why: 'a word is handed back only so that the copies can split the one object',
|
|
696
|
+
sound: false,
|
|
697
|
+
guardedBy: 'frame-audit-stages.test.ts: keeps every word where no split could follow',
|
|
698
|
+
rejects: (c) => c.fills.size < 2 || !c.splits,
|
|
699
|
+
},
|
|
700
|
+
];
|
|
701
|
+
|
|
355
702
|
/** Refuses the whole function, naming the address-taken local as what it cannot lift. */
|
|
356
703
|
type Refuse = (why: string) => never;
|
|
357
704
|
|
|
@@ -361,6 +708,42 @@ function frameRefusal(name: string): Refuse {
|
|
|
361
708
|
};
|
|
362
709
|
}
|
|
363
710
|
|
|
711
|
+
/** THE FRAME BYTES A DECLARATION TYPES AS ONE STRUCT: a call's return storage (`sret`) and a
|
|
712
|
+
* struct passed by value (`byValue`, call-plan.ts `foldByValueArgs`), where the operand is an
|
|
713
|
+
* `laddr`. */
|
|
714
|
+
function declaredStructs(irBlocks: readonly Block[]): FrameRange[] {
|
|
715
|
+
const laddrOff = new Map<Value, number>();
|
|
716
|
+
for (const b of irBlocks) {
|
|
717
|
+
for (const op of b.ops) {
|
|
718
|
+
if (op.opcode === 'laddr') {
|
|
719
|
+
laddrOff.set(op.results[0], op.attrs.off as number);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const out: FrameRange[] = [];
|
|
724
|
+
for (const b of irBlocks) {
|
|
725
|
+
for (const op of b.ops) {
|
|
726
|
+
if (op.opcode !== 'call') {
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
const t = op.results[0]?.type;
|
|
730
|
+
const from = laddrOff.get(op.operands[0]);
|
|
731
|
+
if (op.attrs.sret === true && from !== undefined && t?.kind === 'struct' && t.size !== undefined) {
|
|
732
|
+
out.push({ from, to: from + t.size });
|
|
733
|
+
}
|
|
734
|
+
for (const { at } of byValueOperands(op)) {
|
|
735
|
+
const v = op.operands[at];
|
|
736
|
+
const off = laddrOff.get(v);
|
|
737
|
+
const t = v?.type.kind === 'ptr' ? v.type.to : undefined;
|
|
738
|
+
if (off !== undefined && t?.kind === 'struct' && t.size !== undefined) {
|
|
739
|
+
out.push({ from: off, to: off + t.size });
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return out;
|
|
745
|
+
}
|
|
746
|
+
|
|
364
747
|
/** A CAPTURE MOVED BY A CONSTANT IS THE CAPTURE AT THE SUM, and here the constant is exact: an
|
|
365
748
|
* `add` of a `laddr` and a `const` — a register the lift could not see through, `mov r0, sp /
|
|
366
749
|
* movs r2, #0 / ldrsh r1, [r0, r2]`, or a move the pre-lift walk does not follow — is re-minted
|
|
@@ -369,6 +752,22 @@ function frameRefusal(name: string): Refuse {
|
|
|
369
752
|
* lands on a keyed slot is refused as the move, the capability that is missing
|
|
370
753
|
* (`failIfSlotKeysIt`).
|
|
371
754
|
*
|
|
755
|
+
* AN INDEX INTO A CAPTURE, MOVED BY A NONZERO CONSTANT, IS THE SAME INDEX INTO THE CAPTURE AT THE
|
|
756
|
+
* SUM. agbcc spells `a[i]` on a local at [sp,#k) as the frame base plus the index, moved after:
|
|
757
|
+
* `mov r0, sp / add r0, r0, r4 / add r0, #0x4` or `add r0, r0, sp / add r0, #0x4` — the bytes
|
|
758
|
+
* `add r0, sp, #0x4 / add r0, r0, r4` reach, so `add(add(laddr@a, i), k)` is re-minted as
|
|
759
|
+
* `add(laddr@(a+k), i)`, and the inner sum and capture are dropped when nothing else reads them.
|
|
760
|
+
* The object at [sp,#a) is often the outgoing-argument word (a 5-argument call beside
|
|
761
|
+
* the array), which the index never touches. Zero is left alone: `add(sum, 0)` is agbcc's
|
|
762
|
+
* `mov r1, #0x0 / ldrsb r0, [r0, r1]`, which `indexedAccess` judges where the register put it.
|
|
763
|
+
* The rebased object is held to every rule an object at a+k is held to, so a displaced FIELD of a
|
|
764
|
+
* struct element — no object of its own at a+k, the struct's other bytes at a — lands as a second
|
|
765
|
+
* object beside the first and is refused or lifted as one object by the same rules. A load or
|
|
766
|
+
* store's own nonzero displacement on the sum is the same move: `ldrb r0, [r0, #0x1]` (the field
|
|
767
|
+
* agbcc reads unsigned) and `mov r1, #0x1 / ldrsb r0, [r0, r1]` (signed) reach the same byte, so
|
|
768
|
+
* the access is re-pointed at `add(laddr@(a+d), i)` with displacement 0, and the verdict does not
|
|
769
|
+
* depend on where the literal sits.
|
|
770
|
+
*
|
|
372
771
|
* Returns every `laddr` that survives, in block order. */
|
|
373
772
|
function foldMovedCaptures(
|
|
374
773
|
irBlocks: Block[],
|
|
@@ -387,23 +786,77 @@ function foldMovedCaptures(
|
|
|
387
786
|
}
|
|
388
787
|
}
|
|
389
788
|
}
|
|
789
|
+
// the `add` of a capture and a runtime value that defines each value, for the index rebase
|
|
790
|
+
const indexOf = new Map<Value, { base: Op; index: Value; sum: Op }>();
|
|
791
|
+
const rebased = new Set<Op>();
|
|
390
792
|
for (let changed = true; changed;) {
|
|
391
793
|
changed = false;
|
|
392
794
|
for (const blk of irBlocks) {
|
|
393
|
-
blk.ops = blk.ops.
|
|
795
|
+
blk.ops = blk.ops.flatMap((op) => {
|
|
394
796
|
const [x, y] = op.operands;
|
|
797
|
+
const sum0 = (op.opcode === 'load' || op.opcode === 'store') && x !== undefined ? indexOf.get(x) : undefined;
|
|
798
|
+
const disp = (op.attrs.off as number | undefined) ?? 0;
|
|
799
|
+
if (sum0 !== undefined && disp !== 0) {
|
|
800
|
+
const object = mkOp('laddr', {
|
|
801
|
+
results: [mkValue(sum0.base.results[0].type)],
|
|
802
|
+
attrs: { off: ((sum0.base.attrs.off as number) + disp) | 0 },
|
|
803
|
+
});
|
|
804
|
+
const moves = mkOp('add', {
|
|
805
|
+
results: [mkValue(x.type)],
|
|
806
|
+
operands: [object.results[0], sum0.index],
|
|
807
|
+
attrs: sum0.sum.attrs,
|
|
808
|
+
});
|
|
809
|
+
laddrOf.set(object.results[0], object);
|
|
810
|
+
indexOf.set(moves.results[0], { base: object, index: sum0.index, sum: moves });
|
|
811
|
+
moved.add(sum0.base.results[0]);
|
|
812
|
+
foldedHere.add(object);
|
|
813
|
+
rebased.add(sum0.sum);
|
|
814
|
+
op.operands[0] = moves.results[0];
|
|
815
|
+
op.attrs = { ...op.attrs, off: 0 };
|
|
816
|
+
changed = true;
|
|
817
|
+
return [object, moves, op];
|
|
818
|
+
}
|
|
819
|
+
if (op.opcode !== 'add' || op.operands.length !== 2) {
|
|
820
|
+
return [op];
|
|
821
|
+
}
|
|
395
822
|
const base = laddrOf.get(x) ?? laddrOf.get(y);
|
|
396
823
|
const by = constOf.get(laddrOf.has(x) ? y : x);
|
|
397
|
-
if (
|
|
398
|
-
|
|
824
|
+
if (base !== undefined && by !== undefined) {
|
|
825
|
+
const off = ((base.attrs.off as number) + by) | 0;
|
|
826
|
+
const object = mkOp('laddr', { results: op.results, attrs: { off } });
|
|
827
|
+
laddrOf.set(op.results[0], object);
|
|
828
|
+
moved.add(base.results[0]);
|
|
829
|
+
foldedHere.add(object);
|
|
830
|
+
changed = true;
|
|
831
|
+
return [object];
|
|
832
|
+
}
|
|
833
|
+
const index = base === undefined ? undefined : laddrOf.has(x) ? y : x;
|
|
834
|
+
if (base !== undefined && index !== undefined && !laddrOf.has(index)) {
|
|
835
|
+
// a sum first seen here takes another pass: a use laid out in an EARLIER block was
|
|
836
|
+
// visited before the sum was known, and is moved only on the next one
|
|
837
|
+
if (!indexOf.has(op.results[0])) {
|
|
838
|
+
indexOf.set(op.results[0], { base, index, sum: op });
|
|
839
|
+
changed = true;
|
|
840
|
+
}
|
|
841
|
+
return [op];
|
|
399
842
|
}
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
843
|
+
const sum = indexOf.get(x) ?? indexOf.get(y);
|
|
844
|
+
const k = constOf.get(indexOf.has(x) ? y : x);
|
|
845
|
+
if (sum === undefined || k === undefined || k === 0) {
|
|
846
|
+
return [op];
|
|
847
|
+
}
|
|
848
|
+
const object = mkOp('laddr', {
|
|
849
|
+
results: [mkValue(sum.base.results[0].type)],
|
|
850
|
+
attrs: { off: ((sum.base.attrs.off as number) + k) | 0 },
|
|
851
|
+
});
|
|
852
|
+
const moves = mkOp('add', { results: op.results, operands: [object.results[0], sum.index], attrs: op.attrs });
|
|
853
|
+
laddrOf.set(object.results[0], object);
|
|
854
|
+
indexOf.set(op.results[0], { base: object, index: sum.index, sum: moves });
|
|
855
|
+
moved.add(sum.base.results[0]);
|
|
404
856
|
foldedHere.add(object);
|
|
857
|
+
rebased.add(sum.sum);
|
|
405
858
|
changed = true;
|
|
406
|
-
return object;
|
|
859
|
+
return [object, moves];
|
|
407
860
|
});
|
|
408
861
|
}
|
|
409
862
|
}
|
|
@@ -412,13 +865,24 @@ function foldMovedCaptures(
|
|
|
412
865
|
// the write-back of `stmia r1!, {r2, r3, r4}` through a capture is `add r1, r1, #12`, a pointer
|
|
413
866
|
// to the end of what was copied that the machine never uses. Only those: an unused `mov rD, sp`
|
|
414
867
|
// of the machine's own is still a capture, and is judged.
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
for (const
|
|
418
|
-
|
|
419
|
-
|
|
868
|
+
const readNow = (): Set<Value> => {
|
|
869
|
+
const read = new Set<Value>();
|
|
870
|
+
for (const blk of irBlocks) {
|
|
871
|
+
for (const op of blk.ops) {
|
|
872
|
+
op.operands.forEach((v) => read.add(v));
|
|
873
|
+
(op.successors ?? []).forEach((s) => s.args.forEach((v) => read.add(v)));
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return read;
|
|
877
|
+
};
|
|
878
|
+
// …and an index sum a rebase moved off, once nothing reads it, goes the same way first
|
|
879
|
+
if (rebased.size > 0) {
|
|
880
|
+
const before = readNow();
|
|
881
|
+
for (const blk of irBlocks) {
|
|
882
|
+
blk.ops = blk.ops.filter((op) => !rebased.has(op) || before.has(op.results[0]));
|
|
420
883
|
}
|
|
421
884
|
}
|
|
885
|
+
const read = readNow();
|
|
422
886
|
const laddrs: Op[] = [];
|
|
423
887
|
for (const blk of irBlocks) {
|
|
424
888
|
blk.ops = blk.ops.filter(
|
|
@@ -474,7 +938,7 @@ function frameAddressFacts(
|
|
|
474
938
|
}
|
|
475
939
|
for (const [lit, index] of [d.operands, [...d.operands].reverse()]) {
|
|
476
940
|
const from = literalAddrOf(lit);
|
|
477
|
-
const stride = from === undefined ? 0 :
|
|
941
|
+
const stride = from === undefined ? 0 : linearFactor({ defs: defOf, constOf: addresses.constOf }, index);
|
|
478
942
|
if (from !== undefined && readOnlySinks.has(from + off) && stride > 0 && stride % sinkSpacing === 0) {
|
|
479
943
|
return [...readOnlySinks].filter((s) => (s - (from + off)) % stride === 0);
|
|
480
944
|
}
|
|
@@ -484,47 +948,6 @@ function frameAddressFacts(
|
|
|
484
948
|
return { ...addresses, readsThrough };
|
|
485
949
|
}
|
|
486
950
|
|
|
487
|
-
// `v` as a sum of runtime values times constants, plus a constant (the `1` key): agbcc's `ch *
|
|
488
|
-
// 12` is `((ch << 1) + ch) << 2`, which is 12 times `ch` only once the two `ch` terms are added.
|
|
489
|
-
// Undefined past what this can read.
|
|
490
|
-
function linearOf(facts: LiteralAddresses, v: Value, depth = 0): Map<Value | 1, number> | undefined {
|
|
491
|
-
const c = facts.constOf(v);
|
|
492
|
-
if (c !== undefined) {
|
|
493
|
-
return new Map([[1, c]]);
|
|
494
|
-
}
|
|
495
|
-
const d = facts.defOf.get(v);
|
|
496
|
-
if (d === undefined || depth > 8) {
|
|
497
|
-
return new Map([[v, 1]]);
|
|
498
|
-
}
|
|
499
|
-
const scaled = (x: Value, by: number) => {
|
|
500
|
-
const l = linearOf(facts, x, depth + 1);
|
|
501
|
-
return l && new Map([...l].map(([t, k]) => [t, k * by] as const));
|
|
502
|
-
};
|
|
503
|
-
const k = d.operands.length === 1 ? (d.attrs.imm as number | undefined) : facts.constOf(d.operands[1]);
|
|
504
|
-
if (d.opcode === 'shl' && k !== undefined && k >= 0 && k < 31) {
|
|
505
|
-
return scaled(d.operands[0], 2 ** k);
|
|
506
|
-
}
|
|
507
|
-
if (d.opcode === 'mul' && d.operands.length === 2 && k !== undefined) {
|
|
508
|
-
return scaled(d.operands[0], k);
|
|
509
|
-
}
|
|
510
|
-
if ((d.opcode === 'add' || d.opcode === 'sub') && d.operands.length === 2) {
|
|
511
|
-
const x = linearOf(facts, d.operands[0], depth + 1);
|
|
512
|
-
const y = scaled(d.operands[1], d.opcode === 'add' ? 1 : -1);
|
|
513
|
-
if (x === undefined || y === undefined) {
|
|
514
|
-
return undefined;
|
|
515
|
-
}
|
|
516
|
-
y.forEach((n, t) => x.set(t, (x.get(t) ?? 0) + n));
|
|
517
|
-
return x;
|
|
518
|
-
}
|
|
519
|
-
return new Map([[v, 1]]);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// A number every value of `v` is a multiple of, or 0 when this cannot name one.
|
|
523
|
-
function factorOf(facts: LiteralAddresses, v: Value): number {
|
|
524
|
-
const l = linearOf(facts, v);
|
|
525
|
-
return l === undefined ? 0 : [...l.values()].reduce(gcd, 0);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
951
|
/** A FRAME BASE ADDRESSED THROUGH IS NOT A CAPTURE. Thumb-1 gives `ldr`/`str` an `[sp,#imm]`
|
|
529
952
|
* encoding and gives the sub-word forms none, so a byte or halfword spill can only be spelled
|
|
530
953
|
* by copying sp into a register and addressing through the copy:
|
|
@@ -556,6 +979,7 @@ function splitAddressingCopies(
|
|
|
556
979
|
irBlocks: Block[],
|
|
557
980
|
laddrs: Op[],
|
|
558
981
|
foldedHere: Set<Op>,
|
|
982
|
+
structs: readonly FrameRange[] = [],
|
|
559
983
|
): { laddrs: Op[]; splitRefusal: Map<Value, string> } {
|
|
560
984
|
const splitRefusal = new Map<Value, string>();
|
|
561
985
|
const uses = new Map<Value, { op: Op; idx: number; blk: Block }[]>();
|
|
@@ -593,13 +1017,42 @@ function splitAddressingCopies(
|
|
|
593
1017
|
if (at.length === 0 || accesses.length !== at.length) {
|
|
594
1018
|
continue;
|
|
595
1019
|
}
|
|
1020
|
+
// AN ADDRESS STRICTLY INSIDE A DECLARED STRUCT, ONLY ACCESSED, IS A MEMBER ACCESS OF THAT
|
|
1021
|
+
// STRUCT. A word store into a struct passed by value goes straight to `[sp,#k]` — `x.c = w` is
|
|
1022
|
+
// `str r1, [sp, #0x4]` over `struct F {u16 a,b; u32 c; s8 e,f;}` at [sp,#0) — and the frontend
|
|
1023
|
+
// mints that access its own `laddr` at k. The declaration says those bytes are the struct's,
|
|
1024
|
+
// so the address is re-rooted on the struct's base with the displacement on the access, and
|
|
1025
|
+
// the audit judges one object rather than two that overlap. Any use that is not an access —
|
|
1026
|
+
// `&x.c` handed on — is an interior pointer, which is not this shape and is left to refuse.
|
|
1027
|
+
const off = capture.attrs.off as number;
|
|
1028
|
+
const holders = structs.filter((r) => r.from < off && off < r.to);
|
|
1029
|
+
const holder = holders[0];
|
|
1030
|
+
const inHolder = (u: { op: Op }): boolean =>
|
|
1031
|
+
off + (u.op.attrs.off as number) >= holder!.from &&
|
|
1032
|
+
off + (u.op.attrs.off as number) + (u.op.attrs.width as number) <= holder!.to;
|
|
1033
|
+
if (
|
|
1034
|
+
holder !== undefined &&
|
|
1035
|
+
holders.every((r) => r.from === holder.from && r.to === holder.to) &&
|
|
1036
|
+
accesses.every(inHolder)
|
|
1037
|
+
) {
|
|
1038
|
+
for (const u of accesses) {
|
|
1039
|
+
u.op.attrs = { ...u.op.attrs, off: off - holder.from + (u.op.attrs.off as number) };
|
|
1040
|
+
}
|
|
1041
|
+
capture.attrs = { ...capture.attrs, off: holder.from };
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
596
1044
|
if (!subWord) {
|
|
597
1045
|
splitRefusal.set(capture.results[0], 'a WORD access through the copy, and `ldr`/`str` have an `[sp,#imm]` form');
|
|
598
1046
|
continue;
|
|
599
1047
|
}
|
|
600
1048
|
// Nothing to split when the capture already names ONE object: every access at offset 0 is
|
|
601
|
-
// the frame base itself, which is what the DMA-fill idiom captures
|
|
602
|
-
|
|
1049
|
+
// the frame base itself, which is what the DMA-fill idiom captures — or every access lands in
|
|
1050
|
+
// the declared struct the capture is the start of (`declaredStructs`), whose members they are.
|
|
1051
|
+
const base = capture.attrs.off as number;
|
|
1052
|
+
const into = (r: FrameRange): boolean =>
|
|
1053
|
+
base === r.from &&
|
|
1054
|
+
accesses.every((u) => base + (u.op.attrs.off as number) + (u.op.attrs.width as number) <= r.to);
|
|
1055
|
+
if (accesses.every((u) => u.op.attrs.off === 0) || structs.some(into)) {
|
|
603
1056
|
continue;
|
|
604
1057
|
}
|
|
605
1058
|
for (const u of accesses) {
|
|
@@ -644,8 +1097,16 @@ function frameObjects(laddrs: readonly Op[], owned: FrameRange, fail: Refuse): M
|
|
|
644
1097
|
// saved register's — `g(&e)` for a fifth parameter `e` is `add r0, sp, #0x8` over a one-word
|
|
645
1098
|
// local area — and no other rule of the audit is about that gap. The TOP itself is also C's
|
|
646
1099
|
// one-past-the-end pointer, `g(buf, buf + 8)` over an 8-byte array, and the refusal names both
|
|
647
|
-
// readings.
|
|
1100
|
+
// readings. BELOW IT is the outgoing block a call's stack words are staged in, and on O32 also
|
|
1101
|
+
// the saves IDO keeps under its locals: an address there is no local's either. Every object's
|
|
1102
|
+
// extent is held to the area again below; this says why, for the frontend that supplied it.
|
|
648
1103
|
for (const off of objects.keys()) {
|
|
1104
|
+
if (off < owned.from) {
|
|
1105
|
+
fail(
|
|
1106
|
+
`the captured address at [sp,#${off}) is below the reserved local area, which starts at [sp,#${owned.from}) — ` +
|
|
1107
|
+
'an outgoing stack argument word or a saved register, whose address is not modelled',
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
649
1110
|
if (off === owned.to) {
|
|
650
1111
|
fail(
|
|
651
1112
|
`the captured address at [sp,#${off}) is the top of the reserved local area of ${owned.to - owned.from} bytes — ` +
|
|
@@ -764,10 +1225,70 @@ interface FrameAccess {
|
|
|
764
1225
|
}
|
|
765
1226
|
|
|
766
1227
|
/** A load or store judged an access to the object at `off`, and the address value it reaches the
|
|
767
|
-
* object through: a capture, or a phi that carries one — for a runtime index, the indexed one
|
|
1228
|
+
* object through: a capture, or a phi that carries one — for a runtime index, the indexed one,
|
|
1229
|
+
* and then `index` is the access as `FrameUses.indexed` records it. Without `index` the access
|
|
1230
|
+
* is at its own displacement from `through`. */
|
|
768
1231
|
interface FrameAccessOp {
|
|
769
1232
|
readonly off: number;
|
|
770
1233
|
readonly through: Value;
|
|
1234
|
+
readonly index?: IndexedAccess;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/** THE FRAME BYTES `[lo, hi)` ONE USE MAY TOUCH, or `null` where nothing bounds it, which is every
|
|
1238
|
+
* byte: a runtime index's reach (`FrameUses.indexed`) and an escape's extent
|
|
1239
|
+
* (`FrameUses.escapeExtents`) are each one. */
|
|
1240
|
+
type FrameReach = { readonly lo: number; readonly hi: number } | null;
|
|
1241
|
+
|
|
1242
|
+
/** Whether a use reaching `r` may touch any of the `width` bytes at `lo`: every byte where
|
|
1243
|
+
* nothing bounds it. */
|
|
1244
|
+
function mayTouch(r: FrameReach, lo: number, width: number): boolean {
|
|
1245
|
+
return r === null || overlaps(lo, width, r.lo, r.hi - r.lo);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/** Whether two reaches may share a byte. */
|
|
1249
|
+
function mayShare(a: FrameReach, b: FrameReach): boolean {
|
|
1250
|
+
return b === null || mayTouch(a, b.lo, b.hi - b.lo);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/** One reach through the frame, with the object whose address it starts from (`owner`), what
|
|
1254
|
+
* makes it (`by`: an escape of that address, or a runtime index into it), and `into` where it is
|
|
1255
|
+
* a bounded block copy that WRITES the bytes (the address is its destination). */
|
|
1256
|
+
interface OwnedReach {
|
|
1257
|
+
readonly owner: number;
|
|
1258
|
+
readonly by: 'escape' | 'index';
|
|
1259
|
+
readonly reach: FrameReach;
|
|
1260
|
+
readonly into: boolean;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/** EVERY REACH THROUGH THE FRAME, the one list the split of the one object (`provenParts`) and
|
|
1264
|
+
* the words handed back to the slot model (`reliftRange`) read: each escape's extent, and each
|
|
1265
|
+
* runtime-indexed access's reach. */
|
|
1266
|
+
function frameReaches(uses: FrameUses): OwnedReach[] {
|
|
1267
|
+
return [
|
|
1268
|
+
...[...uses.escapeExtents].flatMap(([owner, extents]) =>
|
|
1269
|
+
extents.map((e): OwnedReach => ({
|
|
1270
|
+
owner,
|
|
1271
|
+
by: 'escape',
|
|
1272
|
+
reach: e === null ? null : { lo: e.lo, hi: e.hi },
|
|
1273
|
+
into: e?.into === true,
|
|
1274
|
+
})),
|
|
1275
|
+
),
|
|
1276
|
+
...[...uses.indexed].flatMap(([owner, accesses]) =>
|
|
1277
|
+
accesses.map((x): OwnedReach => ({ owner, by: 'index', reach: x.reach, into: false })),
|
|
1278
|
+
),
|
|
1279
|
+
];
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/** Where a bounded block copy writes into the frame: the start of each copy's destination. */
|
|
1283
|
+
function copyStarts(reaches: readonly OwnedReach[]): Set<number> {
|
|
1284
|
+
return new Set(reaches.flatMap((r) => (r.into && r.reach !== null ? [r.reach.lo] : [])));
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/** One access through a runtime index: its width and signedness, and the bytes it may touch. */
|
|
1288
|
+
interface IndexedAccess {
|
|
1289
|
+
readonly width: number;
|
|
1290
|
+
readonly signed: boolean;
|
|
1291
|
+
readonly reach: FrameReach;
|
|
771
1292
|
}
|
|
772
1293
|
|
|
773
1294
|
/** Every use of every frame object, by offset — what each later stage reads. Each field is
|
|
@@ -786,10 +1307,30 @@ interface FrameUses {
|
|
|
786
1307
|
readonly publishedToWriter: ReadonlySet<number>;
|
|
787
1308
|
readonly arg0Callees: ReadonlyMap<number, ReadonlySet<string | null>>;
|
|
788
1309
|
readonly returnTemps: ReadonlyMap<number, readonly Op[]>;
|
|
1310
|
+
/** the calls each object is passed to BY VALUE (`foldByValueArgs`), with the struct each passes */
|
|
1311
|
+
readonly byValueReads: ReadonlyMap<number, readonly { readonly op: Op; readonly type: IrType }[]>;
|
|
789
1312
|
readonly useCount: ReadonlyMap<number, number>;
|
|
790
|
-
|
|
1313
|
+
/** every runtime-indexed access of each object, with the frame bytes `[lo, hi)` it may touch
|
|
1314
|
+
* (`reach`, `indexedAccess`), or `null` where nothing bounds the index */
|
|
1315
|
+
readonly indexed: ReadonlyMap<number, readonly IndexedAccess[]>;
|
|
791
1316
|
readonly members: readonly { readonly at: number; readonly width: number }[];
|
|
792
1317
|
readonly memberRefusal: string | undefined;
|
|
1318
|
+
/** every escape of each object's address, as the frame bytes it may reach: `[lo, hi)` where a
|
|
1319
|
+
* literal-count block copy proves them (`boundedCopy`), `null` where nothing does, and `into`
|
|
1320
|
+
* where the copy WRITES them (the address is its destination, not its source). Only the split
|
|
1321
|
+
* of the one object reads this (`provenParts`); the escape windows keep the copy as an
|
|
1322
|
+
* unbounded writer, which is what puts that one object on offer at all (`chooseFrameModel`). */
|
|
1323
|
+
readonly escapeExtents: ReadonlyMap<
|
|
1324
|
+
number,
|
|
1325
|
+
readonly ({ readonly lo: number; readonly hi: number; readonly into: boolean } | null)[]
|
|
1326
|
+
>;
|
|
1327
|
+
readonly fixed: readonly {
|
|
1328
|
+
readonly op: Op;
|
|
1329
|
+
readonly at: number;
|
|
1330
|
+
readonly width: number;
|
|
1331
|
+
readonly isLoad: boolean;
|
|
1332
|
+
readonly frameOnly: boolean;
|
|
1333
|
+
}[];
|
|
793
1334
|
}
|
|
794
1335
|
|
|
795
1336
|
/** Did the address at `off` reach something that may write the frame back: a callee that is not a
|
|
@@ -885,11 +1426,14 @@ function classifyFrameUses({
|
|
|
885
1426
|
// is the callee's return storage, a local of the declared type. `useCount` counts every use of
|
|
886
1427
|
// each object, so a return temp this function does anything else with is told apart.
|
|
887
1428
|
const returnTemps = new Map<number, Op[]>();
|
|
1429
|
+
// …and a call's operand its lowering passes BY VALUE (`foldByValueArgs`) is no escape: the
|
|
1430
|
+
// callee receives a copy of the whole struct, so it is a read of the object's bytes at the call
|
|
1431
|
+
const byValueReads = new Map<number, { op: Op; type: IrType }[]>();
|
|
888
1432
|
const useCount = new Map<number, number>();
|
|
889
1433
|
// The accesses through a runtime-indexed address, per object — kept apart from `accesses`,
|
|
890
1434
|
// which types the object at its own offset: `a[i]` says what one ELEMENT is, not what sits at
|
|
891
1435
|
// `a`.
|
|
892
|
-
const indexed = new Map<number,
|
|
1436
|
+
const indexed = new Map<number, IndexedAccess[]>();
|
|
893
1437
|
for (const off of objects.keys()) {
|
|
894
1438
|
accesses.set(off, []);
|
|
895
1439
|
}
|
|
@@ -897,17 +1441,37 @@ function classifyFrameUses({
|
|
|
897
1441
|
// access at [+k] through a capture is a member of the one object rather than a second object
|
|
898
1442
|
const members: { at: number; width: number }[] = [];
|
|
899
1443
|
// …and without it, the first such access, which the per-object model refuses once the model is
|
|
900
|
-
// chosen (`chooseFrameModel`)
|
|
901
|
-
|
|
1444
|
+
// chosen (`chooseFrameModel`) — save one of a call's return storage or of a struct passed by
|
|
1445
|
+
// value, whose members are the struct's declared ones (`objectShapes`)
|
|
1446
|
+
const memberRefusals: [number, string][] = [];
|
|
1447
|
+
const escapeExtents = new Map<number, ({ lo: number; hi: number; into: boolean } | null)[]>();
|
|
1448
|
+
// …and every value something reads, since a copy whose returned destination is read hands the
|
|
1449
|
+
// address on to a use nothing here follows
|
|
1450
|
+
const readValues = new Set<Value>(
|
|
1451
|
+
irBlocks.flatMap((b) => b.ops.flatMap((op) => [...op.operands, ...(op.successors ?? []).flatMap((sx) => sx.args)])),
|
|
1452
|
+
);
|
|
1453
|
+
// …and in either model, every fixed-offset access as the op that makes it, the frame bytes it
|
|
1454
|
+
// touches, whether it reads them, and whether its address is this frame's on every path — what
|
|
1455
|
+
// `storedNeverRead` asks of a frame that makes a call
|
|
1456
|
+
const fixed: { op: Op; at: number; width: number; isLoad: boolean; frameOnly: boolean }[] = [];
|
|
902
1457
|
const scalar = (kind: string, op: Op, off: number, v: Value): void => {
|
|
1458
|
+
fixed.push({
|
|
1459
|
+
op,
|
|
1460
|
+
at: off + (op.attrs.off as number),
|
|
1461
|
+
width: op.attrs.width as number,
|
|
1462
|
+
isLoad: kind === 'load',
|
|
1463
|
+
frameOnly: frameOnEveryPath.has(v),
|
|
1464
|
+
});
|
|
903
1465
|
if (oneObject !== undefined) {
|
|
904
1466
|
members.push({ at: off + (op.attrs.off as number), width: op.attrs.width as number });
|
|
905
1467
|
return;
|
|
906
1468
|
}
|
|
907
1469
|
if ((op.attrs.off as number) !== 0) {
|
|
908
|
-
|
|
1470
|
+
memberRefusals.push([
|
|
1471
|
+
off,
|
|
909
1472
|
`a ${kind} at [+${op.attrs.off}] through the captured address — ` +
|
|
910
|
-
|
|
1473
|
+
(splitRefusal.get(v) ?? 'only a scalar at the captured address is modelled'),
|
|
1474
|
+
]);
|
|
911
1475
|
}
|
|
912
1476
|
};
|
|
913
1477
|
for (const blk of irBlocks) {
|
|
@@ -934,8 +1498,22 @@ function classifyFrameUses({
|
|
|
934
1498
|
accesses.get(off)!.push({ width: op.attrs.width as number, signed: false, isLoad: false });
|
|
935
1499
|
continue;
|
|
936
1500
|
}
|
|
1501
|
+
if (byValueOperands(op).some((b) => b.at === idx)) {
|
|
1502
|
+
// the fold types the operand a pointer to the struct passed (call-plan.ts `foldByValueArgs`)
|
|
1503
|
+
const type = v.type.kind === 'ptr' ? v.type.to : undefined;
|
|
1504
|
+
if (type?.kind !== 'struct' || type.size === undefined) {
|
|
1505
|
+
throw new Error(`internal: a by-value operand of a call to '${calleeName(op)}' has no declared struct`);
|
|
1506
|
+
}
|
|
1507
|
+
(byValueReads.get(off) ?? byValueReads.set(off, []).get(off)!).push({ op, type });
|
|
1508
|
+
fixed.push({ op, at: off, width: type.size, isLoad: true, frameOnly: frameOnEveryPath.has(v) });
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
937
1511
|
if ((op.opcode === 'store' && idx === 1) || op.opcode === 'call') {
|
|
938
1512
|
escaped.add(off); // the address ESCAPES as a value — the point of the capability
|
|
1513
|
+
const copied = op.opcode === 'call' ? boundedCopy(op, idx, facts, readValues) : undefined;
|
|
1514
|
+
(escapeExtents.get(off) ?? escapeExtents.set(off, []).get(off)!).push(
|
|
1515
|
+
copied === undefined ? null : { lo: off + copied.lo, hi: off + copied.hi, into: copied.into },
|
|
1516
|
+
);
|
|
939
1517
|
const handedTo = op.opcode === 'store' ? facts.readsThrough(op) : [];
|
|
940
1518
|
const read = op.opcode === 'call' ? transferRead(op, idx, off, target, facts) : undefined;
|
|
941
1519
|
if (read !== undefined) {
|
|
@@ -984,7 +1562,14 @@ function classifyFrameUses({
|
|
|
984
1562
|
// judged with the rest (`objectShapes`).
|
|
985
1563
|
const other = op.opcode === 'add' && op.operands.length === 2 ? op.operands[1 - idx] : undefined;
|
|
986
1564
|
if (other !== undefined && taint.get(other) === undefined && defOf.get(other)?.opcode !== 'const') {
|
|
987
|
-
indexedAccess(
|
|
1565
|
+
indexedAccess(
|
|
1566
|
+
irBlocks,
|
|
1567
|
+
{ indexed, accessOps },
|
|
1568
|
+
{ off, through: v },
|
|
1569
|
+
{ sum: op.results[0], range: facts.addressRangeOf(other, blk, 'narrowest') },
|
|
1570
|
+
facts.constOf,
|
|
1571
|
+
fail,
|
|
1572
|
+
);
|
|
988
1573
|
continue;
|
|
989
1574
|
}
|
|
990
1575
|
// A CONSTANT move left after the fold (`foldMovedCaptures`) moves a capture a phi carried.
|
|
@@ -1022,13 +1607,39 @@ function classifyFrameUses({
|
|
|
1022
1607
|
publishedToWriter,
|
|
1023
1608
|
arg0Callees,
|
|
1024
1609
|
returnTemps,
|
|
1610
|
+
byValueReads,
|
|
1025
1611
|
useCount,
|
|
1026
1612
|
indexed,
|
|
1027
1613
|
members,
|
|
1028
|
-
memberRefusal,
|
|
1614
|
+
memberRefusal: memberRefusals.find(([off]) => !returnTemps.has(off) && !byValueReads.has(off))?.[1],
|
|
1615
|
+
escapeExtents,
|
|
1616
|
+
fixed,
|
|
1029
1617
|
};
|
|
1030
1618
|
}
|
|
1031
1619
|
|
|
1620
|
+
// The bytes, relative to the address at operand `idx`, that a call of a standard block copy with
|
|
1621
|
+
// a literal, positive count reaches through it, and whether it writes them (`idx` is its
|
|
1622
|
+
// destination) — or `undefined` for any other callee, operand or count, which this does not
|
|
1623
|
+
// bound, and for a copy whose result (`memcpy` returns its destination) anything in `read` uses.
|
|
1624
|
+
function boundedCopy(
|
|
1625
|
+
op: Op,
|
|
1626
|
+
idx: number,
|
|
1627
|
+
facts: LiteralAddresses,
|
|
1628
|
+
read: ReadonlySet<Value>,
|
|
1629
|
+
): { lo: number; hi: number; into: boolean } | undefined {
|
|
1630
|
+
const roles = blockCopyRoles(op.attrs.target);
|
|
1631
|
+
if (
|
|
1632
|
+
roles === undefined ||
|
|
1633
|
+
(idx !== roles.dst && idx !== roles.src) ||
|
|
1634
|
+
op.operands.length <= roles.n ||
|
|
1635
|
+
op.results.some((r) => read.has(r))
|
|
1636
|
+
) {
|
|
1637
|
+
return undefined;
|
|
1638
|
+
}
|
|
1639
|
+
const n = facts.constOf(op.operands[roles.n]);
|
|
1640
|
+
return n !== undefined && Number.isInteger(n) && n > 0 ? { lo: 0, hi: n, into: idx === roles.dst } : undefined;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1032
1643
|
// The bytes a block transfer that took the address at `idx` reads, when `idx` is its source and
|
|
1033
1644
|
// its control word is a literal.
|
|
1034
1645
|
function transferRead(
|
|
@@ -1051,36 +1662,128 @@ function transferRead(
|
|
|
1051
1662
|
}
|
|
1052
1663
|
|
|
1053
1664
|
// Records every access through `sum`, a runtime index into the object at `off`.
|
|
1665
|
+
//
|
|
1666
|
+
// A SUM MOVED BY A LITERAL IS THE SAME INDEX, DISPLACED. Thumb has no `ldrsb`/`ldrsh` with an
|
|
1667
|
+
// immediate, so agbcc loads the whole displacement into a register (thumb.md:411-505, `mov %0,%2 ;
|
|
1668
|
+
// ldrs[bh] %0,[%1,%0]`) and the frontend lowers that register-offset load as `add` + load@0:
|
|
1669
|
+
// `add r0, r1, r4 / mov r1, #0x0 / ldrsb r1, [r0, r1]` is `a[i]` on an `s8 a[n]` local. So the
|
|
1670
|
+
// uses of an `add` of the sum and a `const` are judged at that displacement. One `add` deep: agbcc
|
|
1671
|
+
// emits one `mov` per load, and no other compiler emits a literal index register. Only
|
|
1672
|
+
// displacement 0 is the indexed element. A nonzero `const` on a sum whose base is a capture never
|
|
1673
|
+
// arrives here: `foldMovedCaptures` has already moved the index to the object at the sum, which is
|
|
1674
|
+
// what agbcc's `mov r0, sp / add r0, r0, r4 / add r0, #0x4` is (an array above the outgoing word),
|
|
1675
|
+
// and what `mov r1, #0x3 / ldrsb r1, [r0, r1]` and a load's own displacement, `ldrb r0, [r0,
|
|
1676
|
+
// #0x1]` (a field of an element struct, or `a[i + 1]`), reach. What is left nonzero — a constant
|
|
1677
|
+
// computed rather than loaded as a `const`, a literal or a displacement on an index into a capture
|
|
1678
|
+
// a phi carries, or a displacement on top of the zero literal, which no compiler emits — refuses
|
|
1679
|
+
// naming the displacement.
|
|
1680
|
+
//
|
|
1681
|
+
// The element is judged here and left where the register put it, so it is spelled with the
|
|
1682
|
+
// literal: `*(s16 *)((u32)sp0 + (a0 << 1) + 0)`. Re-pointing it at the sum spells
|
|
1683
|
+
// `((s16 *)sp0)[a0]`, an index through a cast on the `u8` storage (`objectShapes`), which agbcc
|
|
1684
|
+
// compiles to `add r4, r4, sp` and the source's `mov r1, sp / add r0, r1, r4` does not: the `u16`
|
|
1685
|
+
// twin `((u16 *)sp0)[a0]` misses the same way. Left raw, the access compiles to the source's
|
|
1686
|
+
// bytes with or without the `+ 0`.
|
|
1687
|
+
//
|
|
1688
|
+
// WHAT IT MAY REACH is the object's offset plus every value the index may hold (`range`, ir/bits.ts
|
|
1689
|
+
// `stridedRange`, asked at the sum's block): a mask, a zero-fill shift, an unsigned narrow load or a
|
|
1690
|
+
// `zext` — by its width, or by its operand's range where that is narrower (`(u8)k` of a counter in
|
|
1691
|
+
// [0, 7]); a compare on the way to the access (`if (i < 8) a[i]`); a loop counter its latch bounds,
|
|
1692
|
+
// by induction where the latch bounds one side; and linear forms of them. The range is widened by
|
|
1693
|
+
// the access's width — the stride is dropped, so the bytes are a superset of the ones reached.
|
|
1694
|
+
// Every element is at displacement 0 from the sum (above), so the address is exactly the capture
|
|
1695
|
+
// plus the index. An index nothing bounds, or one whose sum could wrap past the top of the address
|
|
1696
|
+
// space back below the object, reaches every byte (`null`).
|
|
1054
1697
|
function indexedAccess(
|
|
1055
1698
|
irBlocks: readonly Block[],
|
|
1056
|
-
{
|
|
1057
|
-
indexed,
|
|
1058
|
-
accessOps,
|
|
1059
|
-
}: { indexed: Map<number, { width: number; signed: boolean }[]>; accessOps: Map<Op, FrameAccessOp> },
|
|
1699
|
+
{ indexed, accessOps }: { indexed: Map<number, IndexedAccess[]>; accessOps: Map<Op, FrameAccessOp> },
|
|
1060
1700
|
object: FrameAccessOp,
|
|
1061
|
-
sum: Value,
|
|
1701
|
+
{ sum, range }: { sum: Value; range: StridedRange | undefined },
|
|
1702
|
+
constOf: (v: Value) => number | undefined,
|
|
1062
1703
|
fail: Refuse,
|
|
1063
1704
|
): void {
|
|
1064
1705
|
const { off } = object;
|
|
1065
1706
|
const got = indexed.get(off) ?? indexed.set(off, []).get(off)!;
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
for (const
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1707
|
+
const reach = (width: number): FrameReach => {
|
|
1708
|
+
const starts = range === undefined ? null : displaced(range, off, width);
|
|
1709
|
+
return starts === null ? null : { lo: starts.lo, hi: starts.hi + width };
|
|
1710
|
+
};
|
|
1711
|
+
const judge = (at: Value, by: number): void => {
|
|
1712
|
+
for (const blk of irBlocks) {
|
|
1713
|
+
for (const op of blk.ops) {
|
|
1714
|
+
const roles = [
|
|
1715
|
+
...op.operands.flatMap((v, i) => (v === at ? [i] : [])),
|
|
1716
|
+
...(op.successors ?? []).flatMap((s) => (s.args.includes(at) ? [-1] : [])),
|
|
1717
|
+
];
|
|
1718
|
+
for (const i of roles) {
|
|
1719
|
+
const k =
|
|
1720
|
+
at === sum && op.opcode === 'add' && op.operands.length === 2 && i >= 0
|
|
1721
|
+
? constOf(op.operands[1 - i])
|
|
1722
|
+
: undefined;
|
|
1723
|
+
if (k !== undefined) {
|
|
1724
|
+
judge(op.results[0], k);
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
const access = (op.opcode === 'load' || op.opcode === 'store') && i === 0;
|
|
1728
|
+
if (!access) {
|
|
1729
|
+
fail(
|
|
1730
|
+
`a runtime index into the object at [sp,#${off}) flows into \`${i === -1 ? 'a phi' : op.opcode}\` — ` +
|
|
1731
|
+
'only a load or store at the indexed address is modelled',
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
const at0 = (by + (op.attrs.off as number)) | 0;
|
|
1735
|
+
if (at0 !== 0) {
|
|
1736
|
+
fail(
|
|
1737
|
+
`a runtime index into the object at [sp,#${off}) is accessed at displacement ${at0} from the indexed element — ` +
|
|
1738
|
+
'only a load or store at the indexed address is modelled',
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
const width = op.attrs.width as number;
|
|
1742
|
+
const index = { width, signed: op.opcode === 'load' && op.attrs.signed === true, reach: reach(width) };
|
|
1743
|
+
got.push(index);
|
|
1744
|
+
accessOps.set(op, { ...object, index });
|
|
1078
1745
|
}
|
|
1079
|
-
got.push({ width: op.attrs.width as number, signed: op.opcode === 'load' && op.attrs.signed === true });
|
|
1080
|
-
accessOps.set(op, object);
|
|
1081
1746
|
}
|
|
1082
1747
|
}
|
|
1083
|
-
}
|
|
1748
|
+
};
|
|
1749
|
+
judge(sum, 0);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
/** Why two access types on one byte refuse, as every refusal of it words it. THE RULE IS AGBCC'S
|
|
1753
|
+
* AND IS HELD ON EVERY TARGET: agbcc at -O2 serves a read of one type past a store of the other
|
|
1754
|
+
* (`indexedWidth` has the compiled cases), while IDO 7.1, KMC gcc and gcc 2.7.2 reload past every such
|
|
1755
|
+
* store, so on MIPS it is only more cautious than it has to be. The text names no compiler of the
|
|
1756
|
+
* input, since the audit is handed none. */
|
|
1757
|
+
const STALE_ACROSS_TYPES =
|
|
1758
|
+
'and type-based alias analysis can let a read of one type pass a store of the other (agbcc at -O2 does, ' +
|
|
1759
|
+
'and the audit refuses it on every target) — a cast spelling could read stale';
|
|
1760
|
+
|
|
1761
|
+
/** The one width wider than a byte that runtime-indexed accesses take, or why there is not one.
|
|
1762
|
+
*
|
|
1763
|
+
* A runtime index names no byte, so its access type must agree with every access it may reach,
|
|
1764
|
+
* and "agree" is what stops agbcc reordering two of them. At -O2 it runs type-based alias
|
|
1765
|
+
* analysis (toplev.c:3616), and `c_get_alias_set` (c-common.c:1892-2005) gives an integer type
|
|
1766
|
+
* and its unsigned twin one set, a character type set 0 (conflicting with every set), a
|
|
1767
|
+
* member access its member's type's set, and any other type — a pointer included — a new set.
|
|
1768
|
+
* Compiled at the corpus's flags, `k(sp0); ((int *)sp0)[i] = 1; ((T *)sp0)[j] = 0; return
|
|
1769
|
+
* ((int *)sp0)[i];` reloads the word for T = int, unsigned int and unsigned char, and returns the
|
|
1770
|
+
* stale 1 for T = unsigned short, void * and long.
|
|
1771
|
+
*
|
|
1772
|
+
* So ONE WIDTH IS ONE SET only while every access of that width READS AT one integer type or its
|
|
1773
|
+
* twin: a cast on the storage to `s32`/`u32` (`int`/`unsigned int` in every agbcc project), or a
|
|
1774
|
+
* member of an element struct (raise/struct-arrays) whose FIELD is such an integer — never the pointer an element
|
|
1775
|
+
* may hold. `/ptr-field`'s `over-local-storage` gate keeps such a field an integer, and contracts.ts
|
|
1776
|
+
* `assertLocalStorageStaysInteger` holds every respelled tree to it. The storage stays
|
|
1777
|
+
* `u8 name[n]` whatever the width. */
|
|
1778
|
+
function indexedWidth(accesses: readonly { readonly width: number }[]): {
|
|
1779
|
+
width: number | null;
|
|
1780
|
+
refusal: string | null;
|
|
1781
|
+
} {
|
|
1782
|
+
const widths = [...new Set(accesses.map((x) => x.width))].filter((w) => w !== 1);
|
|
1783
|
+
return {
|
|
1784
|
+
width: widths[0] ?? null,
|
|
1785
|
+
refusal: widths.length > 1 ? `accesses ${widths.join(' and ')} bytes wide, ${STALE_ACROSS_TYPES}` : null,
|
|
1786
|
+
};
|
|
1084
1787
|
}
|
|
1085
1788
|
|
|
1086
1789
|
/** The bytes an escape may reach, `[lo, hi)` from the object's own offset, and how it reads them
|
|
@@ -1089,6 +1792,10 @@ interface ReadWindow {
|
|
|
1089
1792
|
readonly lo: number;
|
|
1090
1793
|
readonly hi: number;
|
|
1091
1794
|
readonly why: string;
|
|
1795
|
+
/** bounded only because a store through a computed pointer provably misses the control
|
|
1796
|
+
* halfword (`misses`): without that proof the read is unbounded, and the one-object answer
|
|
1797
|
+
* that held it then stays on offer (`chooseFrameModel`) */
|
|
1798
|
+
readonly sparedByMiss?: true;
|
|
1092
1799
|
}
|
|
1093
1800
|
|
|
1094
1801
|
/** Each op's block and index, over the IR as the classification judged it. */
|
|
@@ -1123,8 +1830,9 @@ const unbounded = (why: string): ReadWindow => ({ lo: -Infinity, hi: Infinity, w
|
|
|
1123
1830
|
* the control stores that bound it are the ones reachable in between. Each has to be a literal
|
|
1124
1831
|
* the target decodes. A store through a pointer this cannot resolve — one that is this frame's
|
|
1125
1832
|
* on only some paths included (`frameOnEveryPath`) — may BE the control halfword, so it leaves
|
|
1126
|
-
* the read unbounded
|
|
1127
|
-
*
|
|
1833
|
+
* the read unbounded; so does a transfer never armed here at all. A store whose every address
|
|
1834
|
+
* misses that halfword (`misses`) is not such a store. An unbounded device read says which of
|
|
1835
|
+
* those it met (`why`): each is a different capability to build. A block transfer
|
|
1128
1836
|
* that took the address as its source adds the bytes its control word reads (`calleeReads`).
|
|
1129
1837
|
*
|
|
1130
1838
|
* A NAMED SYMBOL PLUS A CONSTANT IS RESOLVED WHERE THE SYMBOL MAP PLACES THE NAME, and then
|
|
@@ -1154,9 +1862,11 @@ function readWindow(
|
|
|
1154
1862
|
target: TargetDescription;
|
|
1155
1863
|
},
|
|
1156
1864
|
): ReadWindow {
|
|
1157
|
-
// a callee handed only its own return storage writes the struct it returns and nothing else
|
|
1865
|
+
// a callee handed only its own return storage writes the struct it returns and nothing else —
|
|
1866
|
+
// and this function's own reads of it hand the address to nobody
|
|
1158
1867
|
const temps = uses.returnTemps.get(off);
|
|
1159
|
-
|
|
1868
|
+
const reads = uses.accesses.get(off)?.filter((a) => a.isLoad).length ?? 0;
|
|
1869
|
+
if (temps !== undefined && uses.useCount.get(off) === temps.length + reads) {
|
|
1160
1870
|
return { lo: 0, hi: returnedSize(temps[0]), why: 'that returns its struct into it' };
|
|
1161
1871
|
}
|
|
1162
1872
|
const control = target.capabilities.readSourceControl;
|
|
@@ -1170,6 +1880,7 @@ function readWindow(
|
|
|
1170
1880
|
}
|
|
1171
1881
|
const little = target.capabilities.endianness === 'little';
|
|
1172
1882
|
const halves: number[] = [];
|
|
1883
|
+
let spared = false;
|
|
1173
1884
|
for (const { op: handed, sink } of stores) {
|
|
1174
1885
|
const cnt = sink + control.offset;
|
|
1175
1886
|
let armed = false;
|
|
@@ -1185,7 +1896,10 @@ function readWindow(
|
|
|
1185
1896
|
const base = facts.literalAddrOf(op.operands[0]);
|
|
1186
1897
|
if (base === undefined) {
|
|
1187
1898
|
if (!flow.frameOnEveryPath.has(op.operands[0])) {
|
|
1188
|
-
|
|
1899
|
+
if (!misses(op, facts, cnt, blk)) {
|
|
1900
|
+
return unbounded('a later store through an unresolved pointer may re-arm');
|
|
1901
|
+
}
|
|
1902
|
+
spared = true;
|
|
1189
1903
|
}
|
|
1190
1904
|
continue;
|
|
1191
1905
|
}
|
|
@@ -1226,7 +1940,44 @@ function readWindow(
|
|
|
1226
1940
|
lo = Math.min(lo, reach.lo);
|
|
1227
1941
|
hi = Math.max(hi, reach.hi);
|
|
1228
1942
|
}
|
|
1229
|
-
return { lo, hi, why: 'that reads through it' };
|
|
1943
|
+
return { lo, hi, why: 'that reads through it', ...(spared ? { sparedByMiss: true as const } : {}) };
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
/** The addresses a strided range moved by `off` may hold, where an access `width` bytes wide at
|
|
1947
|
+
* any of them stays inside one address space — or null where a start could fall below 0 or an
|
|
1948
|
+
* access wrap past 2^32, so no interval names the bytes it touches. What a runtime index into the
|
|
1949
|
+
* frame may reach (`indexedAccess`) and where a computed store may land (`misses`) both read it. */
|
|
1950
|
+
function displaced(r: StridedRange, off: number, width: number): StridedRange | null {
|
|
1951
|
+
const lo = r.lo + off;
|
|
1952
|
+
const hi = r.hi + off;
|
|
1953
|
+
return lo < 0 || hi + width > 2 ** 32 ? null : { lo, hi, stride: r.stride };
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
/** A store through a computed pointer that provably misses the control halfword at `cnt`: no
|
|
1957
|
+
* address its base may hold (`addressRangeOf`, a strided interval), displaced by the store's own
|
|
1958
|
+
* offset, starts a store whose bytes overlap `[cnt, cnt + 2)`. agbcc's `gEnts[i].f = 1` after a
|
|
1959
|
+
* transfer, `i` a `u8`, writes one byte at `gEnts + 4 + 8 * [0, 255]`, and no `i` makes that the
|
|
1960
|
+
* channel's control register — so it cannot re-arm the channel, and the window stays where the
|
|
1961
|
+
* literal stores put it. The stride decides it where the interval alone cannot: placed at
|
|
1962
|
+
* 0x04000000 that span covers 0x040000DE, and still no `i` lands on it. Not "the transfer is
|
|
1963
|
+
* over": an immediate transfer is, but a later store that sets the enable bit again reloads the
|
|
1964
|
+
* source register and reads the frame anew (GBATEK, DMA control bit 15), which is what this
|
|
1965
|
+
* guards against. A store this cannot bound on every value stays an unresolved pointer. */
|
|
1966
|
+
function misses(op: Op, facts: FrameAddressFacts, cnt: number, blk: Block): boolean {
|
|
1967
|
+
const r = facts.addressRangeOf(op.operands[0], blk);
|
|
1968
|
+
if (r === undefined) {
|
|
1969
|
+
return false;
|
|
1970
|
+
}
|
|
1971
|
+
const width = op.attrs.width as number;
|
|
1972
|
+
const starts = displaced(r, op.attrs.off as number, width);
|
|
1973
|
+
if (starts === null) {
|
|
1974
|
+
return false;
|
|
1975
|
+
}
|
|
1976
|
+
const { lo, hi } = starts;
|
|
1977
|
+
// the first start at or above the lowest one that overlaps, against the highest that does
|
|
1978
|
+
const from = cnt - width + 1;
|
|
1979
|
+
const first = from <= lo || r.stride === 0 ? lo : lo + Math.ceil((from - lo) / r.stride) * r.stride;
|
|
1980
|
+
return first > Math.min(hi, cnt + 1) || first < from;
|
|
1230
1981
|
}
|
|
1231
1982
|
|
|
1232
1983
|
/** Which model the audit judges the frame in. `refused` is the one mutable fact the audit
|
|
@@ -1234,6 +1985,9 @@ function readWindow(
|
|
|
1234
1985
|
* one-object answer is on offer, and read by the relift test. */
|
|
1235
1986
|
interface ModelChoice {
|
|
1236
1987
|
readonly onOffer: boolean;
|
|
1988
|
+
/** the escapes whose read is unbounded, or was before `misses` bounded it (`sparedByMiss`) — the
|
|
1989
|
+
* one fact both the offer and the relift test (`answered`) read, so the miss proof moves neither */
|
|
1990
|
+
readonly unboundedAt: ReadonlySet<number>;
|
|
1237
1991
|
refused: boolean;
|
|
1238
1992
|
/** refuses `why` where it stands, or, with the one-object answer on offer, asks for it instead */
|
|
1239
1993
|
shapeRefused(why: string): void;
|
|
@@ -1264,27 +2018,56 @@ function writerNotKept(uses: FrameUses, returnsWithoutHiddenPointer: (callee: st
|
|
|
1264
2018
|
return null;
|
|
1265
2019
|
}
|
|
1266
2020
|
|
|
1267
|
-
/** THE MODEL IS CHOSEN BEFORE ANY SHAPE IS JUDGED.
|
|
1268
|
-
* one
|
|
1269
|
-
* out-parameters handed to one callee as readily as a device read
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1273
|
-
*
|
|
1274
|
-
*
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
2021
|
+
/** THE MODEL IS CHOSEN BEFORE ANY SHAPE IS JUDGED. The one-object answer is on offer in two
|
|
2022
|
+
* frames. Where one escape reaches without bound and that answer can hold every writer
|
|
2023
|
+
* (`writerNotKept`) — two out-parameters handed to one callee as readily as a device read. And
|
|
2024
|
+
* where NO ADDRESS ESCAPES AT ALL: then there is no writer and no reader but this function's own
|
|
2025
|
+
* accesses, so keeping every word of the declared area in memory keeps every store and every load
|
|
2026
|
+
* the machine made there, and a spill kept in memory changes no value. That second frame is the
|
|
2027
|
+
* one a by-value struct parameter leaves: agbcc spills its registers word by word (`str r1, [sp]`,
|
|
2028
|
+
* the slot model's) and, Thumb having no sp-relative halfword or byte load, reads its members
|
|
2029
|
+
* through `mov rD, sp` (the object's) — `int f(struct Q { u8 a[9]; } p){ return p.a[1] + p.a[8]; }`,
|
|
2030
|
+
* or a member read signed and unsigned, or the struct copied out a word at a time. The shapes the
|
|
2031
|
+
* per-object model refuses are ones that answer can hold: a member at [+k] through a capture, two
|
|
2032
|
+
* widths or two signednesses at one address, a runtime index, overlapping objects, a second
|
|
2033
|
+
* object beside an untyped one, an object over a slot. So there each of those refusals asks for
|
|
2034
|
+
* that answer instead of declining (`shapeRefused`), and the second audit judges the bytes as one
|
|
2035
|
+
* object by its own rules — two types at one byte are a union there, or refuse. Where the answer
|
|
2036
|
+
* is not on offer, each refuses where it stands.
|
|
2037
|
+
*
|
|
2038
|
+
* A frame nothing escapes from can still hold a CALL'S STACK ARGUMENT: agbcc builds one it cannot
|
|
2039
|
+
* place with `str rX, [sp,#k]` through `mov rD, sp` — a by-value struct block-copied with
|
|
2040
|
+
* `ldmia`/`stmia`, a compound literal stored a byte or a halfword at a time — and the address
|
|
2041
|
+
* escapes nowhere. Kept here as a local, in either model, those stores outlive the call that took
|
|
2042
|
+
* them. That is `storedNeverRead`'s to refuse, the stage after this one, and it asks it of every
|
|
2043
|
+
* call, whatever a prototype says. */
|
|
2044
|
+
function chooseFrameModel({
|
|
2045
|
+
oneObject,
|
|
2046
|
+
uses,
|
|
2047
|
+
windows,
|
|
2048
|
+
returnsWithoutHiddenPointer,
|
|
2049
|
+
fail,
|
|
2050
|
+
}: {
|
|
2051
|
+
oneObject: FrameRange | undefined;
|
|
2052
|
+
uses: FrameUses;
|
|
2053
|
+
windows: ReadonlyMap<number, ReadWindow>;
|
|
2054
|
+
returnsWithoutHiddenPointer: (callee: string) => boolean;
|
|
2055
|
+
fail: Refuse;
|
|
2056
|
+
}): ModelChoice {
|
|
2057
|
+
const unboundedAt = new Set(
|
|
2058
|
+
[...windows]
|
|
2059
|
+
.filter(([, w]) => (w.lo === -Infinity && w.hi === Infinity) || w.sparedByMiss === true)
|
|
2060
|
+
.map(([off]) => off),
|
|
2061
|
+
);
|
|
2062
|
+
const unbounded = oneObject === undefined && unboundedAt.size > 0;
|
|
1284
2063
|
const writer = unbounded ? writerNotKept(uses, returnsWithoutHiddenPointer) : null;
|
|
1285
|
-
const
|
|
2064
|
+
const escapeFree = oneObject === undefined && uses.escaped.size === 0;
|
|
2065
|
+
// A struct passed by value is ONE object of its declared type, which the call names whole; the
|
|
2066
|
+
// one object is bytes, and a call handed them would pass a pointer.
|
|
2067
|
+
const onOffer = uses.byValueReads.size === 0 && ((unbounded && writer === null) || escapeFree);
|
|
1286
2068
|
const model: ModelChoice = {
|
|
1287
2069
|
onOffer,
|
|
2070
|
+
unboundedAt,
|
|
1288
2071
|
refused: false,
|
|
1289
2072
|
shapeRefused: (why) => {
|
|
1290
2073
|
if (!onOffer) {
|
|
@@ -1302,6 +2085,226 @@ function chooseFrameModel(
|
|
|
1302
2085
|
return model;
|
|
1303
2086
|
}
|
|
1304
2087
|
|
|
2088
|
+
/** A FRAME STORE THAT REACHES A CALL UNREAD, BENEATH EVERY WORD PROVEN A LOCAL, MAY BE THAT
|
|
2089
|
+
* CALL'S STACK ARGUMENT — refuse. agbcc builds an argument it cannot place with `str rX, [sp,#k]`
|
|
2090
|
+
* IN PLACE through a capture: a by-value struct is `mov rD, sp` + `stmia`, a compound literal is
|
|
2091
|
+
* `strb`/`strh` through `mov rD, sp` moved by `add rD, #k`. The stack-argument analysis
|
|
2092
|
+
* (stackargs.ts) sees `[sp,#k]` slot stores alone, so it never weighs these, and no prototype
|
|
2093
|
+
* bounds them: a callee no prototype states counts argument registers alone, and a stated list is
|
|
2094
|
+
* a LOWER bound on the words a call takes — a variadic callee's list is a prefix, and DWARF's
|
|
2095
|
+
* `symbolPrototype` drops the `...`. Whatever model then holds the bytes, a local per capture or
|
|
2096
|
+
* the one object, keeps the stores and the call loses its stack words:
|
|
2097
|
+
* `th(1, 2, 3, 4, (struct S){{x, y}})` lifts as `th(1, 2, 3, 4)`. So this asks EVERY call.
|
|
2098
|
+
*
|
|
2099
|
+
* A store is asked when it reaches a call PENDING — on some path from it to the call nothing loads
|
|
2100
|
+
* a byte it wrote, and on EVERY path from the entry some byte of it is written by such a store and
|
|
2101
|
+
* not loaded since — since a word the callee takes is one the caller wrote for it on every path
|
|
2102
|
+
* to the call and did not read back. A store through a pointer that is the
|
|
2103
|
+
* frame's on only some paths (`*(p ? p : &local) = x`) is not asked: it is not a store the
|
|
2104
|
+
* outgoing block alone can own. Nor is a store a runtime index or an escaped address reaches from
|
|
2105
|
+
* an object at or below it: whoever that hands the bytes to may read them.
|
|
2106
|
+
*
|
|
2107
|
+
* AND ONLY A STORE BENEATH THE LOADED-WORD BOUND. Under `localsAboveOutgoingArea` a compiler keeps
|
|
2108
|
+
* one outgoing area at the frame bottom, below every local, and never reads it back after a call;
|
|
2109
|
+
* so a frame byte loaded after a call before anything re-stores it held a value across that call,
|
|
2110
|
+
* its word is a local's, and so is every word above it. Under the strong form,
|
|
2111
|
+
* `outgoingAreaNeverLoadedFrom`, any load in a word at or above that offset bounds it: from 0 on
|
|
2112
|
+
* agbcc, past the a0..a3 home on O32 (whose compilers load a by-value aggregate's home words back)
|
|
2113
|
+
* and from the parameter area on mwcc (`target.ts` says why). The frontend measures that bound
|
|
2114
|
+
* once, off its instructions, and hands it here (`loadedWords`) — the same number its own
|
|
2115
|
+
* stack-argument refusals take (stackargs' `loadedWordBound` on Thumb).
|
|
2116
|
+
* Every compiled outgoing-argument shape sits beneath any such bound, and a local the source reads
|
|
2117
|
+
* after a call does not: `g(); b[0] = x; b[1] = y; return b[0];` reaches no call, `b[0] = x; g();
|
|
2118
|
+
* return b[0];` is above the bound. What this still refuses that is honest is a local the source
|
|
2119
|
+
* stores, never reads back, and calls past — a dead local and an outgoing argument are the same
|
|
2120
|
+
* machine code, and the lift cannot tell which. The after-call liveness weighs this audit's fixed
|
|
2121
|
+
* accesses alone, so it sits at or above the bound the frontend's slot loads would give, which
|
|
2122
|
+
* only asks more; the strong form adds the frontend's bound (`loadedWords`). Without either
|
|
2123
|
+
* premise the bound proves nothing, and every pending store is asked. */
|
|
2124
|
+
function storedNeverRead(
|
|
2125
|
+
irBlocks: readonly Block[],
|
|
2126
|
+
uses: FrameUses,
|
|
2127
|
+
loadedWords: FrameObjectAudit['loadedWords'],
|
|
2128
|
+
target: TargetDescription,
|
|
2129
|
+
fail: Refuse,
|
|
2130
|
+
): void {
|
|
2131
|
+
const calls = irBlocks.flatMap((b) => b.ops.filter((op) => op.opcode === 'call'));
|
|
2132
|
+
if (calls.length === 0) {
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
const byOp = new Map(uses.fixed.map((a) => [a.op, a]));
|
|
2136
|
+
const bytes = (a: { at: number; width: number }): number[] => [...Array(a.width).keys()].map((i) => a.at + i);
|
|
2137
|
+
const index = new Map(irBlocks.map((b, i) => [b, i]));
|
|
2138
|
+
const succs = irBlocks.map((b) => successorsOf(b).map((s) => index.get(s)!));
|
|
2139
|
+
const live = new Set(irBlocks.length > 0 ? [0] : []);
|
|
2140
|
+
for (const stack = [...live]; stack.length;) {
|
|
2141
|
+
for (const s of succs[stack.pop()!]) {
|
|
2142
|
+
if (!live.has(s)) {
|
|
2143
|
+
live.add(s);
|
|
2144
|
+
stack.push(s);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
// THE SURVIVOR BOUND: a backward liveness over the frame bytes this function reads at fixed
|
|
2150
|
+
// offsets through an address that is the frame's on every path. A load makes its bytes live, a
|
|
2151
|
+
// store that writes them kills them, and a call is transparent but marks every byte live across
|
|
2152
|
+
// it. A load whose address may be elsewhere proves nothing and a store that may write elsewhere
|
|
2153
|
+
// kills nothing; nor does a block the entry cannot reach.
|
|
2154
|
+
let bound = Infinity;
|
|
2155
|
+
if (target.compilerBehaviors.localsAboveOutgoingArea === true) {
|
|
2156
|
+
const liveIn = irBlocks.map(() => new Set<number>());
|
|
2157
|
+
for (let changed = true; changed;) {
|
|
2158
|
+
changed = false;
|
|
2159
|
+
for (let b = irBlocks.length - 1; b >= 0; b--) {
|
|
2160
|
+
if (!live.has(b)) {
|
|
2161
|
+
continue;
|
|
2162
|
+
}
|
|
2163
|
+
const cur = new Set(succs[b].flatMap((s) => [...liveIn[s]]));
|
|
2164
|
+
for (const op of [...irBlocks[b].ops].reverse()) {
|
|
2165
|
+
const a = byOp.get(op);
|
|
2166
|
+
if (op.opcode === 'call') {
|
|
2167
|
+
for (const o of cur) {
|
|
2168
|
+
bound = Math.min(bound, o - (o % 4));
|
|
2169
|
+
}
|
|
2170
|
+
} else if (a?.frameOnly) {
|
|
2171
|
+
for (const o of bytes(a)) {
|
|
2172
|
+
if (a.isLoad) {
|
|
2173
|
+
cur.add(o);
|
|
2174
|
+
} else {
|
|
2175
|
+
cur.delete(o);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
if ([...cur].some((o) => !liveIn[b].has(o))) {
|
|
2181
|
+
liveIn[b] = cur;
|
|
2182
|
+
changed = true;
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
// …and under the STRONG form (`outgoingAreaNeverLoadedFrom`) the caller loads no word of the
|
|
2188
|
+
// area from that offset up before a call either, so ANY frame byte loaded there bounds it. The
|
|
2189
|
+
// frontend measured that bound off its instructions (`loadedWords`): the slot reloads the IR
|
|
2190
|
+
// does not show, and the prologue's and epilogue's own loads — without them
|
|
2191
|
+
// `mov r3, sp; strh r0, [r3]; … bl sink; ldr r0, [sp]` refuses the `strh` on a word the code
|
|
2192
|
+
// itself proves a local's. What only this audit sees is a load through a captured address, in a
|
|
2193
|
+
// block the entry reaches, and it bounds the same way. A load in a word below the offset proves
|
|
2194
|
+
// nothing: O32 compilers load the a1..a3 home words they stage a by-value aggregate in.
|
|
2195
|
+
if (loadedWords !== undefined) {
|
|
2196
|
+
bound = Math.min(bound, loadedWords.bound);
|
|
2197
|
+
for (const b of live) {
|
|
2198
|
+
for (const op of irBlocks[b].ops) {
|
|
2199
|
+
const a = byOp.get(op);
|
|
2200
|
+
const word = a === undefined ? -1 : a.at - (a.at % 4);
|
|
2201
|
+
if (a?.isLoad && a.frameOnly && word >= loadedWords.from) {
|
|
2202
|
+
bound = Math.min(bound, word);
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
const from = loadedWords?.from;
|
|
2208
|
+
|
|
2209
|
+
// THE PENDING STORES, two forward dataflows from the entry over the blocks it reaches. The MAY
|
|
2210
|
+
// set is the stores this asks that some path brings to a point unread: a load of any byte a
|
|
2211
|
+
// store wrote ends it — an argument is a word the caller writes and never reads, so a store
|
|
2212
|
+
// with any byte read is a local's — as a frame store covering every byte it wrote does. The
|
|
2213
|
+
// MUST set is the bytes every path brings to a point written by a store this asks and not
|
|
2214
|
+
// loaded since: a load ends the bytes it reads, a frame store the bytes it writes, and a store
|
|
2215
|
+
// that may write elsewhere ends nothing. An argument is staged on EVERY path to its call — a
|
|
2216
|
+
// tail-merged call is staged in each predecessor — so a byte some path reaches the call without
|
|
2217
|
+
// is not this call's to take: a loop's store that the first trip calls past is a local's.
|
|
2218
|
+
const reachedFrom = Math.min(...uses.escaped, ...uses.indexed.keys());
|
|
2219
|
+
type Fixed = FrameUses['fixed'][number];
|
|
2220
|
+
const asked = (a: Fixed): boolean => !a.isLoad && a.frameOnly && a.at + a.width <= reachedFrom && a.at < bound;
|
|
2221
|
+
interface Pending {
|
|
2222
|
+
may: Set<Fixed>;
|
|
2223
|
+
must: Set<number>;
|
|
2224
|
+
}
|
|
2225
|
+
const step = (op: Op, cur: Pending): void => {
|
|
2226
|
+
const a = byOp.get(op);
|
|
2227
|
+
if (a === undefined) {
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
const touched = new Set(bytes(a));
|
|
2231
|
+
for (const p of [...cur.may]) {
|
|
2232
|
+
const mine = bytes(p);
|
|
2233
|
+
if (a.isLoad ? mine.some((o) => touched.has(o)) : a.frameOnly && mine.every((o) => touched.has(o))) {
|
|
2234
|
+
cur.may.delete(p);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
if (a.isLoad || a.frameOnly) {
|
|
2238
|
+
for (const o of touched) {
|
|
2239
|
+
cur.must.delete(o);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
if (asked(a)) {
|
|
2243
|
+
cur.may.add(a);
|
|
2244
|
+
for (const o of touched) {
|
|
2245
|
+
cur.must.add(o);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2249
|
+
const into: (Pending | undefined)[] = irBlocks.map((_, b) =>
|
|
2250
|
+
b === 0 ? { may: new Set(), must: new Set() } : undefined,
|
|
2251
|
+
);
|
|
2252
|
+
for (let changed = true; changed;) {
|
|
2253
|
+
changed = false;
|
|
2254
|
+
for (let b = 0; b < irBlocks.length; b++) {
|
|
2255
|
+
const at = into[b];
|
|
2256
|
+
if (!live.has(b) || at === undefined) {
|
|
2257
|
+
continue;
|
|
2258
|
+
}
|
|
2259
|
+
const cur: Pending = { may: new Set(at.may), must: new Set(at.must) };
|
|
2260
|
+
irBlocks[b].ops.forEach((op) => step(op, cur));
|
|
2261
|
+
for (const s of succs[b]) {
|
|
2262
|
+
const old = into[s];
|
|
2263
|
+
if (s === 0) {
|
|
2264
|
+
continue;
|
|
2265
|
+
}
|
|
2266
|
+
if (old === undefined) {
|
|
2267
|
+
into[s] = { may: new Set(cur.may), must: new Set(cur.must) };
|
|
2268
|
+
changed = true;
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
const may = new Set([...old.may, ...cur.may]);
|
|
2272
|
+
const must = new Set([...old.must].filter((o) => cur.must.has(o)));
|
|
2273
|
+
if (may.size !== old.may.size || must.size !== old.must.size) {
|
|
2274
|
+
into[s] = { may, must };
|
|
2275
|
+
changed = true;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
for (let b = 0; b < irBlocks.length; b++) {
|
|
2281
|
+
const at = into[b];
|
|
2282
|
+
if (!live.has(b) || at === undefined) {
|
|
2283
|
+
continue;
|
|
2284
|
+
}
|
|
2285
|
+
const cur: Pending = { may: new Set(at.may), must: new Set(at.must) };
|
|
2286
|
+
for (const op of irBlocks[b].ops) {
|
|
2287
|
+
// a struct a call is passed by value is read by that call (`byValueReads`), not handed over
|
|
2288
|
+
if (op.opcode === 'call' && byOp.has(op)) {
|
|
2289
|
+
step(op, cur);
|
|
2290
|
+
}
|
|
2291
|
+
if (op.opcode === 'call') {
|
|
2292
|
+
const store = [...cur.may].filter((p) => bytes(p).some((o) => cur.must.has(o))).sort((x, y) => x.at - y.at)[0];
|
|
2293
|
+
if (store !== undefined) {
|
|
2294
|
+
const callee = calleeName(op);
|
|
2295
|
+
fail(
|
|
2296
|
+
`the ${store.width}-byte store at [sp,#${store.at}) through a captured address reaches the call to ` +
|
|
2297
|
+
`${callee === undefined ? 'an indirect callee' : `\`${callee}\``} unread on every path, and no ` +
|
|
2298
|
+
`${from === undefined ? 'load after a call' : from === 0 ? 'load at or beneath it' : `load at or beneath it, from [sp,#${from}) up,`} proves it a local's — it may be ` +
|
|
2299
|
+
"that call's stack argument, which the lift would drop",
|
|
2300
|
+
);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
step(op, cur);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
|
|
1305
2308
|
/** THE ACCEPTANCE'S PREMISE, RE-ASKED OF THE IR. `capturedObjectIsTheWholeFrame` is a reading
|
|
1306
2309
|
* of the TEXT and it is the one thing in this file that switches a refusal OFF, so the two
|
|
1307
2310
|
* facts it claims are re-proven here, where they are exact, rather than left to the
|
|
@@ -1356,6 +2359,26 @@ function recheckWholeFramePremise({
|
|
|
1356
2359
|
}
|
|
1357
2360
|
}
|
|
1358
2361
|
|
|
2362
|
+
/** THE ESCAPE EVENT'S PREMISE, RE-ASKED OF THE IR — `recheckWholeFramePremise`'s first half at
|
|
2363
|
+
* any frame size. The outgoing-argument analysis lets a frame address leaving this function end
|
|
2364
|
+
* the pending stores from its offset up (`StackArgsEscape`), reading the escape off what a
|
|
2365
|
+
* REGISTER holds at a `bl` or a word `str`. Where its verdict leans on that (it would refuse
|
|
2366
|
+
* without the escapes), each offset it read must reach a call operand or a word store outside the
|
|
2367
|
+
* frame in the lifted function too. It catches an arity that shrinks after the event was read
|
|
2368
|
+
* without passing the lowering's `CallSite.leftOut` question. Same two arms, same width as the
|
|
2369
|
+
* licence they re-prove. */
|
|
2370
|
+
function recheckEscapesReliedOn(offsets: ReadonlySet<number>, uses: FrameUses | undefined, fail: Refuse): void {
|
|
2371
|
+
for (const off of offsets) {
|
|
2372
|
+
if (uses === undefined || (!uses.passedToCallee.has(off) && !uses.publishedOutward.has(off))) {
|
|
2373
|
+
fail(
|
|
2374
|
+
`the outgoing-argument analysis set aside the stores from [sp,#${off}] up on that address escaping, and in the ` +
|
|
2375
|
+
'lifted function no call takes it and no word store publishes it outside this frame — so nothing rules out ' +
|
|
2376
|
+
'an outgoing stack argument there',
|
|
2377
|
+
);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
|
|
1359
2382
|
/** NOT A STRUCT-RETURN TEMP. A one-word frame rules out agbcc's block-copy bases (each needs
|
|
1360
2383
|
* two words) but NOT the hidden return pointer of a <=4-byte non-integer-like struct, which is
|
|
1361
2384
|
* exactly one word: `struct S4 { char a,b,c,d; }; struct S4 s = mk(x);` compiles to `add
|
|
@@ -1475,8 +2498,11 @@ function failIfSlotKeysIt(
|
|
|
1475
2498
|
/** ONE OBJECT IN MEMORY, the answer a device read or a callee write nothing bounds is given
|
|
1476
2499
|
* instead of a refusal (`oneObject`, requested by the relift test where the escapes are judged).
|
|
1477
2500
|
* Every byte of `[from, to)` is declared one `u8` array whose address the device or the callee
|
|
1478
|
-
* holds (`writerNotKept` says which writers it can hold)
|
|
1479
|
-
*
|
|
2501
|
+
* holds (`writerNotKept` says which writers it can hold) — or one per part, where the accesses
|
|
2502
|
+
* prove the bytes are several objects no use reads across (`provenParts`) — and every access
|
|
2503
|
+
* inside it is a cast-spelled access to that array (an element of a part, by its index:
|
|
2504
|
+
* structure.ts `framePartDecls`), or a view of the union it is declared as where two widths
|
|
2505
|
+
* meet at one byte — so each store the machine made there is a store the
|
|
1480
2506
|
* recompile makes too. That is what agbcc does for an object whose address escaped: it keeps
|
|
1481
2507
|
* every store to it, in order (flow.c deletes a memory store only when an identical later
|
|
1482
2508
|
* store in the same block overwrites it). And it is right whichever the source had there: a
|
|
@@ -1489,6 +2515,8 @@ function keepAsOneObject({
|
|
|
1489
2515
|
uses,
|
|
1490
2516
|
usedSlotOffsets,
|
|
1491
2517
|
returnsWithoutHiddenPointer,
|
|
2518
|
+
arrayOrder,
|
|
2519
|
+
arrayAlignment,
|
|
1492
2520
|
fail,
|
|
1493
2521
|
}: {
|
|
1494
2522
|
oneObject: FrameRange;
|
|
@@ -1497,6 +2525,8 @@ function keepAsOneObject({
|
|
|
1497
2525
|
uses: FrameUses;
|
|
1498
2526
|
usedSlotOffsets: ReadonlySet<number>;
|
|
1499
2527
|
returnsWithoutHiddenPointer: (callee: string) => boolean;
|
|
2528
|
+
arrayOrder: TargetDescription['compilerBehaviors']['addressTakenArrayOrder'];
|
|
2529
|
+
arrayAlignment: TargetDescription['compilerBehaviors']['addressTakenArrayAlignment'];
|
|
1500
2530
|
fail: Refuse;
|
|
1501
2531
|
}): void {
|
|
1502
2532
|
const kept = `the bytes [sp,#${from}) to [sp,#${to}) kept as one object`;
|
|
@@ -1514,22 +2544,41 @@ function keepAsOneObject({
|
|
|
1514
2544
|
if (writer !== null) {
|
|
1515
2545
|
fail(`${kept} cannot hold every writer — ${writer}`);
|
|
1516
2546
|
}
|
|
1517
|
-
// A runtime index names no byte, so
|
|
1518
|
-
//
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
2547
|
+
// A runtime index names no byte, so the rule below is asked of it at EVERY byte it may reach:
|
|
2548
|
+
// all of them where nothing bounds it, the bytes its `reach` names where one does — a byte no
|
|
2549
|
+
// index reaches is read at one type whatever the index's is, and accesses that share no byte
|
|
2550
|
+
// cannot serve each other a stale value. Two indexed widths that may share a byte refuse here;
|
|
2551
|
+
// an index beside a member of another width refuses after the members are checked against each
|
|
2552
|
+
// other, so a member pair keeps naming its own byte (`indexedWidth` says why one width is
|
|
2553
|
+
// enough).
|
|
2554
|
+
const indexes = [...uses.indexed.values()].flat();
|
|
2555
|
+
indexes.forEach((x, i) => {
|
|
2556
|
+
for (const y of indexes.slice(i + 1)) {
|
|
2557
|
+
const { refusal } = indexedWidth([x, y]);
|
|
2558
|
+
if (refusal !== null && mayShare(x.reach, y.reach)) {
|
|
2559
|
+
fail(`a runtime index into ${kept} ${refusal}`);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
});
|
|
2563
|
+
// ONE TYPE PER BYTE, OR ONE UNION. agbcc at -O2 turns on type-based alias analysis
|
|
2564
|
+
// (toplev.c:3616), and compiled, a `u32` read through a cast is served the earlier `u32` store
|
|
2565
|
+
// straight past a `u16` store to the same bytes — a stale value. So a byte two accesses of
|
|
2566
|
+
// different widths reach cannot be spelled through casts. A byte access is exempt, since
|
|
2567
|
+
// character types alias everything, and so is a signedness difference, since the signed and
|
|
2568
|
+
// unsigned types of one width share an alias set (c-common.c:1962-1974). The first audit asks
|
|
2569
|
+
// for this answer rather than refusing two widths — an object read at both, or a `strh` member
|
|
2570
|
+
// through the captured address over a word the slot model had — so this is where they are judged.
|
|
2571
|
+
//
|
|
2572
|
+
// What two widths at one byte CAN be spelled as is a UNION of per-width views, every access made
|
|
2573
|
+
// directly through it (`sp0.half[1]`): c_get_alias_set gives any access through a member of a
|
|
2574
|
+
// union alias set 0 (c-common.c, the COMPONENT_REF walk), which conflicts with every set, so no
|
|
2575
|
+
// read moves past a store to the same bytes — compiled, a `u16` read, a `u32` store and the same
|
|
2576
|
+
// read again reloads through the union and returns the stale value through casts. So the object
|
|
2577
|
+
// is stamped with the views its accesses use (`unionViews`) and declared as that union
|
|
2578
|
+
// (structure.ts `laddrType`), where every access to it is a fixed offset on the object itself,
|
|
2579
|
+
// aligned to its own width — the cases `unionRefusal` names refuse as before.
|
|
1532
2580
|
const widthAt = new Map<number, number>();
|
|
2581
|
+
let twoWidths: { b: number; had: number; width: number } | undefined;
|
|
1533
2582
|
for (const m of uses.members) {
|
|
1534
2583
|
if (m.at < from || m.at + m.width > to) {
|
|
1535
2584
|
fail(`an access of ${m.width} bytes at [sp,#${m.at}) reaches past ${kept}`);
|
|
@@ -1540,14 +2589,32 @@ function keepAsOneObject({
|
|
|
1540
2589
|
for (let b = m.at; b < m.at + m.width; b++) {
|
|
1541
2590
|
const had = widthAt.get(b);
|
|
1542
2591
|
if (had !== undefined && had !== m.width) {
|
|
1543
|
-
|
|
1544
|
-
`the byte at [sp,#${b}] of ${kept} is accessed ${had} and ${m.width} bytes wide, and agbcc at -O2 ` +
|
|
1545
|
-
'lets a read of one type pass a store of the other (strict aliasing) — a cast spelling would read stale',
|
|
1546
|
-
);
|
|
2592
|
+
twoWidths ??= { b, had, width: m.width };
|
|
1547
2593
|
}
|
|
1548
2594
|
widthAt.set(b, m.width);
|
|
1549
2595
|
}
|
|
1550
2596
|
}
|
|
2597
|
+
if (twoWidths !== undefined) {
|
|
2598
|
+
// any runtime index, a byte one too, and wherever its reach lies: `indexedWidth` leaves width
|
|
2599
|
+
// 1 out because a char access agrees with every width, but a union view is spelled for fixed
|
|
2600
|
+
// offsets on the object alone, and an indexed access is not one (the rewrite below refuses it)
|
|
2601
|
+
const why = unionRefusal({ from, to, uses, indexed: uses.indexed.size > 0 });
|
|
2602
|
+
if (why !== null) {
|
|
2603
|
+
fail(
|
|
2604
|
+
`the byte at [sp,#${twoWidths.b}] of ${kept} is accessed ${twoWidths.had} and ${twoWidths.width} bytes ` +
|
|
2605
|
+
`wide, ${STALE_ACROSS_TYPES}, and ${why}`,
|
|
2606
|
+
);
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
for (const [b, had] of widthAt) {
|
|
2610
|
+
const by = indexes.find((x) => x.width !== 1 && x.width !== had && mayTouch(x.reach, b, 1));
|
|
2611
|
+
if (by !== undefined) {
|
|
2612
|
+
fail(
|
|
2613
|
+
`the byte at [sp,#${b}] of ${kept} is accessed ${had} bytes wide and by a runtime index ${by.width} ` +
|
|
2614
|
+
`bytes wide, ${STALE_ACROSS_TYPES}`,
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
1551
2618
|
// Rewritten onto one `laddr` at `from`: an access through a member at `k` becomes an access
|
|
1552
2619
|
// at `k - from` off the object, and a member address used any other way becomes the object's
|
|
1553
2620
|
// address moved by that constant.
|
|
@@ -1562,15 +2629,35 @@ function keepAsOneObject({
|
|
|
1562
2629
|
// nothing here sees, so a spin on such a byte compiles unqualified; the per-object stamp has the
|
|
1563
2630
|
// same gap (`stampObjects`: not on an ordinary `&local` argument).
|
|
1564
2631
|
const reread = uses.publishedToWriter.size > 0;
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
2632
|
+
// …and declared as SEVERAL objects where the accesses prove the bytes are several
|
|
2633
|
+
// (`provenParts`), each rewritten on its own: an access through a member at `k` lands on the
|
|
2634
|
+
// part that holds `k`, at `k` less the part's start. Only on a compiler that places address-
|
|
2635
|
+
// taken arrays in a known order against their declarations (`addressTakenArrayOrder`): the
|
|
2636
|
+
// parts are declared in that order, or the recompile swaps their addresses. And at a known
|
|
2637
|
+
// alignment (`addressTakenArrayAlignment`), which says where a part may start.
|
|
2638
|
+
const parts =
|
|
2639
|
+
twoWidths === undefined && arrayOrder !== undefined && arrayAlignment !== undefined
|
|
2640
|
+
? provenParts({ from, to }, objects, uses, arrayAlignment)
|
|
2641
|
+
: [{ from, to }];
|
|
2642
|
+
// …stamped `part` where there are several: storage a copy proved one object of the source's, so
|
|
2643
|
+
// its bytes are that object's elements rather than the one object's mix of members and spills,
|
|
2644
|
+
// and the emitter may name them as elements (structure.ts `framePartDecls`), of the type their
|
|
2645
|
+
// accesses agree on (`stampPartElements`, below)
|
|
2646
|
+
const laddrs = parts.map(({ from: lo, to: hi }) =>
|
|
2647
|
+
mkOp('laddr', {
|
|
2648
|
+
results: [mkValue(T.unk(32))],
|
|
2649
|
+
attrs: { off: lo, width: 1, signed: false, count: hi - lo, ...(parts.length > 1 ? { part: true } : {}) },
|
|
2650
|
+
}),
|
|
2651
|
+
);
|
|
2652
|
+
const partOf = (at: number) => parts.findIndex((p) => at >= p.from && at < p.to);
|
|
2653
|
+
const object = laddrs[0];
|
|
1569
2654
|
const base = object.results[0];
|
|
1570
2655
|
const memberAt = new Map<Value, number>();
|
|
1571
2656
|
for (const op of [...objects.values()].flat()) {
|
|
1572
2657
|
memberAt.set(op.results[0], op.attrs.off as number);
|
|
1573
2658
|
}
|
|
2659
|
+
const baseAt = (at: number) => laddrs[partOf(at)].results[0];
|
|
2660
|
+
const startAt = (at: number) => parts[partOf(at)].from;
|
|
1574
2661
|
// The qualifier goes on every access the classification judged one (`accessOps`) — through a
|
|
1575
2662
|
// member, a runtime index into one, or a phi that carries one (`p = c ? buf : gOther; while
|
|
1576
2663
|
// (*p == 0);`). Only an access through a member is rebased: a phi's incoming member is
|
|
@@ -1584,14 +2671,17 @@ function keepAsOneObject({
|
|
|
1584
2671
|
op.attrs = { ...op.attrs, volatile: true };
|
|
1585
2672
|
}
|
|
1586
2673
|
if (access && at !== undefined) {
|
|
1587
|
-
op.operands = [
|
|
1588
|
-
op.attrs = { ...op.attrs, off: (op.attrs.off as number) + at -
|
|
2674
|
+
op.operands = [baseAt(at), ...op.operands.slice(1)];
|
|
2675
|
+
op.attrs = { ...op.attrs, off: (op.attrs.off as number) + at - startAt(at) };
|
|
1589
2676
|
}
|
|
1590
2677
|
op.operands.forEach((v) => memberAt.has(v) && stillUsed.add(v));
|
|
1591
2678
|
(op.successors ?? []).forEach((sx) => sx.args.forEach((v) => memberAt.has(v) && stillUsed.add(v)));
|
|
1592
2679
|
}
|
|
1593
2680
|
}
|
|
1594
|
-
const atBase = (v: Value): Value =>
|
|
2681
|
+
const atBase = (v: Value): Value => {
|
|
2682
|
+
const at = memberAt.get(v);
|
|
2683
|
+
return at !== undefined && at === startAt(at) ? baseAt(at) : v;
|
|
2684
|
+
};
|
|
1595
2685
|
for (const blk of irBlocks) {
|
|
1596
2686
|
blk.ops = blk.ops.flatMap((op) => {
|
|
1597
2687
|
const at = op.opcode === 'laddr' ? memberAt.get(op.results[0]) : undefined;
|
|
@@ -1600,17 +2690,282 @@ function keepAsOneObject({
|
|
|
1600
2690
|
(op.successors ?? []).forEach((sx) => (sx.args = sx.args.map(atBase)));
|
|
1601
2691
|
return [op];
|
|
1602
2692
|
}
|
|
1603
|
-
if (!stillUsed.has(op.results[0]) || at ===
|
|
2693
|
+
if (!stillUsed.has(op.results[0]) || at === startAt(at)) {
|
|
1604
2694
|
return [];
|
|
1605
2695
|
}
|
|
1606
2696
|
const by = mkValue(T.unk(32));
|
|
1607
2697
|
return [
|
|
1608
|
-
mkOp('const', { results: [by], attrs: { value: at -
|
|
1609
|
-
mkOp('add', { operands: [
|
|
2698
|
+
mkOp('const', { results: [by], attrs: { value: at - startAt(at) } }),
|
|
2699
|
+
mkOp('add', { operands: [baseAt(at), by], results: op.results }),
|
|
1610
2700
|
];
|
|
1611
2701
|
});
|
|
1612
2702
|
}
|
|
1613
|
-
|
|
2703
|
+
if (twoWidths !== undefined) {
|
|
2704
|
+
// …every access is now a fixed offset on the object itself, which is what a view spells; one
|
|
2705
|
+
// that is not was carried there by a phi, as a pointer, and a pointer to the union's bytes is
|
|
2706
|
+
// read through a cast again
|
|
2707
|
+
for (const op of uses.accessOps.keys()) {
|
|
2708
|
+
if (op.operands[0] !== base) {
|
|
2709
|
+
fail(
|
|
2710
|
+
`the bytes [sp,#${from}) to [sp,#${to}) are accessed at two widths, which only a union of views ` +
|
|
2711
|
+
'spells without a stale read — and an access reaches them through a pointer a phi carries, which ' +
|
|
2712
|
+
'no view of the union names',
|
|
2713
|
+
);
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
object.attrs = { ...object.attrs, views: unionViews(irBlocks, base) };
|
|
2717
|
+
}
|
|
2718
|
+
if (laddrs.length > 1) {
|
|
2719
|
+
stampPartElements(irBlocks, laddrs, uses.accessOps);
|
|
2720
|
+
}
|
|
2721
|
+
irBlocks[0].ops.unshift(...(arrayOrder === 'descending' ? [...laddrs].reverse() : laddrs));
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
/** THE PARTS THE ACCESSES PROVE `[from, to)` IS MADE OF, lowest first — or the whole range, one
|
|
2725
|
+
* part, wherever they do not. A part is a maximal run of bytes that overlapping uses chain
|
|
2726
|
+
* together — a fixed-offset access, a struct passed by value, and an escape a standard block copy
|
|
2727
|
+
* with a literal count bounds (`escapeExtents`) each name the bytes they touch — and a run that
|
|
2728
|
+
* merely abuts the one below it starts a new part only where such a copy starts — and so must
|
|
2729
|
+
* the lowest part, or nothing is split. Two brace-initialised arrays side by side are agbcc's
|
|
2730
|
+
* `memcpy(a, .LC0, 16); memcpy(b, .LC2, 16)`, and declared as one `u8[32]` neither image can be
|
|
2731
|
+
* folded into its own local (l3/constinit.ts).
|
|
2732
|
+
*
|
|
2733
|
+
* ONE PART, never a refusal, whenever a byte could be read through two parts: a runtime index
|
|
2734
|
+
* nothing bounds (it names no byte, so it may reach every one) or one whose `reach` (the range
|
|
2735
|
+
* ir/bits.ts proves of the index: `a[(i & 7) * 2]`) crosses a part, an escape nothing bounds (a
|
|
2736
|
+
* callee or a holder that may reach any byte from the address it was given), a byte no use names
|
|
2737
|
+
* (nothing says which side it belongs to), or a use through a member that lands outside the part
|
|
2738
|
+
* its own address lies in (`&a[16]` read at [-1]: one pointer walking two parts is one object). The
|
|
2739
|
+
* union views of two widths at one byte are not split either (the caller asks only without
|
|
2740
|
+
* them). An access straddling two runs chains them into one part by construction. */
|
|
2741
|
+
function provenParts(
|
|
2742
|
+
{ from, to }: FrameRange,
|
|
2743
|
+
objects: ReadonlyMap<number, readonly Op[]>,
|
|
2744
|
+
uses: FrameUses,
|
|
2745
|
+
alignment: number,
|
|
2746
|
+
): FrameRange[] {
|
|
2747
|
+
const whole = [{ from, to }];
|
|
2748
|
+
const touched: { owner: number; lo: number; hi: number }[] = [];
|
|
2749
|
+
for (const [op, { off, index }] of uses.accessOps) {
|
|
2750
|
+
// …an access through a runtime index is not at its own displacement: it is its reach (below)
|
|
2751
|
+
if (index !== undefined) {
|
|
2752
|
+
continue;
|
|
2753
|
+
}
|
|
2754
|
+
const at = off + (op.attrs.off as number);
|
|
2755
|
+
touched.push({ owner: off, lo: at, hi: at + (op.attrs.width as number) });
|
|
2756
|
+
}
|
|
2757
|
+
// An escape and a runtime index each touch every byte they may reach (`frameReaches`), so each
|
|
2758
|
+
// chains the runs it spans as one access as wide would — a reach across a copy's start makes
|
|
2759
|
+
// the two copies one part — and a reach that starts in a part other than its object's fails the
|
|
2760
|
+
// owner test below
|
|
2761
|
+
const reaches = frameReaches(uses);
|
|
2762
|
+
for (const { owner, reach } of reaches) {
|
|
2763
|
+
if (reach === null) {
|
|
2764
|
+
return whole;
|
|
2765
|
+
}
|
|
2766
|
+
touched.push({ owner, ...reach });
|
|
2767
|
+
}
|
|
2768
|
+
for (const [off, reads] of uses.byValueReads) {
|
|
2769
|
+
reads.forEach(({ type }) =>
|
|
2770
|
+
touched.push({ owner: off, lo: off, hi: off + (type.kind === 'struct' ? (type.size ?? Infinity) : Infinity) }),
|
|
2771
|
+
);
|
|
2772
|
+
}
|
|
2773
|
+
// …and a boundary only where a bounded copy starts: an address handed to `memcpy` is where
|
|
2774
|
+
// the source put an object it fills. A run starting anywhere else is bytes reached from below,
|
|
2775
|
+
// which says nothing of where one object ends — the one-object relift reads every slot through
|
|
2776
|
+
// an address of its own, so a struct spilled word by word and read through its base shows a
|
|
2777
|
+
// run per word, and is one object at every word. The LOWEST part is held to the same test
|
|
2778
|
+
// (below): where it is only stores, a copy above it is as likely `memcpy(&buf[4], s, 12)` into
|
|
2779
|
+
// the middle of one array as the start of a second one, and declared as two the recompile
|
|
2780
|
+
// reads `buf[6]` through a base it holds where the target re-formed sp+6. Only a copy's
|
|
2781
|
+
// DESTINATION counts: a copy reading OUT of the frame says where the source took bytes from,
|
|
2782
|
+
// which is as likely `memcpy(g, &w[2], 8)` from the middle of one array as a second object.
|
|
2783
|
+
const starts = copyStarts(reaches);
|
|
2784
|
+
const parts: { from: number; to: number }[] = [];
|
|
2785
|
+
for (const { lo, hi } of [...touched].sort((a, b) => a.lo - b.lo)) {
|
|
2786
|
+
const last = parts.at(-1);
|
|
2787
|
+
if (last !== undefined && (lo < last.to || (lo === last.to && !starts.has(lo)))) {
|
|
2788
|
+
last.to = Math.max(last.to, hi);
|
|
2789
|
+
} else {
|
|
2790
|
+
parts.push({ from: lo, to: hi });
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
const tiles =
|
|
2794
|
+
parts.length > 1 &&
|
|
2795
|
+
parts[0].from === from &&
|
|
2796
|
+
parts.at(-1)!.to === to &&
|
|
2797
|
+
parts.every((p, i) => i === 0 || parts[i - 1].to === p.from);
|
|
2798
|
+
const partOf = (at: number) => parts.findIndex((p) => at >= p.from && at < p.to);
|
|
2799
|
+
if (
|
|
2800
|
+
!tiles ||
|
|
2801
|
+
[...objects.keys()].some((off) => partOf(off) < 0) ||
|
|
2802
|
+
touched.some(({ owner, lo }) => partOf(owner) !== partOf(lo))
|
|
2803
|
+
) {
|
|
2804
|
+
return whole;
|
|
2805
|
+
}
|
|
2806
|
+
// …and a part starts only where the compiler places a local array of its own: at a multiple of
|
|
2807
|
+
// its `alignment` (agbcc pads `u8 a[18]` to 20, so a split there declares `b` at sp+20 where the
|
|
2808
|
+
// target had it at sp+18; gcc 2.7.2 on MIPS starts every one at a multiple of 8, so a part at
|
|
2809
|
+
// sp+20 is placed at sp+24 and the frame grows), and never as ONE byte, which declared alone is
|
|
2810
|
+
// a `u8` scalar whose address nothing takes — kept in a register, it drops the frame byte and
|
|
2811
|
+
// the store the target made. Such a part joins the one below it, and the lowest one byte joins
|
|
2812
|
+
// the one above: fewer parts only ever read as the one object does, so this costs a split,
|
|
2813
|
+
// never a value.
|
|
2814
|
+
const merged: { from: number; to: number }[] = [];
|
|
2815
|
+
for (const p of parts) {
|
|
2816
|
+
const last = merged.at(-1);
|
|
2817
|
+
if (last !== undefined && (p.from % alignment !== 0 || p.to - p.from === 1 || last.to - last.from === 1)) {
|
|
2818
|
+
last.to = p.to;
|
|
2819
|
+
} else {
|
|
2820
|
+
merged.push({ ...p });
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
return merged.length > 1 && merged.every((p) => starts.has(p.from)) ? merged : whole;
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
/** Why the one object's bytes, accessed at two widths, cannot be spelled as a union of per-width
|
|
2827
|
+
* views, or null. A view names an ELEMENT, so every access must sit at a multiple of its own width
|
|
2828
|
+
* from the object's start and every view must tile the object; and it names it at a constant
|
|
2829
|
+
* index, so a runtime index into the object — whose element width a view could carry, but whose
|
|
2830
|
+
* spelling nothing here builds — keeps the refusal, and so does a qualifier on the accesses
|
|
2831
|
+
* (`publishedToWriter`), which a member access of an unqualified union cannot carry. */
|
|
2832
|
+
function unionRefusal({
|
|
2833
|
+
from,
|
|
2834
|
+
to,
|
|
2835
|
+
uses,
|
|
2836
|
+
indexed,
|
|
2837
|
+
}: {
|
|
2838
|
+
from: number;
|
|
2839
|
+
to: number;
|
|
2840
|
+
uses: FrameUses;
|
|
2841
|
+
indexed: boolean;
|
|
2842
|
+
}): string | null {
|
|
2843
|
+
if (indexed) {
|
|
2844
|
+
return 'a runtime index reaches them too, which no view of a union is spelled for';
|
|
2845
|
+
}
|
|
2846
|
+
if (uses.publishedToWriter.size > 0) {
|
|
2847
|
+
return 'the address is stored where a writer may hold it, and a view of a union carries no per-access qualifier';
|
|
2848
|
+
}
|
|
2849
|
+
for (const m of uses.members) {
|
|
2850
|
+
if ((m.at - from) % m.width !== 0) {
|
|
2851
|
+
return `the ${m.width}-byte access at [sp,#${m.at}) is not aligned to its width, so no view of a union names it`;
|
|
2852
|
+
}
|
|
2853
|
+
if ((to - from) % m.width !== 0) {
|
|
2854
|
+
return `the ${to - from} bytes are not a whole number of ${m.width}-byte elements, so no view of a union tiles them`;
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
return null;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
/** The views a union object's accesses use, as `laddr`'s `views` attr: one entry per view, its
|
|
2861
|
+
* element width in bytes, NEGATIVE for a signed narrow view — which views, by the rule
|
|
2862
|
+
* raise/structs.ts declares a struct member's union by (ir/types.ts `unionViewSigns`). A word's
|
|
2863
|
+
* one view is `s32` whichever sign it carries, so it is entered unsigned. Widest first, unsigned
|
|
2864
|
+
* before signed. */
|
|
2865
|
+
function unionViews(irBlocks: readonly Block[], base: Value): number[] {
|
|
2866
|
+
const byWidth = new Map<number, { isLoad: boolean; signed: boolean; stamped: boolean }[]>();
|
|
2867
|
+
for (const op of irBlocks.flatMap((b) => b.ops)) {
|
|
2868
|
+
if ((op.opcode === 'load' || op.opcode === 'store') && op.operands[0] === base) {
|
|
2869
|
+
const w = op.attrs.width as number;
|
|
2870
|
+
(byWidth.get(w) ?? byWidth.set(w, []).get(w)!).push({
|
|
2871
|
+
isLoad: op.opcode === 'load',
|
|
2872
|
+
signed: (op.attrs.signed as boolean) ?? false,
|
|
2873
|
+
stamped: op.opcode === 'store' && op.attrs.signed !== undefined,
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
return [...byWidth]
|
|
2878
|
+
.sort(([a], [b]) => b - a)
|
|
2879
|
+
.flatMap(([w, accesses]) => unionViewSigns(w, accesses).map((sg) => (sg && w !== 4 ? -w : w)));
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
/** THE ELEMENT EACH PROVEN PART IS DECLARED WITH, from the census of its own accesses — the way
|
|
2883
|
+
* `unionViews` types a union's views from theirs — stamped `elem` on the part's `laddr` (a width in
|
|
2884
|
+
* bytes, negative for a signed narrow one; absent is the storage default, `u8`).
|
|
2885
|
+
*
|
|
2886
|
+
* A part is an object of the source's (`provenParts`), and its bare element `sp64[i]` is not the
|
|
2887
|
+
* same compile as the cast `((u16 *)sp64)[i]` beside a second array whose base agbcc holds (see
|
|
2888
|
+
* structure.ts `framePartDecls`) — but the bare spelling exists only for an access of the declared
|
|
2889
|
+
* element's width and extension, so a part of `u16` or `s8` elements declared `u8` reaches no
|
|
2890
|
+
* access bare. Every access agreeing on one element is the evidence a declaration of it needs. An access that disagrees with the declaration keeps its cast, which is
|
|
2891
|
+
* one value under any declaration (structure.ts `bareArrayElement`), so the stamp never changes
|
|
2892
|
+
* what an access reads; nor what it aliases, since a bare element of a `u16` array is a `u16`
|
|
2893
|
+
* lvalue, the type the cast it replaces names.
|
|
2894
|
+
*
|
|
2895
|
+
* - One width over every access (a block copy or any other escape has none), aligned to it — the
|
|
2896
|
+
* part's start, its size and every fixed offset — else the storage default.
|
|
2897
|
+
* - Its extension from the LOADS: one extension is that one, both is no element (no evidence picks
|
|
2898
|
+
* between them). An unsigned byte read whose every reader sign-extends it is either: agbcc has
|
|
2899
|
+
* no `ldrsb` with an immediate offset and mwcc no signed byte load at all, so `s8 a[64]`'s `a[3]`
|
|
2900
|
+
* is that read (structure.ts `declaredExtendedRead` spells it bare once the part says `s8`). With
|
|
2901
|
+
* no load committing, such a read makes the element signed, and else a store raise/storesign.ts
|
|
2902
|
+
* stamped signed does, as `unionViewSigns` reads a view's.
|
|
2903
|
+
* - A word is one element whatever its loads say (`scalarTypeForAccess`).
|
|
2904
|
+
* An access whose part is not known — one through a pointer a phi carries, an index nothing bounds
|
|
2905
|
+
* — leaves every part at the default: its width is not counted, so no census is whole. */
|
|
2906
|
+
function stampPartElements(
|
|
2907
|
+
irBlocks: readonly Block[],
|
|
2908
|
+
laddrs: readonly Op[],
|
|
2909
|
+
accessOps: ReadonlyMap<Op, FrameAccessOp>,
|
|
2910
|
+
): void {
|
|
2911
|
+
const bases = laddrs.map((d) => d.results[0]);
|
|
2912
|
+
const start = laddrs.map((d) => d.attrs.off as number);
|
|
2913
|
+
const end = laddrs.map((d) => (d.attrs.off as number) + (d.attrs.count as number));
|
|
2914
|
+
const users = new Map<Value, Op[]>();
|
|
2915
|
+
for (const op of irBlocks.flatMap((b) => b.ops)) {
|
|
2916
|
+
op.operands.forEach((v) => (users.get(v) ?? users.set(v, []).get(v)!).push(op));
|
|
2917
|
+
}
|
|
2918
|
+
type Access = { width: number; at: number | null; load: boolean; signed: boolean | null; stamped: boolean };
|
|
2919
|
+
const census = laddrs.map((): Access[] => []);
|
|
2920
|
+
for (const [op, { index }] of accessOps) {
|
|
2921
|
+
const fixed = bases.indexOf(op.operands[0]);
|
|
2922
|
+
const reach = index?.reach ?? null;
|
|
2923
|
+
const part =
|
|
2924
|
+
fixed >= 0 ? fixed : reach === null ? -1 : start.findIndex((lo, i) => reach.lo >= lo && reach.hi <= end[i]);
|
|
2925
|
+
if (part < 0) {
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2928
|
+
const width = op.attrs.width as number;
|
|
2929
|
+
const load = op.opcode === 'load';
|
|
2930
|
+
// …a sign extension as the frontend lifts it: mwcc's `extsb`, or the `lsl #24; asr #24` pair
|
|
2931
|
+
// (the idiom stage folds it to a `sext` later, pattern/engine.ts `sextPat`)
|
|
2932
|
+
const extends8 = (u: Op): boolean =>
|
|
2933
|
+
(u.opcode === 'sext' && u.attrs.width === 8) ||
|
|
2934
|
+
(u.opcode === 'shl' &&
|
|
2935
|
+
u.attrs.imm === 24 &&
|
|
2936
|
+
(users.get(u.results[0]) ?? []).length > 0 &&
|
|
2937
|
+
users.get(u.results[0])!.every((s) => s.opcode === 'shr_s' && s.attrs.imm === 24));
|
|
2938
|
+
const readers = load ? (users.get(op.results[0]) ?? []) : [];
|
|
2939
|
+
const either = width === 1 && op.attrs.signed !== true && readers.length > 0 && readers.every(extends8);
|
|
2940
|
+
census[part].push({
|
|
2941
|
+
width,
|
|
2942
|
+
at: fixed >= 0 ? (op.attrs.off as number) : null,
|
|
2943
|
+
load,
|
|
2944
|
+
signed: !load || either ? null : op.attrs.signed === true,
|
|
2945
|
+
stamped: !load && op.attrs.signed === true,
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
laddrs.forEach((d, i) => {
|
|
2949
|
+
const accesses = census[i];
|
|
2950
|
+
const widths = new Set(accesses.map((a) => a.width));
|
|
2951
|
+
if (widths.size !== 1) {
|
|
2952
|
+
return;
|
|
2953
|
+
}
|
|
2954
|
+
const [w] = widths;
|
|
2955
|
+
const count = d.attrs.count as number;
|
|
2956
|
+
if (start[i] % w !== 0 || count % w !== 0 || accesses.some((a) => a.at !== null && a.at % w !== 0)) {
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
const signs = new Set(accesses.flatMap((a) => (a.signed === null ? [] : [a.signed])));
|
|
2960
|
+
if (signs.size > 1) {
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
const signed = signs.size === 1 ? [...signs][0] : accesses.some((a) => a.load || a.stamped);
|
|
2964
|
+
const elem = signed && w !== 4 ? -w : w;
|
|
2965
|
+
if (elem !== 1) {
|
|
2966
|
+
d.attrs = { ...d.attrs, elem };
|
|
2967
|
+
}
|
|
2968
|
+
});
|
|
1614
2969
|
}
|
|
1615
2970
|
|
|
1616
2971
|
/** One object's shape: `count` elements of `width` bytes. */
|
|
@@ -1640,8 +2995,10 @@ function slotKeys(usedSlotOffsets: ReadonlySet<number>, off: number, width: numb
|
|
|
1640
2995
|
* AN OBJECT OVER A SLOT IS REFUSED LAST, after the one-object answer is asked: that answer
|
|
1641
2996
|
* routes every word of the local area through `laddr`, so no slot is left for the object to
|
|
1642
2997
|
* overlap, and refusing first would decline exactly the frames it exists for — an object at
|
|
1643
|
-
* [sp,#0] whose first member is a word stored `str rN, [sp]
|
|
1644
|
-
*
|
|
2998
|
+
* [sp,#0] whose first member is a word stored `str rN, [sp]`, or a by-value struct parameter
|
|
2999
|
+
* spilled word by word and read through `mov rD, sp` where nothing escapes. The other per-object
|
|
3000
|
+
* refusals here go through `shapeRefused`, for the same reason: where nothing escapes, each of
|
|
3001
|
+
* them asks for that answer too (`chooseFrameModel`). */
|
|
1645
3002
|
function objectShapes({
|
|
1646
3003
|
objects,
|
|
1647
3004
|
uses,
|
|
@@ -1667,30 +3024,63 @@ function objectShapes({
|
|
|
1667
3024
|
for (const [off, acc] of uses.accesses) {
|
|
1668
3025
|
const byIndex = uses.indexed.get(off) ?? [];
|
|
1669
3026
|
const temps = uses.returnTemps.get(off);
|
|
3027
|
+
const passed = uses.byValueReads.get(off);
|
|
3028
|
+
if (passed !== undefined) {
|
|
3029
|
+
// A struct passed BY VALUE (`foldByValueArgs`): this function writes and reads its members,
|
|
3030
|
+
// and the call reads it whole. A struct-return temp passed on, an index, or any other use of
|
|
3031
|
+
// the address is not modelled.
|
|
3032
|
+
const type = passed[0].type;
|
|
3033
|
+
const at = `the object at [sp,#${off}) is passed by value as ${structSpelling(type)}`;
|
|
3034
|
+
if (temps !== undefined) {
|
|
3035
|
+
fail(`${at}, and is also the storage a call returns a struct into — not modelled`);
|
|
3036
|
+
}
|
|
3037
|
+
if (passed.some((p) => !typeEquals(p.type, type))) {
|
|
3038
|
+
fail(`${at} to calls declared to take different types`);
|
|
3039
|
+
}
|
|
3040
|
+
if (byIndex.length > 0) {
|
|
3041
|
+
fail(`${at}, and this function also indexes it — only its members' accesses are modelled`);
|
|
3042
|
+
}
|
|
3043
|
+
if (uses.useCount.get(off) !== acc.length + passed.length) {
|
|
3044
|
+
fail(`${at}, and its address is also used another way — only its members' accesses are modelled`);
|
|
3045
|
+
}
|
|
3046
|
+
const size = type.kind === 'struct' ? type.size! : 0;
|
|
3047
|
+
const past = uses.fixed.find(
|
|
3048
|
+
(f) => uses.accessOps.get(f.op)?.off === off && (f.at < off || f.at + f.width > off + size),
|
|
3049
|
+
);
|
|
3050
|
+
if (past !== undefined) {
|
|
3051
|
+
fail(`${at}, and this function accesses [sp,#${past.at}) of width ${past.width}, past the struct`);
|
|
3052
|
+
}
|
|
3053
|
+
extent.set(off, { width: 1, count: size });
|
|
3054
|
+
aggregateAt.set(off, type);
|
|
3055
|
+
continue;
|
|
3056
|
+
}
|
|
1670
3057
|
if (temps !== undefined) {
|
|
1671
|
-
// The callee writes the whole struct and this function names it
|
|
1672
|
-
//
|
|
1673
|
-
//
|
|
1674
|
-
//
|
|
1675
|
-
// struct into a global (`gS8 = mk8(x)`, which agbcc stages through the temp) included. So
|
|
1676
|
-
// modelling member reads starts with the slot model handing this audit a read return temp.
|
|
3058
|
+
// The callee writes the whole struct and this function names it as the call's destination,
|
|
3059
|
+
// and READS it: each load is of bytes inside the struct, which the struct's declaration
|
|
3060
|
+
// types (raise/aggmembers.ts reads a word's members, structure.ts spells a member access).
|
|
3061
|
+
// A write, an index, or any other use of the address is not modelled.
|
|
1677
3062
|
const callee = temps[0].attrs.target as string;
|
|
1678
3063
|
const type = temps[0].results[0].type;
|
|
1679
3064
|
const spelling = type.kind === 'struct' ? (type.declared ?? `struct ${type.name}`) : typeToString(type);
|
|
3065
|
+
const at = `the object at [sp,#${off}) is where \`${callee}\` returns ${spelling}`;
|
|
1680
3066
|
if (temps.some((t) => !typeEquals(t.results[0].type, type))) {
|
|
1681
3067
|
fail(`the object at [sp,#${off}) is the struct-return storage of calls declared to return different types`);
|
|
1682
3068
|
}
|
|
1683
|
-
if (acc.
|
|
3069
|
+
if (acc.some((a) => !a.isLoad) || byIndex.length > 0) {
|
|
1684
3070
|
fail(
|
|
1685
|
-
|
|
1686
|
-
'
|
|
3071
|
+
`${at}, and this function also ${byIndex.length > 0 ? 'indexes' : 'writes'} it — only a read of a ` +
|
|
3072
|
+
'returned struct is modelled',
|
|
1687
3073
|
);
|
|
1688
3074
|
}
|
|
1689
|
-
if (uses.useCount.get(off) !== temps.length) {
|
|
1690
|
-
fail(
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
3075
|
+
if (uses.useCount.get(off) !== temps.length + acc.length) {
|
|
3076
|
+
fail(`${at}, and its address is also used another way — only the call it is returned by is modelled`);
|
|
3077
|
+
}
|
|
3078
|
+
const size = returnedSize(temps[0]);
|
|
3079
|
+
const past = uses.fixed.find(
|
|
3080
|
+
(f) => uses.accessOps.get(f.op)?.off === off && (f.at < off || f.at + f.width > off + size),
|
|
3081
|
+
);
|
|
3082
|
+
if (past !== undefined) {
|
|
3083
|
+
fail(`${at}, and this function reads [sp,#${past.at}) of width ${past.width}, past the struct`);
|
|
1694
3084
|
}
|
|
1695
3085
|
extent.set(off, { width: 1, count: returnedSize(temps[0]) });
|
|
1696
3086
|
aggregateAt.set(off, type);
|
|
@@ -1721,26 +3111,18 @@ function objectShapes({
|
|
|
1721
3111
|
// …asked for the one object instead, which sizes no object of its own
|
|
1722
3112
|
continue;
|
|
1723
3113
|
}
|
|
1724
|
-
// An indexed access reads
|
|
1725
|
-
//
|
|
1726
|
-
//
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
`a runtime index into the object at [sp,#${off}) accesses ${a.width} bytes, and the storage ` +
|
|
1731
|
-
'nothing else types is declared as bytes — only a byte element is modelled',
|
|
1732
|
-
);
|
|
1733
|
-
}
|
|
1734
|
-
if (a.signed) {
|
|
1735
|
-
model.shapeRefused(
|
|
1736
|
-
`a runtime index into the object at [sp,#${off}) sign-extends, and the storage is declared unsigned`,
|
|
1737
|
-
);
|
|
1738
|
-
}
|
|
3114
|
+
// An indexed access reads THROUGH A CAST on the storage declared below, at its own width
|
|
3115
|
+
// and signedness, as an access to the one object does (`keepAsOneObject`), so the accesses
|
|
3116
|
+
// wider than a byte must be of one width, one alias set (`indexedWidth`).
|
|
3117
|
+
const index = indexedWidth(byIndex);
|
|
3118
|
+
if (index.refusal !== null) {
|
|
3119
|
+
model.shapeRefused(`a runtime index into the object at [sp,#${off}) ${index.refusal}`);
|
|
1739
3120
|
}
|
|
1740
3121
|
// STORAGE, NOT A TYPE. The reservation says how many bytes the frame holds and the
|
|
1741
3122
|
// conjuncts above say they are all this object's, so the declaration commits to an EXTENT
|
|
1742
3123
|
// and to nothing else: `u8 name[n]` over the declared range, unsigned bytes because no
|
|
1743
3124
|
// access named an element type and inventing one is the guess this refuses everywhere else.
|
|
3125
|
+
// An indexed access names only its own width, which its cast spells where it reads.
|
|
1744
3126
|
//
|
|
1745
3127
|
// THE EXTENT IS A ROUNDED ONE, stated because it is not a defect. agbcc reserves the
|
|
1746
3128
|
// local area in whole words, so a source object of 13, 14, 15 or 16 bytes all reserve
|
|
@@ -2058,8 +3440,9 @@ function stampObjects(
|
|
|
2058
3440
|
): void {
|
|
2059
3441
|
for (const [off, ops] of objects) {
|
|
2060
3442
|
const { width, count } = extent.get(off)!;
|
|
2061
|
-
const signed = uses.accesses.get(off)!.some((a) => a.signed);
|
|
2062
3443
|
const aggregate = aggregateAt.get(off);
|
|
3444
|
+
// a declared struct's storage is bytes, whatever signedness its members are read at
|
|
3445
|
+
const signed = aggregate === undefined && uses.accesses.get(off)!.some((a) => a.signed);
|
|
2063
3446
|
for (const op of ops) {
|
|
2064
3447
|
const qualified = uses.published.has(off) || uses.filledFrom.has(off);
|
|
2065
3448
|
op.attrs = {
|
|
@@ -2092,8 +3475,8 @@ function gcd(a: number, b: number): number {
|
|
|
2092
3475
|
return b === 0 ? Math.abs(a) : gcd(b, a % b);
|
|
2093
3476
|
}
|
|
2094
3477
|
|
|
2095
|
-
/** The size of the struct a call stamped `sret` returns through its argument 0 (frontend/
|
|
2096
|
-
* types the call's result as that struct). */
|
|
3478
|
+
/** The size of the struct a call stamped `sret` returns through its argument 0 (frontend/call-plan.ts
|
|
3479
|
+
* types the call's result as that struct, for every frontend whose lowering passes the pointer). */
|
|
2097
3480
|
function returnedSize(call: Op): number {
|
|
2098
3481
|
const t = call.results[0].type;
|
|
2099
3482
|
if (t.kind !== 'struct' || t.size === undefined) {
|
|
@@ -2102,8 +3485,108 @@ function returnedSize(call: Op): number {
|
|
|
2102
3485
|
return t.size;
|
|
2103
3486
|
}
|
|
2104
3487
|
|
|
3488
|
+
/** The block parameters every incoming value of which is a local's address or another such
|
|
3489
|
+
* parameter: the greatest set closed under that, so a loop-carried address is one too. Each maps to
|
|
3490
|
+
* the frame offsets its incoming addresses name, through such parameters. The entry block's are the
|
|
3491
|
+
* function's parameters, and are left out. */
|
|
3492
|
+
function mergedFrameAddresses(irBlocks: readonly Block[]): Map<Value, ReadonlySet<number>> {
|
|
3493
|
+
const laddrOff = new Map<Value, number>();
|
|
3494
|
+
for (const op of irBlocks.flatMap((b) => b.ops)) {
|
|
3495
|
+
if (op.opcode === 'laddr') {
|
|
3496
|
+
laddrOff.set(op.results[0], op.attrs.off as number);
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
const incoming = new Map<Value, Value[]>();
|
|
3500
|
+
for (const b of irBlocks.slice(1)) {
|
|
3501
|
+
for (const p of b.params) {
|
|
3502
|
+
incoming.set(p, []);
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
for (const op of irBlocks.flatMap((b) => b.ops)) {
|
|
3506
|
+
for (const succ of op.successors) {
|
|
3507
|
+
succ.block.params.forEach((p, i) => incoming.get(p)?.push(succ.args[i]));
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
const merged = new Set([...incoming].filter(([, vs]) => vs.length > 0).map(([p]) => p));
|
|
3511
|
+
for (let changed = true; changed;) {
|
|
3512
|
+
changed = false;
|
|
3513
|
+
for (const p of merged) {
|
|
3514
|
+
if (incoming.get(p)!.some((v) => !laddrOff.has(v) && !merged.has(v))) {
|
|
3515
|
+
merged.delete(p);
|
|
3516
|
+
changed = true;
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
const out = new Map<Value, ReadonlySet<number>>();
|
|
3521
|
+
for (const p of merged) {
|
|
3522
|
+
const offs = new Set<number>();
|
|
3523
|
+
const seen = new Set<Value>([p]);
|
|
3524
|
+
const work = [p];
|
|
3525
|
+
while (work.length > 0) {
|
|
3526
|
+
for (const v of incoming.get(work.pop()!)!) {
|
|
3527
|
+
if (laddrOff.has(v)) {
|
|
3528
|
+
offs.add(laddrOff.get(v)!);
|
|
3529
|
+
} else if (!seen.has(v)) {
|
|
3530
|
+
seen.add(v);
|
|
3531
|
+
work.push(v);
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
out.set(p, offs);
|
|
3536
|
+
}
|
|
3537
|
+
return out;
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
/** A STRUCT RETURNED THROUGH A MERGE OF ONE LOCAL'S ADDRESS IS THAT LOCAL. gcc rotates a loop that
|
|
3541
|
+
* calls a struct-returning function, `while (lo > 0) { qr = ldiv(lo, 10); … }`, so `addiu a0,sp,16`
|
|
3542
|
+
* runs once ahead of the loop and once in the delay slot of its back branch, and hoists it into
|
|
3543
|
+
* both arms of `if (x) a = f(x); else a = f(x + 1);` ahead of one shared `jal`. The call then
|
|
3544
|
+
* reads a block parameter, but every path hands it the same frame address, so the call is handed
|
|
3545
|
+
* that address: an `laddr` of it, minted ahead of the call, which the frame-object audit types as
|
|
3546
|
+
* the struct (`declaredStructs`) and `refuseSretOutsideFrame` accepts. A merge of two offsets is
|
|
3547
|
+
* two objects, and is left for the audit to refuse. Run before the audit, on every frontend whose call
|
|
3548
|
+
* lowering passes the hidden pointer. */
|
|
3549
|
+
export function nameMergedSretObjects(irBlocks: Block[]): void {
|
|
3550
|
+
const merged = mergedFrameAddresses(irBlocks);
|
|
3551
|
+
for (const b of irBlocks) {
|
|
3552
|
+
b.ops = b.ops.flatMap((op) => {
|
|
3553
|
+
const offs = op.opcode === 'call' && op.attrs.sret === true ? merged.get(op.operands[0]) : undefined;
|
|
3554
|
+
if (offs === undefined || offs.size !== 1) {
|
|
3555
|
+
return [op];
|
|
3556
|
+
}
|
|
3557
|
+
const at = mkValue(T.unk(32));
|
|
3558
|
+
op.operands = [at, ...op.operands.slice(1)];
|
|
3559
|
+
return [mkOp('laddr', { results: [at], attrs: { off: [...offs][0] } }), op];
|
|
3560
|
+
});
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
/** A STRUCT RETURNED THROUGH MEMORY IS SPELLED `local = f(..)`, which hands the callee that local's
|
|
3565
|
+
* address: the hidden pointer must be a frame object the audit accepted. A global or a pointer
|
|
3566
|
+
* handed over directly — `gS = f(..)` (agbcc, ido, gcc), IDO's `*p = f(..)`, and the caller's own
|
|
3567
|
+
* hidden pointer forwarded by `return f(..)` — is another spelling, which nothing here states. A
|
|
3568
|
+
* merge of frame addresses never reaches here: one that names one address on every path was handed
|
|
3569
|
+
* that address's `laddr` before the audit (`nameMergedSretObjects`), and a merge of two names no one
|
|
3570
|
+
* storage, which the audit refuses first (`addressFlow`: `a phi merges the frame objects`). Asked
|
|
3571
|
+
* of every frontend whose call lowering passes the hidden pointer
|
|
3572
|
+
* (`CallLowering.memoryReturn`). */
|
|
3573
|
+
export function refuseSretOutsideFrame(name: string, irBlocks: readonly Block[], target: TargetDescription): void {
|
|
3574
|
+
const laddrs = new Set(
|
|
3575
|
+
irBlocks.flatMap((b) => b.ops.filter((op) => op.opcode === 'laddr').map((op) => op.results[0])),
|
|
3576
|
+
);
|
|
3577
|
+
for (const op of irBlocks.flatMap((b) => b.ops)) {
|
|
3578
|
+
if (op.opcode === 'call' && op.attrs.sret === true && !laddrs.has(op.operands[0])) {
|
|
3579
|
+
throw new FrontendUnsupportedError(
|
|
3580
|
+
`cannot lift '${name}': \`${op.attrs.target as string}\` returns struct ${typeToString(op.results[0].type)} through the pointer in ${target.argRegs[0]}, ` +
|
|
3581
|
+
'and that pointer is not the address of a local of this frame — a struct returned into a global or ' +
|
|
3582
|
+
'through a pointer is not modelled',
|
|
3583
|
+
);
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
|
|
2105
3588
|
/** THE CALLER-SIDE SEAM. A frontend calls the audit THROUGH this record rather than through the
|
|
2106
|
-
* binding
|
|
3589
|
+
* `auditFrameObjects` binding, so a process outside core can put a wrapped `gates` table in front of a real
|
|
2107
3590
|
* lift (`pnpm bench gates --pass frame-objects`): a module-namespace binding is read-only and
|
|
2108
3591
|
* cannot be swapped (`apps/benchmark/src/run/gate-census.ts`, WHAT PUTS A PASS IN THE REGISTRY). */
|
|
2109
3592
|
export const FRAME_OBJECT_AUDIT: { run: (audit: FrameObjectAudit) => FrameObjectVerdict } = {
|