@asmlift/core 0.10.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/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +33 -12
- package/src/backend/cpp.ts +1 -0
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +17 -5
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +73 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +2 -1
- package/src/frontend/call-plan.ts +197 -45
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/device-pins.ts +18 -8
- package/src/frontend/frame-objects.ts +504 -61
- package/src/frontend/frontend.ts +20 -0
- package/src/frontend/jump-table.ts +35 -20
- package/src/frontend/mips.ts +226 -8
- package/src/frontend/ppc.ts +26 -2
- package/src/frontend/register-struct-call.ts +1 -86
- package/src/frontend/ssa.ts +263 -1
- package/src/frontend/stackargs.ts +95 -17
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +410 -61
- package/src/ir/alias.ts +29 -8
- package/src/ir/bits.ts +1081 -66
- package/src/ir/core.ts +154 -4
- package/src/ir/entry.ts +3 -5
- package/src/ir/opcodes.ts +28 -0
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/verify.ts +16 -0
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +294 -2
- package/src/l3/basecse.ts +35 -10
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +34 -6
- package/src/l3/dce.ts +52 -8
- package/src/l3/ptrcell.ts +2 -0
- package/src/l3/regspell.ts +2 -0
- package/src/l3/reindex.ts +13 -4
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/tailmerge.ts +2 -0
- package/src/pipeline.ts +507 -23
- package/src/proto-context.ts +135 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +1 -1
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/offsetnames.ts +412 -10
- package/src/raise/pre-recovery.ts +8 -5
- package/src/raise/pre-structure.ts +24 -5
- package/src/raise/reroll.ts +4 -24
- package/src/raise/shortcircuit.ts +192 -23
- package/src/raise/unpre.ts +594 -0
- package/src/rank-variations.ts +97 -4
- package/src/rank.ts +258 -43
- package/src/structure/analysis.ts +56 -10
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +46 -1
- package/src/structure/structure.ts +4221 -368
- package/src/structure/switch-recover.ts +345 -26
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +52 -15
- package/src/target.ts +269 -8
- package/src/trace.ts +28 -14
- package/src/variation-definitions.ts +207 -6
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +63 -2
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
* measured against; IDO, gcc 2.7.2 and mwcc reload past every store of another type (strict
|
|
18
18
|
* aliasing does not reorder there), so the cast and union refusals below are only more cautious on
|
|
19
19
|
* MIPS and PPC. */
|
|
20
|
-
import { linearFactor } from '../ir/bits';
|
|
20
|
+
import { type StridedRange, linearFactor } from '../ir/bits';
|
|
21
21
|
import { type Block, type Op, type Value, byValueOperands, calleeName, mkOp, mkValue, successorsOf } from '../ir/core';
|
|
22
22
|
import { type IrType, T, structSpelling, typeEquals, typeToString, unionViewSigns } from '../ir/types';
|
|
23
|
-
import type
|
|
23
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
24
24
|
import { blockCopyRoles } from '../proto';
|
|
25
25
|
import type { SymbolMap } from '../symbols';
|
|
26
26
|
import { type TargetDescription, blockTransferRead, sourceControlRead, sourceReach } from '../target';
|
|
@@ -53,6 +53,11 @@ export interface FrameObjectAudit {
|
|
|
53
53
|
escapesReliedOn?: ReadonlySet<number>;
|
|
54
54
|
/** every capture the add arm re-minted at a constant offset from it */
|
|
55
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>;
|
|
56
61
|
/** whether a callee's declaration rules out a struct returned through a hidden pointer at
|
|
57
62
|
* argument 0 (aggregate.ts `returnsWithoutHiddenPointer`), asked of the frontend's call
|
|
58
63
|
* declarations */
|
|
@@ -62,6 +67,17 @@ export interface FrameObjectAudit {
|
|
|
62
67
|
/** the retraction rules an escape is judged by; `FRAME_ESCAPE_GATES` when absent, and a
|
|
63
68
|
* census or an ablation hands in its own (`FRAME_OBJECT_AUDIT`) */
|
|
64
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>;
|
|
65
81
|
/** the bytes a first audit asked to have kept as ONE object in memory (`FrameObjectRelift`):
|
|
66
82
|
* the frontend routes every access inside them through an `laddr`, and the audit declares
|
|
67
83
|
* them one object rather than judging each offset on its own */
|
|
@@ -100,31 +116,83 @@ export function areaAboveOutgoing({
|
|
|
100
116
|
export const landsOnArea = (area: FrameRange, at: number, width: number): boolean =>
|
|
101
117
|
at < area.to && at + width > area.from;
|
|
102
118
|
|
|
103
|
-
/**
|
|
104
|
-
* it,
|
|
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. */
|
|
105
140
|
export interface FrameObjectRelift {
|
|
106
|
-
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 };
|
|
107
146
|
}
|
|
108
147
|
|
|
109
148
|
/** THE RELIFT PROTOCOL, AT MOST TWICE, the one copy every frontend that runs the audit calls: lift,
|
|
110
|
-
* and where the audit answers with
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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. */
|
|
113
155
|
export function liftWithRelift<F>(
|
|
114
156
|
name: string,
|
|
115
|
-
liftOnce: (oneObject: FrameRange | undefined) => F | FrameObjectRelift,
|
|
157
|
+
liftOnce: (oneObject: FrameRange | undefined, homesReleased: boolean) => F | FrameObjectRelift,
|
|
116
158
|
): F {
|
|
117
|
-
const first = liftOnce(undefined);
|
|
159
|
+
const first = liftOnce(undefined, false);
|
|
118
160
|
if (!isRelift(first)) {
|
|
119
161
|
return first;
|
|
120
162
|
}
|
|
121
|
-
const
|
|
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
|
+
}
|
|
122
173
|
if (isRelift(again)) {
|
|
123
174
|
throw new Error(`internal: the frame-object audit of '${name}' asked for a second relift`);
|
|
124
175
|
}
|
|
125
176
|
return again;
|
|
126
177
|
}
|
|
127
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
|
+
|
|
128
196
|
const isRelift = <F>(x: F | FrameObjectRelift): x is FrameObjectRelift =>
|
|
129
197
|
typeof x === 'object' && x !== null && 'oneObject' in x;
|
|
130
198
|
|
|
@@ -348,7 +416,30 @@ export const FRAME_ESCAPE_GATES: readonly Gate<FrameEscape>[] = [
|
|
|
348
416
|
* the fold, the split, the one-object answer and the stamp; `foldedHere`, which the split adds to
|
|
349
417
|
* and the slot-overlap refusal reads; and `ModelChoice.refused`, which the model choice and the
|
|
350
418
|
* shape stage set and the relift test reads. */
|
|
351
|
-
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({
|
|
352
443
|
name,
|
|
353
444
|
irBlocks,
|
|
354
445
|
ownedLocals: owned,
|
|
@@ -358,10 +449,12 @@ export function auditFrameObjects({
|
|
|
358
449
|
capturedObjectIsTheWholeFrame,
|
|
359
450
|
escapesReliedOn = new Set(),
|
|
360
451
|
movedCaptures,
|
|
452
|
+
directAccesses = new Set(),
|
|
361
453
|
returnsWithoutHiddenPointer,
|
|
362
454
|
symbols,
|
|
363
455
|
target,
|
|
364
456
|
gates = FRAME_ESCAPE_GATES,
|
|
457
|
+
reliftGates = RELIFT_HAND_BACK_GATES,
|
|
365
458
|
oneObject,
|
|
366
459
|
}: FrameObjectAudit): FrameObjectVerdict {
|
|
367
460
|
const { laddrs: captures, foldedHere } = foldMovedCaptures(irBlocks, movedCaptures);
|
|
@@ -437,7 +530,17 @@ export function auditFrameObjects({
|
|
|
437
530
|
model.unboundedAt.has(e.off) &&
|
|
438
531
|
(e.objectReached !== undefined || e.slotReached !== undefined || e.unaccountedWord !== undefined);
|
|
439
532
|
if (model.onOffer && (model.refused || shapes.overSlot.length > 0 || escapes.some(answered))) {
|
|
440
|
-
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
|
+
};
|
|
441
544
|
}
|
|
442
545
|
const failWithheld: Refuse = (why) => fail(model.withheld(why));
|
|
443
546
|
for (const [off, width] of shapes.overSlot) {
|
|
@@ -455,6 +558,147 @@ export function auditFrameObjects({
|
|
|
455
558
|
return { policy: 'per-object', sinks };
|
|
456
559
|
}
|
|
457
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
|
+
|
|
458
702
|
/** Refuses the whole function, naming the address-taken local as what it cannot lift. */
|
|
459
703
|
type Refuse = (why: string) => never;
|
|
460
704
|
|
|
@@ -981,10 +1225,70 @@ interface FrameAccess {
|
|
|
981
1225
|
}
|
|
982
1226
|
|
|
983
1227
|
/** A load or store judged an access to the object at `off`, and the address value it reaches the
|
|
984
|
-
* 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`. */
|
|
985
1231
|
interface FrameAccessOp {
|
|
986
1232
|
readonly off: number;
|
|
987
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;
|
|
988
1292
|
}
|
|
989
1293
|
|
|
990
1294
|
/** Every use of every frame object, by offset — what each later stage reads. Each field is
|
|
@@ -1006,7 +1310,9 @@ interface FrameUses {
|
|
|
1006
1310
|
/** the calls each object is passed to BY VALUE (`foldByValueArgs`), with the struct each passes */
|
|
1007
1311
|
readonly byValueReads: ReadonlyMap<number, readonly { readonly op: Op; readonly type: IrType }[]>;
|
|
1008
1312
|
readonly useCount: ReadonlyMap<number, number>;
|
|
1009
|
-
|
|
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[]>;
|
|
1010
1316
|
readonly members: readonly { readonly at: number; readonly width: number }[];
|
|
1011
1317
|
readonly memberRefusal: string | undefined;
|
|
1012
1318
|
/** every escape of each object's address, as the frame bytes it may reach: `[lo, hi)` where a
|
|
@@ -1127,7 +1433,7 @@ function classifyFrameUses({
|
|
|
1127
1433
|
// The accesses through a runtime-indexed address, per object — kept apart from `accesses`,
|
|
1128
1434
|
// which types the object at its own offset: `a[i]` says what one ELEMENT is, not what sits at
|
|
1129
1435
|
// `a`.
|
|
1130
|
-
const indexed = new Map<number,
|
|
1436
|
+
const indexed = new Map<number, IndexedAccess[]>();
|
|
1131
1437
|
for (const off of objects.keys()) {
|
|
1132
1438
|
accesses.set(off, []);
|
|
1133
1439
|
}
|
|
@@ -1256,7 +1562,14 @@ function classifyFrameUses({
|
|
|
1256
1562
|
// judged with the rest (`objectShapes`).
|
|
1257
1563
|
const other = op.opcode === 'add' && op.operands.length === 2 ? op.operands[1 - idx] : undefined;
|
|
1258
1564
|
if (other !== undefined && taint.get(other) === undefined && defOf.get(other)?.opcode !== 'const') {
|
|
1259
|
-
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
|
+
);
|
|
1260
1573
|
continue;
|
|
1261
1574
|
}
|
|
1262
1575
|
// A CONSTANT move left after the fold (`foldMovedCaptures`) moves a capture a phi carried.
|
|
@@ -1371,19 +1684,30 @@ function transferRead(
|
|
|
1371
1684
|
// compiles to `add r4, r4, sp` and the source's `mov r1, sp / add r0, r1, r4` does not: the `u16`
|
|
1372
1685
|
// twin `((u16 *)sp0)[a0]` misses the same way. Left raw, the access compiles to the source's
|
|
1373
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`).
|
|
1374
1697
|
function indexedAccess(
|
|
1375
1698
|
irBlocks: readonly Block[],
|
|
1376
|
-
{
|
|
1377
|
-
indexed,
|
|
1378
|
-
accessOps,
|
|
1379
|
-
}: { indexed: Map<number, { width: number; signed: boolean }[]>; accessOps: Map<Op, FrameAccessOp> },
|
|
1699
|
+
{ indexed, accessOps }: { indexed: Map<number, IndexedAccess[]>; accessOps: Map<Op, FrameAccessOp> },
|
|
1380
1700
|
object: FrameAccessOp,
|
|
1381
|
-
sum: Value,
|
|
1701
|
+
{ sum, range }: { sum: Value; range: StridedRange | undefined },
|
|
1382
1702
|
constOf: (v: Value) => number | undefined,
|
|
1383
1703
|
fail: Refuse,
|
|
1384
1704
|
): void {
|
|
1385
1705
|
const { off } = object;
|
|
1386
1706
|
const got = indexed.get(off) ?? indexed.set(off, []).get(off)!;
|
|
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
|
+
};
|
|
1387
1711
|
const judge = (at: Value, by: number): void => {
|
|
1388
1712
|
for (const blk of irBlocks) {
|
|
1389
1713
|
for (const op of blk.ops) {
|
|
@@ -1414,8 +1738,10 @@ function indexedAccess(
|
|
|
1414
1738
|
'only a load or store at the indexed address is modelled',
|
|
1415
1739
|
);
|
|
1416
1740
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
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 });
|
|
1419
1745
|
}
|
|
1420
1746
|
}
|
|
1421
1747
|
}
|
|
@@ -1570,7 +1896,7 @@ function readWindow(
|
|
|
1570
1896
|
const base = facts.literalAddrOf(op.operands[0]);
|
|
1571
1897
|
if (base === undefined) {
|
|
1572
1898
|
if (!flow.frameOnEveryPath.has(op.operands[0])) {
|
|
1573
|
-
if (!misses(op, facts, cnt)) {
|
|
1899
|
+
if (!misses(op, facts, cnt, blk)) {
|
|
1574
1900
|
return unbounded('a later store through an unresolved pointer may re-arm');
|
|
1575
1901
|
}
|
|
1576
1902
|
spared = true;
|
|
@@ -1617,6 +1943,16 @@ function readWindow(
|
|
|
1617
1943
|
return { lo, hi, why: 'that reads through it', ...(spared ? { sparedByMiss: true as const } : {}) };
|
|
1618
1944
|
}
|
|
1619
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
|
+
|
|
1620
1956
|
/** A store through a computed pointer that provably misses the control halfword at `cnt`: no
|
|
1621
1957
|
* address its base may hold (`addressRangeOf`, a strided interval), displaced by the store's own
|
|
1622
1958
|
* offset, starts a store whose bytes overlap `[cnt, cnt + 2)`. agbcc's `gEnts[i].f = 1` after a
|
|
@@ -1627,18 +1963,17 @@ function readWindow(
|
|
|
1627
1963
|
* over": an immediate transfer is, but a later store that sets the enable bit again reloads the
|
|
1628
1964
|
* source register and reads the frame anew (GBATEK, DMA control bit 15), which is what this
|
|
1629
1965
|
* guards against. A store this cannot bound on every value stays an unresolved pointer. */
|
|
1630
|
-
function misses(op: Op, facts: FrameAddressFacts, cnt: number): boolean {
|
|
1631
|
-
const r = facts.addressRangeOf(op.operands[0]);
|
|
1966
|
+
function misses(op: Op, facts: FrameAddressFacts, cnt: number, blk: Block): boolean {
|
|
1967
|
+
const r = facts.addressRangeOf(op.operands[0], blk);
|
|
1632
1968
|
if (r === undefined) {
|
|
1633
1969
|
return false;
|
|
1634
1970
|
}
|
|
1635
|
-
const off = op.attrs.off as number;
|
|
1636
1971
|
const width = op.attrs.width as number;
|
|
1637
|
-
const
|
|
1638
|
-
|
|
1639
|
-
if (lo < 0 || hi + width > 2 ** 32) {
|
|
1972
|
+
const starts = displaced(r, op.attrs.off as number, width);
|
|
1973
|
+
if (starts === null) {
|
|
1640
1974
|
return false;
|
|
1641
1975
|
}
|
|
1976
|
+
const { lo, hi } = starts;
|
|
1642
1977
|
// the first start at or above the lowest one that overlaps, against the highest that does
|
|
1643
1978
|
const from = cnt - width + 1;
|
|
1644
1979
|
const first = from <= lo || r.stride === 0 ? lo : lo + Math.ceil((from - lo) / r.stride) * r.stride;
|
|
@@ -2166,7 +2501,7 @@ function failIfSlotKeysIt(
|
|
|
2166
2501
|
* holds (`writerNotKept` says which writers it can hold) — or one per part, where the accesses
|
|
2167
2502
|
* prove the bytes are several objects no use reads across (`provenParts`) — and every access
|
|
2168
2503
|
* inside it is a cast-spelled access to that array (an element of a part, by its index:
|
|
2169
|
-
* structure.ts `
|
|
2504
|
+
* structure.ts `framePartDecls`), or a view of the union it is declared as where two widths
|
|
2170
2505
|
* meet at one byte — so each store the machine made there is a store the
|
|
2171
2506
|
* recompile makes too. That is what agbcc does for an object whose address escaped: it keeps
|
|
2172
2507
|
* every store to it, in order (flow.c deletes a memory store only when an identical later
|
|
@@ -2210,13 +2545,21 @@ function keepAsOneObject({
|
|
|
2210
2545
|
fail(`${kept} cannot hold every writer — ${writer}`);
|
|
2211
2546
|
}
|
|
2212
2547
|
// A runtime index names no byte, so the rule below is asked of it at EVERY byte it may reach:
|
|
2213
|
-
// all of them
|
|
2214
|
-
//
|
|
2215
|
-
//
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
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
|
+
});
|
|
2220
2563
|
// ONE TYPE PER BYTE, OR ONE UNION. agbcc at -O2 turns on type-based alias analysis
|
|
2221
2564
|
// (toplev.c:3616), and compiled, a `u32` read through a cast is served the earlier `u32` store
|
|
2222
2565
|
// straight past a `u16` store to the same bytes — a stale value. So a byte two accesses of
|
|
@@ -2252,8 +2595,9 @@ function keepAsOneObject({
|
|
|
2252
2595
|
}
|
|
2253
2596
|
}
|
|
2254
2597
|
if (twoWidths !== undefined) {
|
|
2255
|
-
// any runtime index, a byte one too: `indexedWidth` leaves width
|
|
2256
|
-
// agrees with every width, but a union view is
|
|
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)
|
|
2257
2601
|
const why = unionRefusal({ from, to, uses, indexed: uses.indexed.size > 0 });
|
|
2258
2602
|
if (why !== null) {
|
|
2259
2603
|
fail(
|
|
@@ -2263,9 +2607,10 @@ function keepAsOneObject({
|
|
|
2263
2607
|
}
|
|
2264
2608
|
}
|
|
2265
2609
|
for (const [b, had] of widthAt) {
|
|
2266
|
-
|
|
2610
|
+
const by = indexes.find((x) => x.width !== 1 && x.width !== had && mayTouch(x.reach, b, 1));
|
|
2611
|
+
if (by !== undefined) {
|
|
2267
2612
|
fail(
|
|
2268
|
-
`the byte at [sp,#${b}] of ${kept} is accessed ${had} bytes wide and by a runtime index ${
|
|
2613
|
+
`the byte at [sp,#${b}] of ${kept} is accessed ${had} bytes wide and by a runtime index ${by.width} ` +
|
|
2269
2614
|
`bytes wide, ${STALE_ACROSS_TYPES}`,
|
|
2270
2615
|
);
|
|
2271
2616
|
}
|
|
@@ -2296,7 +2641,8 @@ function keepAsOneObject({
|
|
|
2296
2641
|
: [{ from, to }];
|
|
2297
2642
|
// …stamped `part` where there are several: storage a copy proved one object of the source's, so
|
|
2298
2643
|
// its bytes are that object's elements rather than the one object's mix of members and spills,
|
|
2299
|
-
// and the emitter may name them as elements (structure.ts `
|
|
2644
|
+
// and the emitter may name them as elements (structure.ts `framePartDecls`), of the type their
|
|
2645
|
+
// accesses agree on (`stampPartElements`, below)
|
|
2300
2646
|
const laddrs = parts.map(({ from: lo, to: hi }) =>
|
|
2301
2647
|
mkOp('laddr', {
|
|
2302
2648
|
results: [mkValue(T.unk(32))],
|
|
@@ -2369,6 +2715,9 @@ function keepAsOneObject({
|
|
|
2369
2715
|
}
|
|
2370
2716
|
object.attrs = { ...object.attrs, views: unionViews(irBlocks, base) };
|
|
2371
2717
|
}
|
|
2718
|
+
if (laddrs.length > 1) {
|
|
2719
|
+
stampPartElements(irBlocks, laddrs, uses.accessOps);
|
|
2720
|
+
}
|
|
2372
2721
|
irBlocks[0].ops.unshift(...(arrayOrder === 'descending' ? [...laddrs].reverse() : laddrs));
|
|
2373
2722
|
}
|
|
2374
2723
|
|
|
@@ -2382,10 +2731,11 @@ function keepAsOneObject({
|
|
|
2382
2731
|
* folded into its own local (l3/constinit.ts).
|
|
2383
2732
|
*
|
|
2384
2733
|
* ONE PART, never a refusal, whenever a byte could be read through two parts: a runtime index
|
|
2385
|
-
* (it names no byte, so it may reach every one)
|
|
2386
|
-
*
|
|
2387
|
-
*
|
|
2388
|
-
*
|
|
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
|
|
2389
2739
|
* union views of two widths at one byte are not split either (the caller asks only without
|
|
2390
2740
|
* them). An access straddling two runs chains them into one part by construction. */
|
|
2391
2741
|
function provenParts(
|
|
@@ -2395,27 +2745,31 @@ function provenParts(
|
|
|
2395
2745
|
alignment: number,
|
|
2396
2746
|
): FrameRange[] {
|
|
2397
2747
|
const whole = [{ from, to }];
|
|
2398
|
-
if (uses.indexed.size > 0) {
|
|
2399
|
-
return whole;
|
|
2400
|
-
}
|
|
2401
2748
|
const touched: { owner: number; lo: number; hi: number }[] = [];
|
|
2402
|
-
for (const [op, { off }] of uses.accessOps) {
|
|
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
|
+
}
|
|
2403
2754
|
const at = off + (op.attrs.off as number);
|
|
2404
2755
|
touched.push({ owner: off, lo: at, hi: at + (op.attrs.width as number) });
|
|
2405
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
|
+
}
|
|
2406
2768
|
for (const [off, reads] of uses.byValueReads) {
|
|
2407
2769
|
reads.forEach(({ type }) =>
|
|
2408
2770
|
touched.push({ owner: off, lo: off, hi: off + (type.kind === 'struct' ? (type.size ?? Infinity) : Infinity) }),
|
|
2409
2771
|
);
|
|
2410
2772
|
}
|
|
2411
|
-
for (const [off, extents] of uses.escapeExtents) {
|
|
2412
|
-
for (const e of extents) {
|
|
2413
|
-
if (e === null) {
|
|
2414
|
-
return whole;
|
|
2415
|
-
}
|
|
2416
|
-
touched.push({ owner: off, ...e });
|
|
2417
|
-
}
|
|
2418
|
-
}
|
|
2419
2773
|
// …and a boundary only where a bounded copy starts: an address handed to `memcpy` is where
|
|
2420
2774
|
// the source put an object it fills. A run starting anywhere else is bytes reached from below,
|
|
2421
2775
|
// which says nothing of where one object ends — the one-object relift reads every slot through
|
|
@@ -2426,7 +2780,7 @@ function provenParts(
|
|
|
2426
2780
|
// reads `buf[6]` through a base it holds where the target re-formed sp+6. Only a copy's
|
|
2427
2781
|
// DESTINATION counts: a copy reading OUT of the frame says where the source took bytes from,
|
|
2428
2782
|
// which is as likely `memcpy(g, &w[2], 8)` from the middle of one array as a second object.
|
|
2429
|
-
const starts =
|
|
2783
|
+
const starts = copyStarts(reaches);
|
|
2430
2784
|
const parts: { from: number; to: number }[] = [];
|
|
2431
2785
|
for (const { lo, hi } of [...touched].sort((a, b) => a.lo - b.lo)) {
|
|
2432
2786
|
const last = parts.at(-1);
|
|
@@ -2525,6 +2879,95 @@ function unionViews(irBlocks: readonly Block[], base: Value): number[] {
|
|
|
2525
2879
|
.flatMap(([w, accesses]) => unionViewSigns(w, accesses).map((sg) => (sg && w !== 4 ? -w : w)));
|
|
2526
2880
|
}
|
|
2527
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
|
+
});
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2528
2971
|
/** One object's shape: `count` elements of `width` bytes. */
|
|
2529
2972
|
interface ObjectShape {
|
|
2530
2973
|
readonly width: number;
|