@asmlift/core 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +165 -50
- package/src/backend/cpp.ts +2 -1
- package/src/backend/pascal.ts +31 -12
- package/src/contracts.ts +173 -16
- package/src/declare.ts +69 -20
- package/src/frontend/asmdata.ts +7 -1
- package/src/frontend/call-plan.ts +964 -60
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +23 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1232 -192
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2076 -215
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2711 -232
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1460 -44
- package/src/frontend/stackargs.ts +106 -57
- package/src/frontend/thumb.ts +2044 -318
- package/src/ir/alias.ts +42 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +100 -5
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +159 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +109 -9
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +80 -11
- package/src/l3/ast.ts +290 -18
- package/src/l3/basecse.ts +33 -5
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +2 -2
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +31 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +9 -4
- package/src/l3/reindex.ts +283 -96
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +4 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +541 -24
- package/src/proto-context.ts +905 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +10 -26
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +102 -19
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +694 -0
- package/src/raise/shortcircuit.ts +26 -12
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +89 -31
- package/src/rank.ts +307 -54
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +165 -2
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +2180 -1054
- package/src/structure/switch-recover.ts +244 -100
- package/src/symbols.ts +6 -0
- package/src/target.ts +765 -52
- package/src/trace.ts +26 -8
- package/src/variation-definitions.ts +126 -14
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +5 -1
|
@@ -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
|
-
|
|
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 { 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';
|
|
16
23
|
import type { Gate } 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,7 +39,18 @@ 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>;
|
|
37
56
|
/** whether a callee's declaration rules out a struct returned through a hidden pointer at
|
|
@@ -54,12 +73,61 @@ export interface FrameRange {
|
|
|
54
73
|
readonly to: number;
|
|
55
74
|
}
|
|
56
75
|
|
|
76
|
+
/** THE LOCAL AREA ABOVE A FRAME'S OUTGOING AREA, `[from, to)` in post-push stack-pointer offsets:
|
|
77
|
+
* from where the outgoing words end (`outgoingEnd`, rounded up to `align`) to the lowest register
|
|
78
|
+
* save at or above that, or the frame's top. The rule O32's `above-outgoing-area` compilers
|
|
79
|
+
* (frontend/mips.ts `o32LocalArea`) and mwcc (frontend/ppc.ts, `compilerBehaviors.linkageLocalArea`)
|
|
80
|
+
* both keep; each frontend measures the numbers it is applied to. Null where nothing is left. */
|
|
81
|
+
export function areaAboveOutgoing({
|
|
82
|
+
frameSize,
|
|
83
|
+
outgoingEnd,
|
|
84
|
+
saves,
|
|
85
|
+
align,
|
|
86
|
+
}: {
|
|
87
|
+
frameSize: number;
|
|
88
|
+
outgoingEnd: number;
|
|
89
|
+
saves: readonly number[];
|
|
90
|
+
align: number;
|
|
91
|
+
}): FrameRange | null {
|
|
92
|
+
const from = Math.ceil(outgoingEnd / align) * align;
|
|
93
|
+
const area = { from, to: Math.min(frameSize, ...saves.filter((o) => o >= from)) };
|
|
94
|
+
return area.from < area.to ? area : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Whether a `width`-byte frame access at post-push offset `at` lands on a byte of `area`. A
|
|
98
|
+
* frontend that routes accesses into the area by their post-push offset asks it of every access
|
|
99
|
+
* made at ANOTHER depth, which that routing would misname, and draws no area where one does. */
|
|
100
|
+
export const landsOnArea = (area: FrameRange, at: number, width: number): boolean =>
|
|
101
|
+
at < area.to && at + width > area.from;
|
|
102
|
+
|
|
57
103
|
/** What an audit answers instead of a refusal when a device may read the frame, or a callee write
|
|
58
104
|
* it, without bound: lift again with `oneObject` set to these bytes. */
|
|
59
105
|
export interface FrameObjectRelift {
|
|
60
106
|
readonly oneObject: FrameRange;
|
|
61
107
|
}
|
|
62
108
|
|
|
109
|
+
/** THE RELIFT PROTOCOL, AT MOST TWICE, the one copy every frontend that runs the audit calls: lift,
|
|
110
|
+
* and where the audit answers with bytes to keep as one object (`FrameObjectRelift`), lift once
|
|
111
|
+
* more with them routed through an `laddr`. The second audit judges them as one object and refuses
|
|
112
|
+
* rather than asking again, so a second request is an internal error, not a third lift. */
|
|
113
|
+
export function liftWithRelift<F>(
|
|
114
|
+
name: string,
|
|
115
|
+
liftOnce: (oneObject: FrameRange | undefined) => F | FrameObjectRelift,
|
|
116
|
+
): F {
|
|
117
|
+
const first = liftOnce(undefined);
|
|
118
|
+
if (!isRelift(first)) {
|
|
119
|
+
return first;
|
|
120
|
+
}
|
|
121
|
+
const again = liftOnce(first.oneObject);
|
|
122
|
+
if (isRelift(again)) {
|
|
123
|
+
throw new Error(`internal: the frame-object audit of '${name}' asked for a second relift`);
|
|
124
|
+
}
|
|
125
|
+
return again;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const isRelift = <F>(x: F | FrameObjectRelift): x is FrameObjectRelift =>
|
|
129
|
+
typeof x === 'object' && x !== null && 'oneObject' in x;
|
|
130
|
+
|
|
63
131
|
/** What an audit answers: lift again (`FrameObjectRelift`), the device pin the accepted function
|
|
64
132
|
* owes (`DevicePins`, applied by `pinDeviceAccesses`), or nothing, when no frame address was
|
|
65
133
|
* taken. */
|
|
@@ -247,13 +315,20 @@ export const FRAME_ESCAPE_GATES: readonly Gate<FrameEscape>[] = [
|
|
|
247
315
|
* anywhere as a VALUE — into an MMIO register (the DMA-fill idiom), a call, a phi — and it is
|
|
248
316
|
* judged in one of two models, chosen before any shape is:
|
|
249
317
|
* - 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
|
-
*
|
|
318
|
+
* width and one agreed extension, or a read or write through a runtime index into storage
|
|
319
|
+
* nothing else types — bytes, or ONE wider width, read through a cast on the bytes
|
|
320
|
+
* (`indexedWidth`) — and its bytes must belong to nothing else in the frame;
|
|
252
321
|
* - 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
|
-
*
|
|
322
|
+
* that answer can hold (`writerNotKept`), or where no address escapes at all and the
|
|
323
|
+
* per-object model refuses (`oneObject`, which this answers and the frontend lifts again
|
|
324
|
+
* with): the local area is one `u8` array in memory, every fixed-offset access in it is a
|
|
325
|
+
* member at its own offset, a runtime index may reach it at a byte or at ONE wider
|
|
326
|
+
* width (`indexedWidth`), and two widths at one byte are the views of one union
|
|
327
|
+
* (`unionViews`), or refuse where no view names them (`unionRefusal`).
|
|
328
|
+
* In either model, a store through a capture that reaches a call unread on every path, beneath
|
|
329
|
+
* every word a load proves a local's, declines: it may be that call's stack argument
|
|
330
|
+
* (`storedNeverRead`). Which load proves it is the compiler's premise: one after a call under
|
|
331
|
+
* `localsAboveOutgoingArea`, and any load from the offset up under `outgoingAreaNeverLoadedFrom`.
|
|
257
332
|
* Any use the audit cannot vouch for declines the whole function loudly. Nothing here guesses: a
|
|
258
333
|
* scalar's declared type is exactly the access type the machine used, and an object NO access
|
|
259
334
|
* reaches is sized by the frame reservation and left untyped.
|
|
@@ -279,7 +354,9 @@ export function auditFrameObjects({
|
|
|
279
354
|
ownedLocals: owned,
|
|
280
355
|
declaredLocals: declared,
|
|
281
356
|
usedSlotOffsets,
|
|
357
|
+
loadedWords,
|
|
282
358
|
capturedObjectIsTheWholeFrame,
|
|
359
|
+
escapesReliedOn = new Set(),
|
|
283
360
|
movedCaptures,
|
|
284
361
|
returnsWithoutHiddenPointer,
|
|
285
362
|
symbols,
|
|
@@ -290,24 +367,48 @@ export function auditFrameObjects({
|
|
|
290
367
|
const { laddrs: captures, foldedHere } = foldMovedCaptures(irBlocks, movedCaptures);
|
|
291
368
|
// …and the audit runs for a licensed acceptance with no object at all, so the premise re-check
|
|
292
369
|
// is total rather than resting on "the capture always survives into the IR".
|
|
293
|
-
if (captures.length === 0 && !capturedObjectIsTheWholeFrame) {
|
|
370
|
+
if (captures.length === 0 && !capturedObjectIsTheWholeFrame && escapesReliedOn.size === 0) {
|
|
294
371
|
return undefined;
|
|
295
372
|
}
|
|
296
373
|
const fail = frameRefusal(name);
|
|
374
|
+
if (captures.length === 0 && escapesReliedOn.size > 0) {
|
|
375
|
+
recheckEscapesReliedOn(escapesReliedOn, undefined, fail);
|
|
376
|
+
}
|
|
297
377
|
const facts = frameAddressFacts(irBlocks, symbols, target);
|
|
298
|
-
const { laddrs, splitRefusal } = splitAddressingCopies(irBlocks, captures, foldedHere);
|
|
378
|
+
const { laddrs, splitRefusal } = splitAddressingCopies(irBlocks, captures, foldedHere, declaredStructs(irBlocks));
|
|
299
379
|
const objects = frameObjects(laddrs, owned, fail);
|
|
300
380
|
const flow = addressFlow(irBlocks, objects, facts, fail);
|
|
301
|
-
const uses = classifyFrameUses({
|
|
381
|
+
const uses = classifyFrameUses({
|
|
382
|
+
irBlocks,
|
|
383
|
+
objects,
|
|
384
|
+
flow,
|
|
385
|
+
facts,
|
|
386
|
+
splitRefusal,
|
|
387
|
+
oneObject,
|
|
388
|
+
target,
|
|
389
|
+
fail,
|
|
390
|
+
});
|
|
302
391
|
const windows = escapeWindows(irBlocks, uses, flow, facts, target);
|
|
303
|
-
const model = chooseFrameModel(oneObject, uses, windows, returnsWithoutHiddenPointer, fail);
|
|
392
|
+
const model = chooseFrameModel({ oneObject, uses, windows, returnsWithoutHiddenPointer, fail });
|
|
304
393
|
if (capturedObjectIsTheWholeFrame) {
|
|
305
394
|
recheckWholeFramePremise({ uses, returnsWithoutHiddenPointer, fail });
|
|
306
395
|
}
|
|
396
|
+
recheckEscapesReliedOn(escapesReliedOn, uses, fail);
|
|
397
|
+
storedNeverRead(irBlocks, uses, loadedWords, target, fail);
|
|
307
398
|
// The device pin's trigger: the `readOnlyAddressSinks` registers a frame address was stored to.
|
|
308
399
|
const sinks = [...new Set([...uses.sourceStores.values()].flat().map((s) => s.sink))];
|
|
309
400
|
if (oneObject !== undefined) {
|
|
310
|
-
keepAsOneObject({
|
|
401
|
+
keepAsOneObject({
|
|
402
|
+
oneObject,
|
|
403
|
+
irBlocks,
|
|
404
|
+
objects,
|
|
405
|
+
uses,
|
|
406
|
+
usedSlotOffsets,
|
|
407
|
+
returnsWithoutHiddenPointer,
|
|
408
|
+
arrayOrder: target.compilerBehaviors.addressTakenArrayOrder,
|
|
409
|
+
arrayAlignment: target.compilerBehaviors.addressTakenArrayAlignment,
|
|
410
|
+
fail,
|
|
411
|
+
});
|
|
311
412
|
return { policy: 'one-object', sinks };
|
|
312
413
|
}
|
|
313
414
|
const shapes = objectShapes({
|
|
@@ -321,17 +422,19 @@ export function auditFrameObjects({
|
|
|
321
422
|
fail,
|
|
322
423
|
});
|
|
323
424
|
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
|
-
//
|
|
425
|
+
// THE ONE-OBJECT ANSWER, where it is on offer (`chooseFrameModel`: one escape reaches without
|
|
426
|
+
// bound and every writer is one it can hold, or no address escapes at all) and the per-object
|
|
427
|
+
// model does not describe the frame: it refused a shape, an object sits over a slot, or the
|
|
428
|
+
// unbounded reach meets another object, a slot or a word nothing accounts for. Then what the
|
|
429
|
+
// device may read or the callee may write — or, with no escape, only this function's own
|
|
430
|
+
// accesses — is kept rather than refused: lift again with the local area as one object in memory
|
|
329
431
|
// (`keepAsOneObject`). Below the local area are the outgoing arguments and above it the saved
|
|
330
432
|
// registers, neither of them an object. Where the per-object model does describe the frame —
|
|
331
433
|
// one object and nothing else in reach — it stands, and the object keeps its own type.
|
|
434
|
+
// An escape `misses` bounded is asked as the unbounded read it was without that proof
|
|
435
|
+
// (`ModelChoice.unboundedAt`): the proof may add a per-object lift, never take the one-object one.
|
|
332
436
|
const answered = (e: FrameEscape): boolean =>
|
|
333
|
-
e.
|
|
334
|
-
e.hi === Infinity &&
|
|
437
|
+
model.unboundedAt.has(e.off) &&
|
|
335
438
|
(e.objectReached !== undefined || e.slotReached !== undefined || e.unaccountedWord !== undefined);
|
|
336
439
|
if (model.onOffer && (model.refused || shapes.overSlot.length > 0 || escapes.some(answered))) {
|
|
337
440
|
return { oneObject: { from: declared.from, to: declared.to } };
|
|
@@ -361,6 +464,42 @@ function frameRefusal(name: string): Refuse {
|
|
|
361
464
|
};
|
|
362
465
|
}
|
|
363
466
|
|
|
467
|
+
/** THE FRAME BYTES A DECLARATION TYPES AS ONE STRUCT: a call's return storage (`sret`) and a
|
|
468
|
+
* struct passed by value (`byValue`, call-plan.ts `foldByValueArgs`), where the operand is an
|
|
469
|
+
* `laddr`. */
|
|
470
|
+
function declaredStructs(irBlocks: readonly Block[]): FrameRange[] {
|
|
471
|
+
const laddrOff = new Map<Value, number>();
|
|
472
|
+
for (const b of irBlocks) {
|
|
473
|
+
for (const op of b.ops) {
|
|
474
|
+
if (op.opcode === 'laddr') {
|
|
475
|
+
laddrOff.set(op.results[0], op.attrs.off as number);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const out: FrameRange[] = [];
|
|
480
|
+
for (const b of irBlocks) {
|
|
481
|
+
for (const op of b.ops) {
|
|
482
|
+
if (op.opcode !== 'call') {
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const t = op.results[0]?.type;
|
|
486
|
+
const from = laddrOff.get(op.operands[0]);
|
|
487
|
+
if (op.attrs.sret === true && from !== undefined && t?.kind === 'struct' && t.size !== undefined) {
|
|
488
|
+
out.push({ from, to: from + t.size });
|
|
489
|
+
}
|
|
490
|
+
for (const { at } of byValueOperands(op)) {
|
|
491
|
+
const v = op.operands[at];
|
|
492
|
+
const off = laddrOff.get(v);
|
|
493
|
+
const t = v?.type.kind === 'ptr' ? v.type.to : undefined;
|
|
494
|
+
if (off !== undefined && t?.kind === 'struct' && t.size !== undefined) {
|
|
495
|
+
out.push({ from: off, to: off + t.size });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return out;
|
|
501
|
+
}
|
|
502
|
+
|
|
364
503
|
/** A CAPTURE MOVED BY A CONSTANT IS THE CAPTURE AT THE SUM, and here the constant is exact: an
|
|
365
504
|
* `add` of a `laddr` and a `const` — a register the lift could not see through, `mov r0, sp /
|
|
366
505
|
* movs r2, #0 / ldrsh r1, [r0, r2]`, or a move the pre-lift walk does not follow — is re-minted
|
|
@@ -369,6 +508,22 @@ function frameRefusal(name: string): Refuse {
|
|
|
369
508
|
* lands on a keyed slot is refused as the move, the capability that is missing
|
|
370
509
|
* (`failIfSlotKeysIt`).
|
|
371
510
|
*
|
|
511
|
+
* AN INDEX INTO A CAPTURE, MOVED BY A NONZERO CONSTANT, IS THE SAME INDEX INTO THE CAPTURE AT THE
|
|
512
|
+
* SUM. agbcc spells `a[i]` on a local at [sp,#k) as the frame base plus the index, moved after:
|
|
513
|
+
* `mov r0, sp / add r0, r0, r4 / add r0, #0x4` or `add r0, r0, sp / add r0, #0x4` — the bytes
|
|
514
|
+
* `add r0, sp, #0x4 / add r0, r0, r4` reach, so `add(add(laddr@a, i), k)` is re-minted as
|
|
515
|
+
* `add(laddr@(a+k), i)`, and the inner sum and capture are dropped when nothing else reads them.
|
|
516
|
+
* The object at [sp,#a) is often the outgoing-argument word (a 5-argument call beside
|
|
517
|
+
* the array), which the index never touches. Zero is left alone: `add(sum, 0)` is agbcc's
|
|
518
|
+
* `mov r1, #0x0 / ldrsb r0, [r0, r1]`, which `indexedAccess` judges where the register put it.
|
|
519
|
+
* The rebased object is held to every rule an object at a+k is held to, so a displaced FIELD of a
|
|
520
|
+
* struct element — no object of its own at a+k, the struct's other bytes at a — lands as a second
|
|
521
|
+
* object beside the first and is refused or lifted as one object by the same rules. A load or
|
|
522
|
+
* store's own nonzero displacement on the sum is the same move: `ldrb r0, [r0, #0x1]` (the field
|
|
523
|
+
* agbcc reads unsigned) and `mov r1, #0x1 / ldrsb r0, [r0, r1]` (signed) reach the same byte, so
|
|
524
|
+
* the access is re-pointed at `add(laddr@(a+d), i)` with displacement 0, and the verdict does not
|
|
525
|
+
* depend on where the literal sits.
|
|
526
|
+
*
|
|
372
527
|
* Returns every `laddr` that survives, in block order. */
|
|
373
528
|
function foldMovedCaptures(
|
|
374
529
|
irBlocks: Block[],
|
|
@@ -387,23 +542,77 @@ function foldMovedCaptures(
|
|
|
387
542
|
}
|
|
388
543
|
}
|
|
389
544
|
}
|
|
545
|
+
// the `add` of a capture and a runtime value that defines each value, for the index rebase
|
|
546
|
+
const indexOf = new Map<Value, { base: Op; index: Value; sum: Op }>();
|
|
547
|
+
const rebased = new Set<Op>();
|
|
390
548
|
for (let changed = true; changed;) {
|
|
391
549
|
changed = false;
|
|
392
550
|
for (const blk of irBlocks) {
|
|
393
|
-
blk.ops = blk.ops.
|
|
551
|
+
blk.ops = blk.ops.flatMap((op) => {
|
|
394
552
|
const [x, y] = op.operands;
|
|
553
|
+
const sum0 = (op.opcode === 'load' || op.opcode === 'store') && x !== undefined ? indexOf.get(x) : undefined;
|
|
554
|
+
const disp = (op.attrs.off as number | undefined) ?? 0;
|
|
555
|
+
if (sum0 !== undefined && disp !== 0) {
|
|
556
|
+
const object = mkOp('laddr', {
|
|
557
|
+
results: [mkValue(sum0.base.results[0].type)],
|
|
558
|
+
attrs: { off: ((sum0.base.attrs.off as number) + disp) | 0 },
|
|
559
|
+
});
|
|
560
|
+
const moves = mkOp('add', {
|
|
561
|
+
results: [mkValue(x.type)],
|
|
562
|
+
operands: [object.results[0], sum0.index],
|
|
563
|
+
attrs: sum0.sum.attrs,
|
|
564
|
+
});
|
|
565
|
+
laddrOf.set(object.results[0], object);
|
|
566
|
+
indexOf.set(moves.results[0], { base: object, index: sum0.index, sum: moves });
|
|
567
|
+
moved.add(sum0.base.results[0]);
|
|
568
|
+
foldedHere.add(object);
|
|
569
|
+
rebased.add(sum0.sum);
|
|
570
|
+
op.operands[0] = moves.results[0];
|
|
571
|
+
op.attrs = { ...op.attrs, off: 0 };
|
|
572
|
+
changed = true;
|
|
573
|
+
return [object, moves, op];
|
|
574
|
+
}
|
|
575
|
+
if (op.opcode !== 'add' || op.operands.length !== 2) {
|
|
576
|
+
return [op];
|
|
577
|
+
}
|
|
395
578
|
const base = laddrOf.get(x) ?? laddrOf.get(y);
|
|
396
579
|
const by = constOf.get(laddrOf.has(x) ? y : x);
|
|
397
|
-
if (
|
|
398
|
-
|
|
580
|
+
if (base !== undefined && by !== undefined) {
|
|
581
|
+
const off = ((base.attrs.off as number) + by) | 0;
|
|
582
|
+
const object = mkOp('laddr', { results: op.results, attrs: { off } });
|
|
583
|
+
laddrOf.set(op.results[0], object);
|
|
584
|
+
moved.add(base.results[0]);
|
|
585
|
+
foldedHere.add(object);
|
|
586
|
+
changed = true;
|
|
587
|
+
return [object];
|
|
588
|
+
}
|
|
589
|
+
const index = base === undefined ? undefined : laddrOf.has(x) ? y : x;
|
|
590
|
+
if (base !== undefined && index !== undefined && !laddrOf.has(index)) {
|
|
591
|
+
// a sum first seen here takes another pass: a use laid out in an EARLIER block was
|
|
592
|
+
// visited before the sum was known, and is moved only on the next one
|
|
593
|
+
if (!indexOf.has(op.results[0])) {
|
|
594
|
+
indexOf.set(op.results[0], { base, index, sum: op });
|
|
595
|
+
changed = true;
|
|
596
|
+
}
|
|
597
|
+
return [op];
|
|
598
|
+
}
|
|
599
|
+
const sum = indexOf.get(x) ?? indexOf.get(y);
|
|
600
|
+
const k = constOf.get(indexOf.has(x) ? y : x);
|
|
601
|
+
if (sum === undefined || k === undefined || k === 0) {
|
|
602
|
+
return [op];
|
|
399
603
|
}
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
604
|
+
const object = mkOp('laddr', {
|
|
605
|
+
results: [mkValue(sum.base.results[0].type)],
|
|
606
|
+
attrs: { off: ((sum.base.attrs.off as number) + k) | 0 },
|
|
607
|
+
});
|
|
608
|
+
const moves = mkOp('add', { results: op.results, operands: [object.results[0], sum.index], attrs: op.attrs });
|
|
609
|
+
laddrOf.set(object.results[0], object);
|
|
610
|
+
indexOf.set(op.results[0], { base: object, index: sum.index, sum: moves });
|
|
611
|
+
moved.add(sum.base.results[0]);
|
|
404
612
|
foldedHere.add(object);
|
|
613
|
+
rebased.add(sum.sum);
|
|
405
614
|
changed = true;
|
|
406
|
-
return object;
|
|
615
|
+
return [object, moves];
|
|
407
616
|
});
|
|
408
617
|
}
|
|
409
618
|
}
|
|
@@ -412,13 +621,24 @@ function foldMovedCaptures(
|
|
|
412
621
|
// the write-back of `stmia r1!, {r2, r3, r4}` through a capture is `add r1, r1, #12`, a pointer
|
|
413
622
|
// to the end of what was copied that the machine never uses. Only those: an unused `mov rD, sp`
|
|
414
623
|
// of the machine's own is still a capture, and is judged.
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
for (const
|
|
418
|
-
|
|
419
|
-
|
|
624
|
+
const readNow = (): Set<Value> => {
|
|
625
|
+
const read = new Set<Value>();
|
|
626
|
+
for (const blk of irBlocks) {
|
|
627
|
+
for (const op of blk.ops) {
|
|
628
|
+
op.operands.forEach((v) => read.add(v));
|
|
629
|
+
(op.successors ?? []).forEach((s) => s.args.forEach((v) => read.add(v)));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return read;
|
|
633
|
+
};
|
|
634
|
+
// …and an index sum a rebase moved off, once nothing reads it, goes the same way first
|
|
635
|
+
if (rebased.size > 0) {
|
|
636
|
+
const before = readNow();
|
|
637
|
+
for (const blk of irBlocks) {
|
|
638
|
+
blk.ops = blk.ops.filter((op) => !rebased.has(op) || before.has(op.results[0]));
|
|
420
639
|
}
|
|
421
640
|
}
|
|
641
|
+
const read = readNow();
|
|
422
642
|
const laddrs: Op[] = [];
|
|
423
643
|
for (const blk of irBlocks) {
|
|
424
644
|
blk.ops = blk.ops.filter(
|
|
@@ -474,7 +694,7 @@ function frameAddressFacts(
|
|
|
474
694
|
}
|
|
475
695
|
for (const [lit, index] of [d.operands, [...d.operands].reverse()]) {
|
|
476
696
|
const from = literalAddrOf(lit);
|
|
477
|
-
const stride = from === undefined ? 0 :
|
|
697
|
+
const stride = from === undefined ? 0 : linearFactor({ defs: defOf, constOf: addresses.constOf }, index);
|
|
478
698
|
if (from !== undefined && readOnlySinks.has(from + off) && stride > 0 && stride % sinkSpacing === 0) {
|
|
479
699
|
return [...readOnlySinks].filter((s) => (s - (from + off)) % stride === 0);
|
|
480
700
|
}
|
|
@@ -484,47 +704,6 @@ function frameAddressFacts(
|
|
|
484
704
|
return { ...addresses, readsThrough };
|
|
485
705
|
}
|
|
486
706
|
|
|
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
707
|
/** A FRAME BASE ADDRESSED THROUGH IS NOT A CAPTURE. Thumb-1 gives `ldr`/`str` an `[sp,#imm]`
|
|
529
708
|
* encoding and gives the sub-word forms none, so a byte or halfword spill can only be spelled
|
|
530
709
|
* by copying sp into a register and addressing through the copy:
|
|
@@ -556,6 +735,7 @@ function splitAddressingCopies(
|
|
|
556
735
|
irBlocks: Block[],
|
|
557
736
|
laddrs: Op[],
|
|
558
737
|
foldedHere: Set<Op>,
|
|
738
|
+
structs: readonly FrameRange[] = [],
|
|
559
739
|
): { laddrs: Op[]; splitRefusal: Map<Value, string> } {
|
|
560
740
|
const splitRefusal = new Map<Value, string>();
|
|
561
741
|
const uses = new Map<Value, { op: Op; idx: number; blk: Block }[]>();
|
|
@@ -593,13 +773,42 @@ function splitAddressingCopies(
|
|
|
593
773
|
if (at.length === 0 || accesses.length !== at.length) {
|
|
594
774
|
continue;
|
|
595
775
|
}
|
|
776
|
+
// AN ADDRESS STRICTLY INSIDE A DECLARED STRUCT, ONLY ACCESSED, IS A MEMBER ACCESS OF THAT
|
|
777
|
+
// STRUCT. A word store into a struct passed by value goes straight to `[sp,#k]` — `x.c = w` is
|
|
778
|
+
// `str r1, [sp, #0x4]` over `struct F {u16 a,b; u32 c; s8 e,f;}` at [sp,#0) — and the frontend
|
|
779
|
+
// mints that access its own `laddr` at k. The declaration says those bytes are the struct's,
|
|
780
|
+
// so the address is re-rooted on the struct's base with the displacement on the access, and
|
|
781
|
+
// the audit judges one object rather than two that overlap. Any use that is not an access —
|
|
782
|
+
// `&x.c` handed on — is an interior pointer, which is not this shape and is left to refuse.
|
|
783
|
+
const off = capture.attrs.off as number;
|
|
784
|
+
const holders = structs.filter((r) => r.from < off && off < r.to);
|
|
785
|
+
const holder = holders[0];
|
|
786
|
+
const inHolder = (u: { op: Op }): boolean =>
|
|
787
|
+
off + (u.op.attrs.off as number) >= holder!.from &&
|
|
788
|
+
off + (u.op.attrs.off as number) + (u.op.attrs.width as number) <= holder!.to;
|
|
789
|
+
if (
|
|
790
|
+
holder !== undefined &&
|
|
791
|
+
holders.every((r) => r.from === holder.from && r.to === holder.to) &&
|
|
792
|
+
accesses.every(inHolder)
|
|
793
|
+
) {
|
|
794
|
+
for (const u of accesses) {
|
|
795
|
+
u.op.attrs = { ...u.op.attrs, off: off - holder.from + (u.op.attrs.off as number) };
|
|
796
|
+
}
|
|
797
|
+
capture.attrs = { ...capture.attrs, off: holder.from };
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
596
800
|
if (!subWord) {
|
|
597
801
|
splitRefusal.set(capture.results[0], 'a WORD access through the copy, and `ldr`/`str` have an `[sp,#imm]` form');
|
|
598
802
|
continue;
|
|
599
803
|
}
|
|
600
804
|
// 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
|
-
|
|
805
|
+
// the frame base itself, which is what the DMA-fill idiom captures — or every access lands in
|
|
806
|
+
// the declared struct the capture is the start of (`declaredStructs`), whose members they are.
|
|
807
|
+
const base = capture.attrs.off as number;
|
|
808
|
+
const into = (r: FrameRange): boolean =>
|
|
809
|
+
base === r.from &&
|
|
810
|
+
accesses.every((u) => base + (u.op.attrs.off as number) + (u.op.attrs.width as number) <= r.to);
|
|
811
|
+
if (accesses.every((u) => u.op.attrs.off === 0) || structs.some(into)) {
|
|
603
812
|
continue;
|
|
604
813
|
}
|
|
605
814
|
for (const u of accesses) {
|
|
@@ -644,8 +853,16 @@ function frameObjects(laddrs: readonly Op[], owned: FrameRange, fail: Refuse): M
|
|
|
644
853
|
// saved register's — `g(&e)` for a fifth parameter `e` is `add r0, sp, #0x8` over a one-word
|
|
645
854
|
// local area — and no other rule of the audit is about that gap. The TOP itself is also C's
|
|
646
855
|
// one-past-the-end pointer, `g(buf, buf + 8)` over an 8-byte array, and the refusal names both
|
|
647
|
-
// readings.
|
|
856
|
+
// readings. BELOW IT is the outgoing block a call's stack words are staged in, and on O32 also
|
|
857
|
+
// the saves IDO keeps under its locals: an address there is no local's either. Every object's
|
|
858
|
+
// extent is held to the area again below; this says why, for the frontend that supplied it.
|
|
648
859
|
for (const off of objects.keys()) {
|
|
860
|
+
if (off < owned.from) {
|
|
861
|
+
fail(
|
|
862
|
+
`the captured address at [sp,#${off}) is below the reserved local area, which starts at [sp,#${owned.from}) — ` +
|
|
863
|
+
'an outgoing stack argument word or a saved register, whose address is not modelled',
|
|
864
|
+
);
|
|
865
|
+
}
|
|
649
866
|
if (off === owned.to) {
|
|
650
867
|
fail(
|
|
651
868
|
`the captured address at [sp,#${off}) is the top of the reserved local area of ${owned.to - owned.from} bytes — ` +
|
|
@@ -786,10 +1003,28 @@ interface FrameUses {
|
|
|
786
1003
|
readonly publishedToWriter: ReadonlySet<number>;
|
|
787
1004
|
readonly arg0Callees: ReadonlyMap<number, ReadonlySet<string | null>>;
|
|
788
1005
|
readonly returnTemps: ReadonlyMap<number, readonly Op[]>;
|
|
1006
|
+
/** the calls each object is passed to BY VALUE (`foldByValueArgs`), with the struct each passes */
|
|
1007
|
+
readonly byValueReads: ReadonlyMap<number, readonly { readonly op: Op; readonly type: IrType }[]>;
|
|
789
1008
|
readonly useCount: ReadonlyMap<number, number>;
|
|
790
1009
|
readonly indexed: ReadonlyMap<number, readonly { readonly width: number; readonly signed: boolean }[]>;
|
|
791
1010
|
readonly members: readonly { readonly at: number; readonly width: number }[];
|
|
792
1011
|
readonly memberRefusal: string | undefined;
|
|
1012
|
+
/** every escape of each object's address, as the frame bytes it may reach: `[lo, hi)` where a
|
|
1013
|
+
* literal-count block copy proves them (`boundedCopy`), `null` where nothing does, and `into`
|
|
1014
|
+
* where the copy WRITES them (the address is its destination, not its source). Only the split
|
|
1015
|
+
* of the one object reads this (`provenParts`); the escape windows keep the copy as an
|
|
1016
|
+
* unbounded writer, which is what puts that one object on offer at all (`chooseFrameModel`). */
|
|
1017
|
+
readonly escapeExtents: ReadonlyMap<
|
|
1018
|
+
number,
|
|
1019
|
+
readonly ({ readonly lo: number; readonly hi: number; readonly into: boolean } | null)[]
|
|
1020
|
+
>;
|
|
1021
|
+
readonly fixed: readonly {
|
|
1022
|
+
readonly op: Op;
|
|
1023
|
+
readonly at: number;
|
|
1024
|
+
readonly width: number;
|
|
1025
|
+
readonly isLoad: boolean;
|
|
1026
|
+
readonly frameOnly: boolean;
|
|
1027
|
+
}[];
|
|
793
1028
|
}
|
|
794
1029
|
|
|
795
1030
|
/** Did the address at `off` reach something that may write the frame back: a callee that is not a
|
|
@@ -885,6 +1120,9 @@ function classifyFrameUses({
|
|
|
885
1120
|
// is the callee's return storage, a local of the declared type. `useCount` counts every use of
|
|
886
1121
|
// each object, so a return temp this function does anything else with is told apart.
|
|
887
1122
|
const returnTemps = new Map<number, Op[]>();
|
|
1123
|
+
// …and a call's operand its lowering passes BY VALUE (`foldByValueArgs`) is no escape: the
|
|
1124
|
+
// callee receives a copy of the whole struct, so it is a read of the object's bytes at the call
|
|
1125
|
+
const byValueReads = new Map<number, { op: Op; type: IrType }[]>();
|
|
888
1126
|
const useCount = new Map<number, number>();
|
|
889
1127
|
// The accesses through a runtime-indexed address, per object — kept apart from `accesses`,
|
|
890
1128
|
// which types the object at its own offset: `a[i]` says what one ELEMENT is, not what sits at
|
|
@@ -897,17 +1135,37 @@ function classifyFrameUses({
|
|
|
897
1135
|
// access at [+k] through a capture is a member of the one object rather than a second object
|
|
898
1136
|
const members: { at: number; width: number }[] = [];
|
|
899
1137
|
// …and without it, the first such access, which the per-object model refuses once the model is
|
|
900
|
-
// chosen (`chooseFrameModel`)
|
|
901
|
-
|
|
1138
|
+
// chosen (`chooseFrameModel`) — save one of a call's return storage or of a struct passed by
|
|
1139
|
+
// value, whose members are the struct's declared ones (`objectShapes`)
|
|
1140
|
+
const memberRefusals: [number, string][] = [];
|
|
1141
|
+
const escapeExtents = new Map<number, ({ lo: number; hi: number; into: boolean } | null)[]>();
|
|
1142
|
+
// …and every value something reads, since a copy whose returned destination is read hands the
|
|
1143
|
+
// address on to a use nothing here follows
|
|
1144
|
+
const readValues = new Set<Value>(
|
|
1145
|
+
irBlocks.flatMap((b) => b.ops.flatMap((op) => [...op.operands, ...(op.successors ?? []).flatMap((sx) => sx.args)])),
|
|
1146
|
+
);
|
|
1147
|
+
// …and in either model, every fixed-offset access as the op that makes it, the frame bytes it
|
|
1148
|
+
// touches, whether it reads them, and whether its address is this frame's on every path — what
|
|
1149
|
+
// `storedNeverRead` asks of a frame that makes a call
|
|
1150
|
+
const fixed: { op: Op; at: number; width: number; isLoad: boolean; frameOnly: boolean }[] = [];
|
|
902
1151
|
const scalar = (kind: string, op: Op, off: number, v: Value): void => {
|
|
1152
|
+
fixed.push({
|
|
1153
|
+
op,
|
|
1154
|
+
at: off + (op.attrs.off as number),
|
|
1155
|
+
width: op.attrs.width as number,
|
|
1156
|
+
isLoad: kind === 'load',
|
|
1157
|
+
frameOnly: frameOnEveryPath.has(v),
|
|
1158
|
+
});
|
|
903
1159
|
if (oneObject !== undefined) {
|
|
904
1160
|
members.push({ at: off + (op.attrs.off as number), width: op.attrs.width as number });
|
|
905
1161
|
return;
|
|
906
1162
|
}
|
|
907
1163
|
if ((op.attrs.off as number) !== 0) {
|
|
908
|
-
|
|
1164
|
+
memberRefusals.push([
|
|
1165
|
+
off,
|
|
909
1166
|
`a ${kind} at [+${op.attrs.off}] through the captured address — ` +
|
|
910
|
-
|
|
1167
|
+
(splitRefusal.get(v) ?? 'only a scalar at the captured address is modelled'),
|
|
1168
|
+
]);
|
|
911
1169
|
}
|
|
912
1170
|
};
|
|
913
1171
|
for (const blk of irBlocks) {
|
|
@@ -934,8 +1192,22 @@ function classifyFrameUses({
|
|
|
934
1192
|
accesses.get(off)!.push({ width: op.attrs.width as number, signed: false, isLoad: false });
|
|
935
1193
|
continue;
|
|
936
1194
|
}
|
|
1195
|
+
if (byValueOperands(op).some((b) => b.at === idx)) {
|
|
1196
|
+
// the fold types the operand a pointer to the struct passed (call-plan.ts `foldByValueArgs`)
|
|
1197
|
+
const type = v.type.kind === 'ptr' ? v.type.to : undefined;
|
|
1198
|
+
if (type?.kind !== 'struct' || type.size === undefined) {
|
|
1199
|
+
throw new Error(`internal: a by-value operand of a call to '${calleeName(op)}' has no declared struct`);
|
|
1200
|
+
}
|
|
1201
|
+
(byValueReads.get(off) ?? byValueReads.set(off, []).get(off)!).push({ op, type });
|
|
1202
|
+
fixed.push({ op, at: off, width: type.size, isLoad: true, frameOnly: frameOnEveryPath.has(v) });
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
937
1205
|
if ((op.opcode === 'store' && idx === 1) || op.opcode === 'call') {
|
|
938
1206
|
escaped.add(off); // the address ESCAPES as a value — the point of the capability
|
|
1207
|
+
const copied = op.opcode === 'call' ? boundedCopy(op, idx, facts, readValues) : undefined;
|
|
1208
|
+
(escapeExtents.get(off) ?? escapeExtents.set(off, []).get(off)!).push(
|
|
1209
|
+
copied === undefined ? null : { lo: off + copied.lo, hi: off + copied.hi, into: copied.into },
|
|
1210
|
+
);
|
|
939
1211
|
const handedTo = op.opcode === 'store' ? facts.readsThrough(op) : [];
|
|
940
1212
|
const read = op.opcode === 'call' ? transferRead(op, idx, off, target, facts) : undefined;
|
|
941
1213
|
if (read !== undefined) {
|
|
@@ -984,7 +1256,7 @@ function classifyFrameUses({
|
|
|
984
1256
|
// judged with the rest (`objectShapes`).
|
|
985
1257
|
const other = op.opcode === 'add' && op.operands.length === 2 ? op.operands[1 - idx] : undefined;
|
|
986
1258
|
if (other !== undefined && taint.get(other) === undefined && defOf.get(other)?.opcode !== 'const') {
|
|
987
|
-
indexedAccess(irBlocks, { indexed, accessOps }, { off, through: v }, op.results[0], fail);
|
|
1259
|
+
indexedAccess(irBlocks, { indexed, accessOps }, { off, through: v }, op.results[0], facts.constOf, fail);
|
|
988
1260
|
continue;
|
|
989
1261
|
}
|
|
990
1262
|
// A CONSTANT move left after the fold (`foldMovedCaptures`) moves a capture a phi carried.
|
|
@@ -1022,13 +1294,39 @@ function classifyFrameUses({
|
|
|
1022
1294
|
publishedToWriter,
|
|
1023
1295
|
arg0Callees,
|
|
1024
1296
|
returnTemps,
|
|
1297
|
+
byValueReads,
|
|
1025
1298
|
useCount,
|
|
1026
1299
|
indexed,
|
|
1027
1300
|
members,
|
|
1028
|
-
memberRefusal,
|
|
1301
|
+
memberRefusal: memberRefusals.find(([off]) => !returnTemps.has(off) && !byValueReads.has(off))?.[1],
|
|
1302
|
+
escapeExtents,
|
|
1303
|
+
fixed,
|
|
1029
1304
|
};
|
|
1030
1305
|
}
|
|
1031
1306
|
|
|
1307
|
+
// The bytes, relative to the address at operand `idx`, that a call of a standard block copy with
|
|
1308
|
+
// a literal, positive count reaches through it, and whether it writes them (`idx` is its
|
|
1309
|
+
// destination) — or `undefined` for any other callee, operand or count, which this does not
|
|
1310
|
+
// bound, and for a copy whose result (`memcpy` returns its destination) anything in `read` uses.
|
|
1311
|
+
function boundedCopy(
|
|
1312
|
+
op: Op,
|
|
1313
|
+
idx: number,
|
|
1314
|
+
facts: LiteralAddresses,
|
|
1315
|
+
read: ReadonlySet<Value>,
|
|
1316
|
+
): { lo: number; hi: number; into: boolean } | undefined {
|
|
1317
|
+
const roles = blockCopyRoles(op.attrs.target);
|
|
1318
|
+
if (
|
|
1319
|
+
roles === undefined ||
|
|
1320
|
+
(idx !== roles.dst && idx !== roles.src) ||
|
|
1321
|
+
op.operands.length <= roles.n ||
|
|
1322
|
+
op.results.some((r) => read.has(r))
|
|
1323
|
+
) {
|
|
1324
|
+
return undefined;
|
|
1325
|
+
}
|
|
1326
|
+
const n = facts.constOf(op.operands[roles.n]);
|
|
1327
|
+
return n !== undefined && Number.isInteger(n) && n > 0 ? { lo: 0, hi: n, into: idx === roles.dst } : undefined;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1032
1330
|
// The bytes a block transfer that took the address at `idx` reads, when `idx` is its source and
|
|
1033
1331
|
// its control word is a literal.
|
|
1034
1332
|
function transferRead(
|
|
@@ -1051,6 +1349,28 @@ function transferRead(
|
|
|
1051
1349
|
}
|
|
1052
1350
|
|
|
1053
1351
|
// Records every access through `sum`, a runtime index into the object at `off`.
|
|
1352
|
+
//
|
|
1353
|
+
// A SUM MOVED BY A LITERAL IS THE SAME INDEX, DISPLACED. Thumb has no `ldrsb`/`ldrsh` with an
|
|
1354
|
+
// immediate, so agbcc loads the whole displacement into a register (thumb.md:411-505, `mov %0,%2 ;
|
|
1355
|
+
// ldrs[bh] %0,[%1,%0]`) and the frontend lowers that register-offset load as `add` + load@0:
|
|
1356
|
+
// `add r0, r1, r4 / mov r1, #0x0 / ldrsb r1, [r0, r1]` is `a[i]` on an `s8 a[n]` local. So the
|
|
1357
|
+
// uses of an `add` of the sum and a `const` are judged at that displacement. One `add` deep: agbcc
|
|
1358
|
+
// emits one `mov` per load, and no other compiler emits a literal index register. Only
|
|
1359
|
+
// displacement 0 is the indexed element. A nonzero `const` on a sum whose base is a capture never
|
|
1360
|
+
// arrives here: `foldMovedCaptures` has already moved the index to the object at the sum, which is
|
|
1361
|
+
// what agbcc's `mov r0, sp / add r0, r0, r4 / add r0, #0x4` is (an array above the outgoing word),
|
|
1362
|
+
// and what `mov r1, #0x3 / ldrsb r1, [r0, r1]` and a load's own displacement, `ldrb r0, [r0,
|
|
1363
|
+
// #0x1]` (a field of an element struct, or `a[i + 1]`), reach. What is left nonzero — a constant
|
|
1364
|
+
// computed rather than loaded as a `const`, a literal or a displacement on an index into a capture
|
|
1365
|
+
// a phi carries, or a displacement on top of the zero literal, which no compiler emits — refuses
|
|
1366
|
+
// naming the displacement.
|
|
1367
|
+
//
|
|
1368
|
+
// The element is judged here and left where the register put it, so it is spelled with the
|
|
1369
|
+
// literal: `*(s16 *)((u32)sp0 + (a0 << 1) + 0)`. Re-pointing it at the sum spells
|
|
1370
|
+
// `((s16 *)sp0)[a0]`, an index through a cast on the `u8` storage (`objectShapes`), which agbcc
|
|
1371
|
+
// compiles to `add r4, r4, sp` and the source's `mov r1, sp / add r0, r1, r4` does not: the `u16`
|
|
1372
|
+
// twin `((u16 *)sp0)[a0]` misses the same way. Left raw, the access compiles to the source's
|
|
1373
|
+
// bytes with or without the `+ 0`.
|
|
1054
1374
|
function indexedAccess(
|
|
1055
1375
|
irBlocks: readonly Block[],
|
|
1056
1376
|
{
|
|
@@ -1059,28 +1379,85 @@ function indexedAccess(
|
|
|
1059
1379
|
}: { indexed: Map<number, { width: number; signed: boolean }[]>; accessOps: Map<Op, FrameAccessOp> },
|
|
1060
1380
|
object: FrameAccessOp,
|
|
1061
1381
|
sum: Value,
|
|
1382
|
+
constOf: (v: Value) => number | undefined,
|
|
1062
1383
|
fail: Refuse,
|
|
1063
1384
|
): void {
|
|
1064
1385
|
const { off } = object;
|
|
1065
1386
|
const got = indexed.get(off) ?? indexed.set(off, []).get(off)!;
|
|
1066
|
-
|
|
1067
|
-
for (const
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1387
|
+
const judge = (at: Value, by: number): void => {
|
|
1388
|
+
for (const blk of irBlocks) {
|
|
1389
|
+
for (const op of blk.ops) {
|
|
1390
|
+
const roles = [
|
|
1391
|
+
...op.operands.flatMap((v, i) => (v === at ? [i] : [])),
|
|
1392
|
+
...(op.successors ?? []).flatMap((s) => (s.args.includes(at) ? [-1] : [])),
|
|
1393
|
+
];
|
|
1394
|
+
for (const i of roles) {
|
|
1395
|
+
const k =
|
|
1396
|
+
at === sum && op.opcode === 'add' && op.operands.length === 2 && i >= 0
|
|
1397
|
+
? constOf(op.operands[1 - i])
|
|
1398
|
+
: undefined;
|
|
1399
|
+
if (k !== undefined) {
|
|
1400
|
+
judge(op.results[0], k);
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
const access = (op.opcode === 'load' || op.opcode === 'store') && i === 0;
|
|
1404
|
+
if (!access) {
|
|
1405
|
+
fail(
|
|
1406
|
+
`a runtime index into the object at [sp,#${off}) flows into \`${i === -1 ? 'a phi' : op.opcode}\` — ` +
|
|
1407
|
+
'only a load or store at the indexed address is modelled',
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
const at0 = (by + (op.attrs.off as number)) | 0;
|
|
1411
|
+
if (at0 !== 0) {
|
|
1412
|
+
fail(
|
|
1413
|
+
`a runtime index into the object at [sp,#${off}) is accessed at displacement ${at0} from the indexed element — ` +
|
|
1414
|
+
'only a load or store at the indexed address is modelled',
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
got.push({ width: op.attrs.width as number, signed: op.opcode === 'load' && op.attrs.signed === true });
|
|
1418
|
+
accessOps.set(op, object);
|
|
1078
1419
|
}
|
|
1079
|
-
got.push({ width: op.attrs.width as number, signed: op.opcode === 'load' && op.attrs.signed === true });
|
|
1080
|
-
accessOps.set(op, object);
|
|
1081
1420
|
}
|
|
1082
1421
|
}
|
|
1083
|
-
}
|
|
1422
|
+
};
|
|
1423
|
+
judge(sum, 0);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/** Why two access types on one byte refuse, as every refusal of it words it. THE RULE IS AGBCC'S
|
|
1427
|
+
* AND IS HELD ON EVERY TARGET: agbcc at -O2 serves a read of one type past a store of the other
|
|
1428
|
+
* (`indexedWidth` has the compiled cases), while IDO 7.1, KMC gcc and gcc 2.7.2 reload past every such
|
|
1429
|
+
* store, so on MIPS it is only more cautious than it has to be. The text names no compiler of the
|
|
1430
|
+
* input, since the audit is handed none. */
|
|
1431
|
+
const STALE_ACROSS_TYPES =
|
|
1432
|
+
'and type-based alias analysis can let a read of one type pass a store of the other (agbcc at -O2 does, ' +
|
|
1433
|
+
'and the audit refuses it on every target) — a cast spelling could read stale';
|
|
1434
|
+
|
|
1435
|
+
/** The one width wider than a byte that runtime-indexed accesses take, or why there is not one.
|
|
1436
|
+
*
|
|
1437
|
+
* A runtime index names no byte, so its access type must agree with every access it may reach,
|
|
1438
|
+
* and "agree" is what stops agbcc reordering two of them. At -O2 it runs type-based alias
|
|
1439
|
+
* analysis (toplev.c:3616), and `c_get_alias_set` (c-common.c:1892-2005) gives an integer type
|
|
1440
|
+
* and its unsigned twin one set, a character type set 0 (conflicting with every set), a
|
|
1441
|
+
* member access its member's type's set, and any other type — a pointer included — a new set.
|
|
1442
|
+
* Compiled at the corpus's flags, `k(sp0); ((int *)sp0)[i] = 1; ((T *)sp0)[j] = 0; return
|
|
1443
|
+
* ((int *)sp0)[i];` reloads the word for T = int, unsigned int and unsigned char, and returns the
|
|
1444
|
+
* stale 1 for T = unsigned short, void * and long.
|
|
1445
|
+
*
|
|
1446
|
+
* So ONE WIDTH IS ONE SET only while every access of that width READS AT one integer type or its
|
|
1447
|
+
* twin: a cast on the storage to `s32`/`u32` (`int`/`unsigned int` in every agbcc project), or a
|
|
1448
|
+
* member of an element struct (raise/struct-arrays) whose FIELD is such an integer — never the pointer an element
|
|
1449
|
+
* may hold. `/ptr-field`'s `over-local-storage` gate keeps such a field an integer, and contracts.ts
|
|
1450
|
+
* `assertLocalStorageStaysInteger` holds every respelled tree to it. The storage stays
|
|
1451
|
+
* `u8 name[n]` whatever the width. */
|
|
1452
|
+
function indexedWidth(accesses: readonly { readonly width: number }[]): {
|
|
1453
|
+
width: number | null;
|
|
1454
|
+
refusal: string | null;
|
|
1455
|
+
} {
|
|
1456
|
+
const widths = [...new Set(accesses.map((x) => x.width))].filter((w) => w !== 1);
|
|
1457
|
+
return {
|
|
1458
|
+
width: widths[0] ?? null,
|
|
1459
|
+
refusal: widths.length > 1 ? `accesses ${widths.join(' and ')} bytes wide, ${STALE_ACROSS_TYPES}` : null,
|
|
1460
|
+
};
|
|
1084
1461
|
}
|
|
1085
1462
|
|
|
1086
1463
|
/** The bytes an escape may reach, `[lo, hi)` from the object's own offset, and how it reads them
|
|
@@ -1089,6 +1466,10 @@ interface ReadWindow {
|
|
|
1089
1466
|
readonly lo: number;
|
|
1090
1467
|
readonly hi: number;
|
|
1091
1468
|
readonly why: string;
|
|
1469
|
+
/** bounded only because a store through a computed pointer provably misses the control
|
|
1470
|
+
* halfword (`misses`): without that proof the read is unbounded, and the one-object answer
|
|
1471
|
+
* that held it then stays on offer (`chooseFrameModel`) */
|
|
1472
|
+
readonly sparedByMiss?: true;
|
|
1092
1473
|
}
|
|
1093
1474
|
|
|
1094
1475
|
/** Each op's block and index, over the IR as the classification judged it. */
|
|
@@ -1123,8 +1504,9 @@ const unbounded = (why: string): ReadWindow => ({ lo: -Infinity, hi: Infinity, w
|
|
|
1123
1504
|
* the control stores that bound it are the ones reachable in between. Each has to be a literal
|
|
1124
1505
|
* the target decodes. A store through a pointer this cannot resolve — one that is this frame's
|
|
1125
1506
|
* on only some paths included (`frameOnEveryPath`) — may BE the control halfword, so it leaves
|
|
1126
|
-
* the read unbounded
|
|
1127
|
-
*
|
|
1507
|
+
* the read unbounded; so does a transfer never armed here at all. A store whose every address
|
|
1508
|
+
* misses that halfword (`misses`) is not such a store. An unbounded device read says which of
|
|
1509
|
+
* those it met (`why`): each is a different capability to build. A block transfer
|
|
1128
1510
|
* that took the address as its source adds the bytes its control word reads (`calleeReads`).
|
|
1129
1511
|
*
|
|
1130
1512
|
* A NAMED SYMBOL PLUS A CONSTANT IS RESOLVED WHERE THE SYMBOL MAP PLACES THE NAME, and then
|
|
@@ -1154,9 +1536,11 @@ function readWindow(
|
|
|
1154
1536
|
target: TargetDescription;
|
|
1155
1537
|
},
|
|
1156
1538
|
): ReadWindow {
|
|
1157
|
-
// a callee handed only its own return storage writes the struct it returns and nothing else
|
|
1539
|
+
// a callee handed only its own return storage writes the struct it returns and nothing else —
|
|
1540
|
+
// and this function's own reads of it hand the address to nobody
|
|
1158
1541
|
const temps = uses.returnTemps.get(off);
|
|
1159
|
-
|
|
1542
|
+
const reads = uses.accesses.get(off)?.filter((a) => a.isLoad).length ?? 0;
|
|
1543
|
+
if (temps !== undefined && uses.useCount.get(off) === temps.length + reads) {
|
|
1160
1544
|
return { lo: 0, hi: returnedSize(temps[0]), why: 'that returns its struct into it' };
|
|
1161
1545
|
}
|
|
1162
1546
|
const control = target.capabilities.readSourceControl;
|
|
@@ -1170,6 +1554,7 @@ function readWindow(
|
|
|
1170
1554
|
}
|
|
1171
1555
|
const little = target.capabilities.endianness === 'little';
|
|
1172
1556
|
const halves: number[] = [];
|
|
1557
|
+
let spared = false;
|
|
1173
1558
|
for (const { op: handed, sink } of stores) {
|
|
1174
1559
|
const cnt = sink + control.offset;
|
|
1175
1560
|
let armed = false;
|
|
@@ -1185,7 +1570,10 @@ function readWindow(
|
|
|
1185
1570
|
const base = facts.literalAddrOf(op.operands[0]);
|
|
1186
1571
|
if (base === undefined) {
|
|
1187
1572
|
if (!flow.frameOnEveryPath.has(op.operands[0])) {
|
|
1188
|
-
|
|
1573
|
+
if (!misses(op, facts, cnt)) {
|
|
1574
|
+
return unbounded('a later store through an unresolved pointer may re-arm');
|
|
1575
|
+
}
|
|
1576
|
+
spared = true;
|
|
1189
1577
|
}
|
|
1190
1578
|
continue;
|
|
1191
1579
|
}
|
|
@@ -1226,7 +1614,35 @@ function readWindow(
|
|
|
1226
1614
|
lo = Math.min(lo, reach.lo);
|
|
1227
1615
|
hi = Math.max(hi, reach.hi);
|
|
1228
1616
|
}
|
|
1229
|
-
return { lo, hi, why: 'that reads through it' };
|
|
1617
|
+
return { lo, hi, why: 'that reads through it', ...(spared ? { sparedByMiss: true as const } : {}) };
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
/** A store through a computed pointer that provably misses the control halfword at `cnt`: no
|
|
1621
|
+
* address its base may hold (`addressRangeOf`, a strided interval), displaced by the store's own
|
|
1622
|
+
* offset, starts a store whose bytes overlap `[cnt, cnt + 2)`. agbcc's `gEnts[i].f = 1` after a
|
|
1623
|
+
* transfer, `i` a `u8`, writes one byte at `gEnts + 4 + 8 * [0, 255]`, and no `i` makes that the
|
|
1624
|
+
* channel's control register — so it cannot re-arm the channel, and the window stays where the
|
|
1625
|
+
* literal stores put it. The stride decides it where the interval alone cannot: placed at
|
|
1626
|
+
* 0x04000000 that span covers 0x040000DE, and still no `i` lands on it. Not "the transfer is
|
|
1627
|
+
* over": an immediate transfer is, but a later store that sets the enable bit again reloads the
|
|
1628
|
+
* source register and reads the frame anew (GBATEK, DMA control bit 15), which is what this
|
|
1629
|
+
* 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]);
|
|
1632
|
+
if (r === undefined) {
|
|
1633
|
+
return false;
|
|
1634
|
+
}
|
|
1635
|
+
const off = op.attrs.off as number;
|
|
1636
|
+
const width = op.attrs.width as number;
|
|
1637
|
+
const lo = r.lo + off;
|
|
1638
|
+
const hi = r.hi + off;
|
|
1639
|
+
if (lo < 0 || hi + width > 2 ** 32) {
|
|
1640
|
+
return false;
|
|
1641
|
+
}
|
|
1642
|
+
// the first start at or above the lowest one that overlaps, against the highest that does
|
|
1643
|
+
const from = cnt - width + 1;
|
|
1644
|
+
const first = from <= lo || r.stride === 0 ? lo : lo + Math.ceil((from - lo) / r.stride) * r.stride;
|
|
1645
|
+
return first > Math.min(hi, cnt + 1) || first < from;
|
|
1230
1646
|
}
|
|
1231
1647
|
|
|
1232
1648
|
/** Which model the audit judges the frame in. `refused` is the one mutable fact the audit
|
|
@@ -1234,6 +1650,9 @@ function readWindow(
|
|
|
1234
1650
|
* one-object answer is on offer, and read by the relift test. */
|
|
1235
1651
|
interface ModelChoice {
|
|
1236
1652
|
readonly onOffer: boolean;
|
|
1653
|
+
/** the escapes whose read is unbounded, or was before `misses` bounded it (`sparedByMiss`) — the
|
|
1654
|
+
* one fact both the offer and the relift test (`answered`) read, so the miss proof moves neither */
|
|
1655
|
+
readonly unboundedAt: ReadonlySet<number>;
|
|
1237
1656
|
refused: boolean;
|
|
1238
1657
|
/** refuses `why` where it stands, or, with the one-object answer on offer, asks for it instead */
|
|
1239
1658
|
shapeRefused(why: string): void;
|
|
@@ -1264,27 +1683,56 @@ function writerNotKept(uses: FrameUses, returnsWithoutHiddenPointer: (callee: st
|
|
|
1264
1683
|
return null;
|
|
1265
1684
|
}
|
|
1266
1685
|
|
|
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
|
-
|
|
1686
|
+
/** THE MODEL IS CHOSEN BEFORE ANY SHAPE IS JUDGED. The one-object answer is on offer in two
|
|
1687
|
+
* frames. Where one escape reaches without bound and that answer can hold every writer
|
|
1688
|
+
* (`writerNotKept`) — two out-parameters handed to one callee as readily as a device read. And
|
|
1689
|
+
* where NO ADDRESS ESCAPES AT ALL: then there is no writer and no reader but this function's own
|
|
1690
|
+
* accesses, so keeping every word of the declared area in memory keeps every store and every load
|
|
1691
|
+
* the machine made there, and a spill kept in memory changes no value. That second frame is the
|
|
1692
|
+
* one a by-value struct parameter leaves: agbcc spills its registers word by word (`str r1, [sp]`,
|
|
1693
|
+
* the slot model's) and, Thumb having no sp-relative halfword or byte load, reads its members
|
|
1694
|
+
* through `mov rD, sp` (the object's) — `int f(struct Q { u8 a[9]; } p){ return p.a[1] + p.a[8]; }`,
|
|
1695
|
+
* or a member read signed and unsigned, or the struct copied out a word at a time. The shapes the
|
|
1696
|
+
* per-object model refuses are ones that answer can hold: a member at [+k] through a capture, two
|
|
1697
|
+
* widths or two signednesses at one address, a runtime index, overlapping objects, a second
|
|
1698
|
+
* object beside an untyped one, an object over a slot. So there each of those refusals asks for
|
|
1699
|
+
* that answer instead of declining (`shapeRefused`), and the second audit judges the bytes as one
|
|
1700
|
+
* object by its own rules — two types at one byte are a union there, or refuse. Where the answer
|
|
1701
|
+
* is not on offer, each refuses where it stands.
|
|
1702
|
+
*
|
|
1703
|
+
* A frame nothing escapes from can still hold a CALL'S STACK ARGUMENT: agbcc builds one it cannot
|
|
1704
|
+
* place with `str rX, [sp,#k]` through `mov rD, sp` — a by-value struct block-copied with
|
|
1705
|
+
* `ldmia`/`stmia`, a compound literal stored a byte or a halfword at a time — and the address
|
|
1706
|
+
* escapes nowhere. Kept here as a local, in either model, those stores outlive the call that took
|
|
1707
|
+
* them. That is `storedNeverRead`'s to refuse, the stage after this one, and it asks it of every
|
|
1708
|
+
* call, whatever a prototype says. */
|
|
1709
|
+
function chooseFrameModel({
|
|
1710
|
+
oneObject,
|
|
1711
|
+
uses,
|
|
1712
|
+
windows,
|
|
1713
|
+
returnsWithoutHiddenPointer,
|
|
1714
|
+
fail,
|
|
1715
|
+
}: {
|
|
1716
|
+
oneObject: FrameRange | undefined;
|
|
1717
|
+
uses: FrameUses;
|
|
1718
|
+
windows: ReadonlyMap<number, ReadWindow>;
|
|
1719
|
+
returnsWithoutHiddenPointer: (callee: string) => boolean;
|
|
1720
|
+
fail: Refuse;
|
|
1721
|
+
}): ModelChoice {
|
|
1722
|
+
const unboundedAt = new Set(
|
|
1723
|
+
[...windows]
|
|
1724
|
+
.filter(([, w]) => (w.lo === -Infinity && w.hi === Infinity) || w.sparedByMiss === true)
|
|
1725
|
+
.map(([off]) => off),
|
|
1726
|
+
);
|
|
1727
|
+
const unbounded = oneObject === undefined && unboundedAt.size > 0;
|
|
1284
1728
|
const writer = unbounded ? writerNotKept(uses, returnsWithoutHiddenPointer) : null;
|
|
1285
|
-
const
|
|
1729
|
+
const escapeFree = oneObject === undefined && uses.escaped.size === 0;
|
|
1730
|
+
// A struct passed by value is ONE object of its declared type, which the call names whole; the
|
|
1731
|
+
// one object is bytes, and a call handed them would pass a pointer.
|
|
1732
|
+
const onOffer = uses.byValueReads.size === 0 && ((unbounded && writer === null) || escapeFree);
|
|
1286
1733
|
const model: ModelChoice = {
|
|
1287
1734
|
onOffer,
|
|
1735
|
+
unboundedAt,
|
|
1288
1736
|
refused: false,
|
|
1289
1737
|
shapeRefused: (why) => {
|
|
1290
1738
|
if (!onOffer) {
|
|
@@ -1302,6 +1750,226 @@ function chooseFrameModel(
|
|
|
1302
1750
|
return model;
|
|
1303
1751
|
}
|
|
1304
1752
|
|
|
1753
|
+
/** A FRAME STORE THAT REACHES A CALL UNREAD, BENEATH EVERY WORD PROVEN A LOCAL, MAY BE THAT
|
|
1754
|
+
* CALL'S STACK ARGUMENT — refuse. agbcc builds an argument it cannot place with `str rX, [sp,#k]`
|
|
1755
|
+
* IN PLACE through a capture: a by-value struct is `mov rD, sp` + `stmia`, a compound literal is
|
|
1756
|
+
* `strb`/`strh` through `mov rD, sp` moved by `add rD, #k`. The stack-argument analysis
|
|
1757
|
+
* (stackargs.ts) sees `[sp,#k]` slot stores alone, so it never weighs these, and no prototype
|
|
1758
|
+
* bounds them: a callee no prototype states counts argument registers alone, and a stated list is
|
|
1759
|
+
* a LOWER bound on the words a call takes — a variadic callee's list is a prefix, and DWARF's
|
|
1760
|
+
* `symbolPrototype` drops the `...`. Whatever model then holds the bytes, a local per capture or
|
|
1761
|
+
* the one object, keeps the stores and the call loses its stack words:
|
|
1762
|
+
* `th(1, 2, 3, 4, (struct S){{x, y}})` lifts as `th(1, 2, 3, 4)`. So this asks EVERY call.
|
|
1763
|
+
*
|
|
1764
|
+
* A store is asked when it reaches a call PENDING — on some path from it to the call nothing loads
|
|
1765
|
+
* a byte it wrote, and on EVERY path from the entry some byte of it is written by such a store and
|
|
1766
|
+
* not loaded since — since a word the callee takes is one the caller wrote for it on every path
|
|
1767
|
+
* to the call and did not read back. A store through a pointer that is the
|
|
1768
|
+
* frame's on only some paths (`*(p ? p : &local) = x`) is not asked: it is not a store the
|
|
1769
|
+
* outgoing block alone can own. Nor is a store a runtime index or an escaped address reaches from
|
|
1770
|
+
* an object at or below it: whoever that hands the bytes to may read them.
|
|
1771
|
+
*
|
|
1772
|
+
* AND ONLY A STORE BENEATH THE LOADED-WORD BOUND. Under `localsAboveOutgoingArea` a compiler keeps
|
|
1773
|
+
* one outgoing area at the frame bottom, below every local, and never reads it back after a call;
|
|
1774
|
+
* so a frame byte loaded after a call before anything re-stores it held a value across that call,
|
|
1775
|
+
* its word is a local's, and so is every word above it. Under the strong form,
|
|
1776
|
+
* `outgoingAreaNeverLoadedFrom`, any load in a word at or above that offset bounds it: from 0 on
|
|
1777
|
+
* agbcc, past the a0..a3 home on O32 (whose compilers load a by-value aggregate's home words back)
|
|
1778
|
+
* and from the parameter area on mwcc (`target.ts` says why). The frontend measures that bound
|
|
1779
|
+
* once, off its instructions, and hands it here (`loadedWords`) — the same number its own
|
|
1780
|
+
* stack-argument refusals take (stackargs' `loadedWordBound` on Thumb).
|
|
1781
|
+
* Every compiled outgoing-argument shape sits beneath any such bound, and a local the source reads
|
|
1782
|
+
* after a call does not: `g(); b[0] = x; b[1] = y; return b[0];` reaches no call, `b[0] = x; g();
|
|
1783
|
+
* return b[0];` is above the bound. What this still refuses that is honest is a local the source
|
|
1784
|
+
* stores, never reads back, and calls past — a dead local and an outgoing argument are the same
|
|
1785
|
+
* machine code, and the lift cannot tell which. The after-call liveness weighs this audit's fixed
|
|
1786
|
+
* accesses alone, so it sits at or above the bound the frontend's slot loads would give, which
|
|
1787
|
+
* only asks more; the strong form adds the frontend's bound (`loadedWords`). Without either
|
|
1788
|
+
* premise the bound proves nothing, and every pending store is asked. */
|
|
1789
|
+
function storedNeverRead(
|
|
1790
|
+
irBlocks: readonly Block[],
|
|
1791
|
+
uses: FrameUses,
|
|
1792
|
+
loadedWords: FrameObjectAudit['loadedWords'],
|
|
1793
|
+
target: TargetDescription,
|
|
1794
|
+
fail: Refuse,
|
|
1795
|
+
): void {
|
|
1796
|
+
const calls = irBlocks.flatMap((b) => b.ops.filter((op) => op.opcode === 'call'));
|
|
1797
|
+
if (calls.length === 0) {
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const byOp = new Map(uses.fixed.map((a) => [a.op, a]));
|
|
1801
|
+
const bytes = (a: { at: number; width: number }): number[] => [...Array(a.width).keys()].map((i) => a.at + i);
|
|
1802
|
+
const index = new Map(irBlocks.map((b, i) => [b, i]));
|
|
1803
|
+
const succs = irBlocks.map((b) => successorsOf(b).map((s) => index.get(s)!));
|
|
1804
|
+
const live = new Set(irBlocks.length > 0 ? [0] : []);
|
|
1805
|
+
for (const stack = [...live]; stack.length;) {
|
|
1806
|
+
for (const s of succs[stack.pop()!]) {
|
|
1807
|
+
if (!live.has(s)) {
|
|
1808
|
+
live.add(s);
|
|
1809
|
+
stack.push(s);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
// THE SURVIVOR BOUND: a backward liveness over the frame bytes this function reads at fixed
|
|
1815
|
+
// offsets through an address that is the frame's on every path. A load makes its bytes live, a
|
|
1816
|
+
// store that writes them kills them, and a call is transparent but marks every byte live across
|
|
1817
|
+
// it. A load whose address may be elsewhere proves nothing and a store that may write elsewhere
|
|
1818
|
+
// kills nothing; nor does a block the entry cannot reach.
|
|
1819
|
+
let bound = Infinity;
|
|
1820
|
+
if (target.compilerBehaviors.localsAboveOutgoingArea === true) {
|
|
1821
|
+
const liveIn = irBlocks.map(() => new Set<number>());
|
|
1822
|
+
for (let changed = true; changed;) {
|
|
1823
|
+
changed = false;
|
|
1824
|
+
for (let b = irBlocks.length - 1; b >= 0; b--) {
|
|
1825
|
+
if (!live.has(b)) {
|
|
1826
|
+
continue;
|
|
1827
|
+
}
|
|
1828
|
+
const cur = new Set(succs[b].flatMap((s) => [...liveIn[s]]));
|
|
1829
|
+
for (const op of [...irBlocks[b].ops].reverse()) {
|
|
1830
|
+
const a = byOp.get(op);
|
|
1831
|
+
if (op.opcode === 'call') {
|
|
1832
|
+
for (const o of cur) {
|
|
1833
|
+
bound = Math.min(bound, o - (o % 4));
|
|
1834
|
+
}
|
|
1835
|
+
} else if (a?.frameOnly) {
|
|
1836
|
+
for (const o of bytes(a)) {
|
|
1837
|
+
if (a.isLoad) {
|
|
1838
|
+
cur.add(o);
|
|
1839
|
+
} else {
|
|
1840
|
+
cur.delete(o);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
if ([...cur].some((o) => !liveIn[b].has(o))) {
|
|
1846
|
+
liveIn[b] = cur;
|
|
1847
|
+
changed = true;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
// …and under the STRONG form (`outgoingAreaNeverLoadedFrom`) the caller loads no word of the
|
|
1853
|
+
// area from that offset up before a call either, so ANY frame byte loaded there bounds it. The
|
|
1854
|
+
// frontend measured that bound off its instructions (`loadedWords`): the slot reloads the IR
|
|
1855
|
+
// does not show, and the prologue's and epilogue's own loads — without them
|
|
1856
|
+
// `mov r3, sp; strh r0, [r3]; … bl sink; ldr r0, [sp]` refuses the `strh` on a word the code
|
|
1857
|
+
// itself proves a local's. What only this audit sees is a load through a captured address, in a
|
|
1858
|
+
// block the entry reaches, and it bounds the same way. A load in a word below the offset proves
|
|
1859
|
+
// nothing: O32 compilers load the a1..a3 home words they stage a by-value aggregate in.
|
|
1860
|
+
if (loadedWords !== undefined) {
|
|
1861
|
+
bound = Math.min(bound, loadedWords.bound);
|
|
1862
|
+
for (const b of live) {
|
|
1863
|
+
for (const op of irBlocks[b].ops) {
|
|
1864
|
+
const a = byOp.get(op);
|
|
1865
|
+
const word = a === undefined ? -1 : a.at - (a.at % 4);
|
|
1866
|
+
if (a?.isLoad && a.frameOnly && word >= loadedWords.from) {
|
|
1867
|
+
bound = Math.min(bound, word);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
const from = loadedWords?.from;
|
|
1873
|
+
|
|
1874
|
+
// THE PENDING STORES, two forward dataflows from the entry over the blocks it reaches. The MAY
|
|
1875
|
+
// set is the stores this asks that some path brings to a point unread: a load of any byte a
|
|
1876
|
+
// store wrote ends it — an argument is a word the caller writes and never reads, so a store
|
|
1877
|
+
// with any byte read is a local's — as a frame store covering every byte it wrote does. The
|
|
1878
|
+
// MUST set is the bytes every path brings to a point written by a store this asks and not
|
|
1879
|
+
// loaded since: a load ends the bytes it reads, a frame store the bytes it writes, and a store
|
|
1880
|
+
// that may write elsewhere ends nothing. An argument is staged on EVERY path to its call — a
|
|
1881
|
+
// tail-merged call is staged in each predecessor — so a byte some path reaches the call without
|
|
1882
|
+
// is not this call's to take: a loop's store that the first trip calls past is a local's.
|
|
1883
|
+
const reachedFrom = Math.min(...uses.escaped, ...uses.indexed.keys());
|
|
1884
|
+
type Fixed = FrameUses['fixed'][number];
|
|
1885
|
+
const asked = (a: Fixed): boolean => !a.isLoad && a.frameOnly && a.at + a.width <= reachedFrom && a.at < bound;
|
|
1886
|
+
interface Pending {
|
|
1887
|
+
may: Set<Fixed>;
|
|
1888
|
+
must: Set<number>;
|
|
1889
|
+
}
|
|
1890
|
+
const step = (op: Op, cur: Pending): void => {
|
|
1891
|
+
const a = byOp.get(op);
|
|
1892
|
+
if (a === undefined) {
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
const touched = new Set(bytes(a));
|
|
1896
|
+
for (const p of [...cur.may]) {
|
|
1897
|
+
const mine = bytes(p);
|
|
1898
|
+
if (a.isLoad ? mine.some((o) => touched.has(o)) : a.frameOnly && mine.every((o) => touched.has(o))) {
|
|
1899
|
+
cur.may.delete(p);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
if (a.isLoad || a.frameOnly) {
|
|
1903
|
+
for (const o of touched) {
|
|
1904
|
+
cur.must.delete(o);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
if (asked(a)) {
|
|
1908
|
+
cur.may.add(a);
|
|
1909
|
+
for (const o of touched) {
|
|
1910
|
+
cur.must.add(o);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
const into: (Pending | undefined)[] = irBlocks.map((_, b) =>
|
|
1915
|
+
b === 0 ? { may: new Set(), must: new Set() } : undefined,
|
|
1916
|
+
);
|
|
1917
|
+
for (let changed = true; changed;) {
|
|
1918
|
+
changed = false;
|
|
1919
|
+
for (let b = 0; b < irBlocks.length; b++) {
|
|
1920
|
+
const at = into[b];
|
|
1921
|
+
if (!live.has(b) || at === undefined) {
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
const cur: Pending = { may: new Set(at.may), must: new Set(at.must) };
|
|
1925
|
+
irBlocks[b].ops.forEach((op) => step(op, cur));
|
|
1926
|
+
for (const s of succs[b]) {
|
|
1927
|
+
const old = into[s];
|
|
1928
|
+
if (s === 0) {
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
if (old === undefined) {
|
|
1932
|
+
into[s] = { may: new Set(cur.may), must: new Set(cur.must) };
|
|
1933
|
+
changed = true;
|
|
1934
|
+
continue;
|
|
1935
|
+
}
|
|
1936
|
+
const may = new Set([...old.may, ...cur.may]);
|
|
1937
|
+
const must = new Set([...old.must].filter((o) => cur.must.has(o)));
|
|
1938
|
+
if (may.size !== old.may.size || must.size !== old.must.size) {
|
|
1939
|
+
into[s] = { may, must };
|
|
1940
|
+
changed = true;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
for (let b = 0; b < irBlocks.length; b++) {
|
|
1946
|
+
const at = into[b];
|
|
1947
|
+
if (!live.has(b) || at === undefined) {
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
const cur: Pending = { may: new Set(at.may), must: new Set(at.must) };
|
|
1951
|
+
for (const op of irBlocks[b].ops) {
|
|
1952
|
+
// a struct a call is passed by value is read by that call (`byValueReads`), not handed over
|
|
1953
|
+
if (op.opcode === 'call' && byOp.has(op)) {
|
|
1954
|
+
step(op, cur);
|
|
1955
|
+
}
|
|
1956
|
+
if (op.opcode === 'call') {
|
|
1957
|
+
const store = [...cur.may].filter((p) => bytes(p).some((o) => cur.must.has(o))).sort((x, y) => x.at - y.at)[0];
|
|
1958
|
+
if (store !== undefined) {
|
|
1959
|
+
const callee = calleeName(op);
|
|
1960
|
+
fail(
|
|
1961
|
+
`the ${store.width}-byte store at [sp,#${store.at}) through a captured address reaches the call to ` +
|
|
1962
|
+
`${callee === undefined ? 'an indirect callee' : `\`${callee}\``} unread on every path, and no ` +
|
|
1963
|
+
`${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 ` +
|
|
1964
|
+
"that call's stack argument, which the lift would drop",
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
step(op, cur);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1305
1973
|
/** THE ACCEPTANCE'S PREMISE, RE-ASKED OF THE IR. `capturedObjectIsTheWholeFrame` is a reading
|
|
1306
1974
|
* of the TEXT and it is the one thing in this file that switches a refusal OFF, so the two
|
|
1307
1975
|
* facts it claims are re-proven here, where they are exact, rather than left to the
|
|
@@ -1356,6 +2024,26 @@ function recheckWholeFramePremise({
|
|
|
1356
2024
|
}
|
|
1357
2025
|
}
|
|
1358
2026
|
|
|
2027
|
+
/** THE ESCAPE EVENT'S PREMISE, RE-ASKED OF THE IR — `recheckWholeFramePremise`'s first half at
|
|
2028
|
+
* any frame size. The outgoing-argument analysis lets a frame address leaving this function end
|
|
2029
|
+
* the pending stores from its offset up (`StackArgsEscape`), reading the escape off what a
|
|
2030
|
+
* REGISTER holds at a `bl` or a word `str`. Where its verdict leans on that (it would refuse
|
|
2031
|
+
* without the escapes), each offset it read must reach a call operand or a word store outside the
|
|
2032
|
+
* frame in the lifted function too. It catches an arity that shrinks after the event was read
|
|
2033
|
+
* without passing the lowering's `CallSite.leftOut` question. Same two arms, same width as the
|
|
2034
|
+
* licence they re-prove. */
|
|
2035
|
+
function recheckEscapesReliedOn(offsets: ReadonlySet<number>, uses: FrameUses | undefined, fail: Refuse): void {
|
|
2036
|
+
for (const off of offsets) {
|
|
2037
|
+
if (uses === undefined || (!uses.passedToCallee.has(off) && !uses.publishedOutward.has(off))) {
|
|
2038
|
+
fail(
|
|
2039
|
+
`the outgoing-argument analysis set aside the stores from [sp,#${off}] up on that address escaping, and in the ` +
|
|
2040
|
+
'lifted function no call takes it and no word store publishes it outside this frame — so nothing rules out ' +
|
|
2041
|
+
'an outgoing stack argument there',
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
|
|
1359
2047
|
/** NOT A STRUCT-RETURN TEMP. A one-word frame rules out agbcc's block-copy bases (each needs
|
|
1360
2048
|
* two words) but NOT the hidden return pointer of a <=4-byte non-integer-like struct, which is
|
|
1361
2049
|
* exactly one word: `struct S4 { char a,b,c,d; }; struct S4 s = mk(x);` compiles to `add
|
|
@@ -1475,8 +2163,11 @@ function failIfSlotKeysIt(
|
|
|
1475
2163
|
/** ONE OBJECT IN MEMORY, the answer a device read or a callee write nothing bounds is given
|
|
1476
2164
|
* instead of a refusal (`oneObject`, requested by the relift test where the escapes are judged).
|
|
1477
2165
|
* 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
|
-
*
|
|
2166
|
+
* holds (`writerNotKept` says which writers it can hold) — or one per part, where the accesses
|
|
2167
|
+
* prove the bytes are several objects no use reads across (`provenParts`) — and every access
|
|
2168
|
+
* inside it is a cast-spelled access to that array (an element of a part, by its index:
|
|
2169
|
+
* structure.ts `frameArrayElement`), or a view of the union it is declared as where two widths
|
|
2170
|
+
* meet at one byte — so each store the machine made there is a store the
|
|
1480
2171
|
* recompile makes too. That is what agbcc does for an object whose address escaped: it keeps
|
|
1481
2172
|
* every store to it, in order (flow.c deletes a memory store only when an identical later
|
|
1482
2173
|
* store in the same block overwrites it). And it is right whichever the source had there: a
|
|
@@ -1489,6 +2180,8 @@ function keepAsOneObject({
|
|
|
1489
2180
|
uses,
|
|
1490
2181
|
usedSlotOffsets,
|
|
1491
2182
|
returnsWithoutHiddenPointer,
|
|
2183
|
+
arrayOrder,
|
|
2184
|
+
arrayAlignment,
|
|
1492
2185
|
fail,
|
|
1493
2186
|
}: {
|
|
1494
2187
|
oneObject: FrameRange;
|
|
@@ -1497,6 +2190,8 @@ function keepAsOneObject({
|
|
|
1497
2190
|
uses: FrameUses;
|
|
1498
2191
|
usedSlotOffsets: ReadonlySet<number>;
|
|
1499
2192
|
returnsWithoutHiddenPointer: (callee: string) => boolean;
|
|
2193
|
+
arrayOrder: TargetDescription['compilerBehaviors']['addressTakenArrayOrder'];
|
|
2194
|
+
arrayAlignment: TargetDescription['compilerBehaviors']['addressTakenArrayAlignment'];
|
|
1500
2195
|
fail: Refuse;
|
|
1501
2196
|
}): void {
|
|
1502
2197
|
const kept = `the bytes [sp,#${from}) to [sp,#${to}) kept as one object`;
|
|
@@ -1514,22 +2209,33 @@ function keepAsOneObject({
|
|
|
1514
2209
|
if (writer !== null) {
|
|
1515
2210
|
fail(`${kept} cannot hold every writer — ${writer}`);
|
|
1516
2211
|
}
|
|
1517
|
-
// A runtime index names no byte, so
|
|
1518
|
-
//
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
//
|
|
1526
|
-
//
|
|
1527
|
-
//
|
|
1528
|
-
//
|
|
1529
|
-
//
|
|
1530
|
-
//
|
|
1531
|
-
//
|
|
2212
|
+
// A runtime index names no byte, so the rule below is asked of it at EVERY byte it may reach:
|
|
2213
|
+
// all of them. Two indexed widths refuse here; an index beside a member of another width
|
|
2214
|
+
// refuses after the members are checked against each other, so a member pair keeps naming its
|
|
2215
|
+
// own byte (`indexedWidth` says why one width is enough).
|
|
2216
|
+
const index = indexedWidth([...uses.indexed.values()].flat());
|
|
2217
|
+
if (index.refusal !== null) {
|
|
2218
|
+
fail(`a runtime index into ${kept} ${index.refusal}`);
|
|
2219
|
+
}
|
|
2220
|
+
// ONE TYPE PER BYTE, OR ONE UNION. agbcc at -O2 turns on type-based alias analysis
|
|
2221
|
+
// (toplev.c:3616), and compiled, a `u32` read through a cast is served the earlier `u32` store
|
|
2222
|
+
// straight past a `u16` store to the same bytes — a stale value. So a byte two accesses of
|
|
2223
|
+
// different widths reach cannot be spelled through casts. A byte access is exempt, since
|
|
2224
|
+
// character types alias everything, and so is a signedness difference, since the signed and
|
|
2225
|
+
// unsigned types of one width share an alias set (c-common.c:1962-1974). The first audit asks
|
|
2226
|
+
// for this answer rather than refusing two widths — an object read at both, or a `strh` member
|
|
2227
|
+
// through the captured address over a word the slot model had — so this is where they are judged.
|
|
2228
|
+
//
|
|
2229
|
+
// What two widths at one byte CAN be spelled as is a UNION of per-width views, every access made
|
|
2230
|
+
// directly through it (`sp0.half[1]`): c_get_alias_set gives any access through a member of a
|
|
2231
|
+
// union alias set 0 (c-common.c, the COMPONENT_REF walk), which conflicts with every set, so no
|
|
2232
|
+
// read moves past a store to the same bytes — compiled, a `u16` read, a `u32` store and the same
|
|
2233
|
+
// read again reloads through the union and returns the stale value through casts. So the object
|
|
2234
|
+
// is stamped with the views its accesses use (`unionViews`) and declared as that union
|
|
2235
|
+
// (structure.ts `laddrType`), where every access to it is a fixed offset on the object itself,
|
|
2236
|
+
// aligned to its own width — the cases `unionRefusal` names refuse as before.
|
|
1532
2237
|
const widthAt = new Map<number, number>();
|
|
2238
|
+
let twoWidths: { b: number; had: number; width: number } | undefined;
|
|
1533
2239
|
for (const m of uses.members) {
|
|
1534
2240
|
if (m.at < from || m.at + m.width > to) {
|
|
1535
2241
|
fail(`an access of ${m.width} bytes at [sp,#${m.at}) reaches past ${kept}`);
|
|
@@ -1540,14 +2246,30 @@ function keepAsOneObject({
|
|
|
1540
2246
|
for (let b = m.at; b < m.at + m.width; b++) {
|
|
1541
2247
|
const had = widthAt.get(b);
|
|
1542
2248
|
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
|
-
);
|
|
2249
|
+
twoWidths ??= { b, had, width: m.width };
|
|
1547
2250
|
}
|
|
1548
2251
|
widthAt.set(b, m.width);
|
|
1549
2252
|
}
|
|
1550
2253
|
}
|
|
2254
|
+
if (twoWidths !== undefined) {
|
|
2255
|
+
// any runtime index, a byte one too: `indexedWidth` leaves width 1 out because a char access
|
|
2256
|
+
// agrees with every width, but a union view is still spelled for fixed offsets alone
|
|
2257
|
+
const why = unionRefusal({ from, to, uses, indexed: uses.indexed.size > 0 });
|
|
2258
|
+
if (why !== null) {
|
|
2259
|
+
fail(
|
|
2260
|
+
`the byte at [sp,#${twoWidths.b}] of ${kept} is accessed ${twoWidths.had} and ${twoWidths.width} bytes ` +
|
|
2261
|
+
`wide, ${STALE_ACROSS_TYPES}, and ${why}`,
|
|
2262
|
+
);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
for (const [b, had] of widthAt) {
|
|
2266
|
+
if (index.width !== null && had !== index.width) {
|
|
2267
|
+
fail(
|
|
2268
|
+
`the byte at [sp,#${b}] of ${kept} is accessed ${had} bytes wide and by a runtime index ${index.width} ` +
|
|
2269
|
+
`bytes wide, ${STALE_ACROSS_TYPES}`,
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
1551
2273
|
// Rewritten onto one `laddr` at `from`: an access through a member at `k` becomes an access
|
|
1552
2274
|
// at `k - from` off the object, and a member address used any other way becomes the object's
|
|
1553
2275
|
// address moved by that constant.
|
|
@@ -1562,15 +2284,34 @@ function keepAsOneObject({
|
|
|
1562
2284
|
// nothing here sees, so a spin on such a byte compiles unqualified; the per-object stamp has the
|
|
1563
2285
|
// same gap (`stampObjects`: not on an ordinary `&local` argument).
|
|
1564
2286
|
const reread = uses.publishedToWriter.size > 0;
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
2287
|
+
// …and declared as SEVERAL objects where the accesses prove the bytes are several
|
|
2288
|
+
// (`provenParts`), each rewritten on its own: an access through a member at `k` lands on the
|
|
2289
|
+
// part that holds `k`, at `k` less the part's start. Only on a compiler that places address-
|
|
2290
|
+
// taken arrays in a known order against their declarations (`addressTakenArrayOrder`): the
|
|
2291
|
+
// parts are declared in that order, or the recompile swaps their addresses. And at a known
|
|
2292
|
+
// alignment (`addressTakenArrayAlignment`), which says where a part may start.
|
|
2293
|
+
const parts =
|
|
2294
|
+
twoWidths === undefined && arrayOrder !== undefined && arrayAlignment !== undefined
|
|
2295
|
+
? provenParts({ from, to }, objects, uses, arrayAlignment)
|
|
2296
|
+
: [{ from, to }];
|
|
2297
|
+
// …stamped `part` where there are several: storage a copy proved one object of the source's, so
|
|
2298
|
+
// 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 `frameArrayElement`)
|
|
2300
|
+
const laddrs = parts.map(({ from: lo, to: hi }) =>
|
|
2301
|
+
mkOp('laddr', {
|
|
2302
|
+
results: [mkValue(T.unk(32))],
|
|
2303
|
+
attrs: { off: lo, width: 1, signed: false, count: hi - lo, ...(parts.length > 1 ? { part: true } : {}) },
|
|
2304
|
+
}),
|
|
2305
|
+
);
|
|
2306
|
+
const partOf = (at: number) => parts.findIndex((p) => at >= p.from && at < p.to);
|
|
2307
|
+
const object = laddrs[0];
|
|
1569
2308
|
const base = object.results[0];
|
|
1570
2309
|
const memberAt = new Map<Value, number>();
|
|
1571
2310
|
for (const op of [...objects.values()].flat()) {
|
|
1572
2311
|
memberAt.set(op.results[0], op.attrs.off as number);
|
|
1573
2312
|
}
|
|
2313
|
+
const baseAt = (at: number) => laddrs[partOf(at)].results[0];
|
|
2314
|
+
const startAt = (at: number) => parts[partOf(at)].from;
|
|
1574
2315
|
// The qualifier goes on every access the classification judged one (`accessOps`) — through a
|
|
1575
2316
|
// member, a runtime index into one, or a phi that carries one (`p = c ? buf : gOther; while
|
|
1576
2317
|
// (*p == 0);`). Only an access through a member is rebased: a phi's incoming member is
|
|
@@ -1584,14 +2325,17 @@ function keepAsOneObject({
|
|
|
1584
2325
|
op.attrs = { ...op.attrs, volatile: true };
|
|
1585
2326
|
}
|
|
1586
2327
|
if (access && at !== undefined) {
|
|
1587
|
-
op.operands = [
|
|
1588
|
-
op.attrs = { ...op.attrs, off: (op.attrs.off as number) + at -
|
|
2328
|
+
op.operands = [baseAt(at), ...op.operands.slice(1)];
|
|
2329
|
+
op.attrs = { ...op.attrs, off: (op.attrs.off as number) + at - startAt(at) };
|
|
1589
2330
|
}
|
|
1590
2331
|
op.operands.forEach((v) => memberAt.has(v) && stillUsed.add(v));
|
|
1591
2332
|
(op.successors ?? []).forEach((sx) => sx.args.forEach((v) => memberAt.has(v) && stillUsed.add(v)));
|
|
1592
2333
|
}
|
|
1593
2334
|
}
|
|
1594
|
-
const atBase = (v: Value): Value =>
|
|
2335
|
+
const atBase = (v: Value): Value => {
|
|
2336
|
+
const at = memberAt.get(v);
|
|
2337
|
+
return at !== undefined && at === startAt(at) ? baseAt(at) : v;
|
|
2338
|
+
};
|
|
1595
2339
|
for (const blk of irBlocks) {
|
|
1596
2340
|
blk.ops = blk.ops.flatMap((op) => {
|
|
1597
2341
|
const at = op.opcode === 'laddr' ? memberAt.get(op.results[0]) : undefined;
|
|
@@ -1600,17 +2344,185 @@ function keepAsOneObject({
|
|
|
1600
2344
|
(op.successors ?? []).forEach((sx) => (sx.args = sx.args.map(atBase)));
|
|
1601
2345
|
return [op];
|
|
1602
2346
|
}
|
|
1603
|
-
if (!stillUsed.has(op.results[0]) || at ===
|
|
2347
|
+
if (!stillUsed.has(op.results[0]) || at === startAt(at)) {
|
|
1604
2348
|
return [];
|
|
1605
2349
|
}
|
|
1606
2350
|
const by = mkValue(T.unk(32));
|
|
1607
2351
|
return [
|
|
1608
|
-
mkOp('const', { results: [by], attrs: { value: at -
|
|
1609
|
-
mkOp('add', { operands: [
|
|
2352
|
+
mkOp('const', { results: [by], attrs: { value: at - startAt(at) } }),
|
|
2353
|
+
mkOp('add', { operands: [baseAt(at), by], results: op.results }),
|
|
1610
2354
|
];
|
|
1611
2355
|
});
|
|
1612
2356
|
}
|
|
1613
|
-
|
|
2357
|
+
if (twoWidths !== undefined) {
|
|
2358
|
+
// …every access is now a fixed offset on the object itself, which is what a view spells; one
|
|
2359
|
+
// that is not was carried there by a phi, as a pointer, and a pointer to the union's bytes is
|
|
2360
|
+
// read through a cast again
|
|
2361
|
+
for (const op of uses.accessOps.keys()) {
|
|
2362
|
+
if (op.operands[0] !== base) {
|
|
2363
|
+
fail(
|
|
2364
|
+
`the bytes [sp,#${from}) to [sp,#${to}) are accessed at two widths, which only a union of views ` +
|
|
2365
|
+
'spells without a stale read — and an access reaches them through a pointer a phi carries, which ' +
|
|
2366
|
+
'no view of the union names',
|
|
2367
|
+
);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
object.attrs = { ...object.attrs, views: unionViews(irBlocks, base) };
|
|
2371
|
+
}
|
|
2372
|
+
irBlocks[0].ops.unshift(...(arrayOrder === 'descending' ? [...laddrs].reverse() : laddrs));
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
/** THE PARTS THE ACCESSES PROVE `[from, to)` IS MADE OF, lowest first — or the whole range, one
|
|
2376
|
+
* part, wherever they do not. A part is a maximal run of bytes that overlapping uses chain
|
|
2377
|
+
* together — a fixed-offset access, a struct passed by value, and an escape a standard block copy
|
|
2378
|
+
* with a literal count bounds (`escapeExtents`) each name the bytes they touch — and a run that
|
|
2379
|
+
* merely abuts the one below it starts a new part only where such a copy starts — and so must
|
|
2380
|
+
* the lowest part, or nothing is split. Two brace-initialised arrays side by side are agbcc's
|
|
2381
|
+
* `memcpy(a, .LC0, 16); memcpy(b, .LC2, 16)`, and declared as one `u8[32]` neither image can be
|
|
2382
|
+
* folded into its own local (l3/constinit.ts).
|
|
2383
|
+
*
|
|
2384
|
+
* 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), an escape nothing bounds (a callee or a holder
|
|
2386
|
+
* that may reach any byte from the address it was given), a byte no use names (nothing says
|
|
2387
|
+
* which side it belongs to), or a use through a member that lands outside the part its own
|
|
2388
|
+
* address lies in (`&a[16]` read at [-1]: one pointer walking two parts is one object). The
|
|
2389
|
+
* union views of two widths at one byte are not split either (the caller asks only without
|
|
2390
|
+
* them). An access straddling two runs chains them into one part by construction. */
|
|
2391
|
+
function provenParts(
|
|
2392
|
+
{ from, to }: FrameRange,
|
|
2393
|
+
objects: ReadonlyMap<number, readonly Op[]>,
|
|
2394
|
+
uses: FrameUses,
|
|
2395
|
+
alignment: number,
|
|
2396
|
+
): FrameRange[] {
|
|
2397
|
+
const whole = [{ from, to }];
|
|
2398
|
+
if (uses.indexed.size > 0) {
|
|
2399
|
+
return whole;
|
|
2400
|
+
}
|
|
2401
|
+
const touched: { owner: number; lo: number; hi: number }[] = [];
|
|
2402
|
+
for (const [op, { off }] of uses.accessOps) {
|
|
2403
|
+
const at = off + (op.attrs.off as number);
|
|
2404
|
+
touched.push({ owner: off, lo: at, hi: at + (op.attrs.width as number) });
|
|
2405
|
+
}
|
|
2406
|
+
for (const [off, reads] of uses.byValueReads) {
|
|
2407
|
+
reads.forEach(({ type }) =>
|
|
2408
|
+
touched.push({ owner: off, lo: off, hi: off + (type.kind === 'struct' ? (type.size ?? Infinity) : Infinity) }),
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
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
|
+
// …and a boundary only where a bounded copy starts: an address handed to `memcpy` is where
|
|
2420
|
+
// the source put an object it fills. A run starting anywhere else is bytes reached from below,
|
|
2421
|
+
// which says nothing of where one object ends — the one-object relift reads every slot through
|
|
2422
|
+
// an address of its own, so a struct spilled word by word and read through its base shows a
|
|
2423
|
+
// run per word, and is one object at every word. The LOWEST part is held to the same test
|
|
2424
|
+
// (below): where it is only stores, a copy above it is as likely `memcpy(&buf[4], s, 12)` into
|
|
2425
|
+
// the middle of one array as the start of a second one, and declared as two the recompile
|
|
2426
|
+
// reads `buf[6]` through a base it holds where the target re-formed sp+6. Only a copy's
|
|
2427
|
+
// DESTINATION counts: a copy reading OUT of the frame says where the source took bytes from,
|
|
2428
|
+
// which is as likely `memcpy(g, &w[2], 8)` from the middle of one array as a second object.
|
|
2429
|
+
const starts = new Set([...uses.escapeExtents.values()].flat().flatMap((e) => (e?.into ? [e.lo] : [])));
|
|
2430
|
+
const parts: { from: number; to: number }[] = [];
|
|
2431
|
+
for (const { lo, hi } of [...touched].sort((a, b) => a.lo - b.lo)) {
|
|
2432
|
+
const last = parts.at(-1);
|
|
2433
|
+
if (last !== undefined && (lo < last.to || (lo === last.to && !starts.has(lo)))) {
|
|
2434
|
+
last.to = Math.max(last.to, hi);
|
|
2435
|
+
} else {
|
|
2436
|
+
parts.push({ from: lo, to: hi });
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
const tiles =
|
|
2440
|
+
parts.length > 1 &&
|
|
2441
|
+
parts[0].from === from &&
|
|
2442
|
+
parts.at(-1)!.to === to &&
|
|
2443
|
+
parts.every((p, i) => i === 0 || parts[i - 1].to === p.from);
|
|
2444
|
+
const partOf = (at: number) => parts.findIndex((p) => at >= p.from && at < p.to);
|
|
2445
|
+
if (
|
|
2446
|
+
!tiles ||
|
|
2447
|
+
[...objects.keys()].some((off) => partOf(off) < 0) ||
|
|
2448
|
+
touched.some(({ owner, lo }) => partOf(owner) !== partOf(lo))
|
|
2449
|
+
) {
|
|
2450
|
+
return whole;
|
|
2451
|
+
}
|
|
2452
|
+
// …and a part starts only where the compiler places a local array of its own: at a multiple of
|
|
2453
|
+
// its `alignment` (agbcc pads `u8 a[18]` to 20, so a split there declares `b` at sp+20 where the
|
|
2454
|
+
// target had it at sp+18; gcc 2.7.2 on MIPS starts every one at a multiple of 8, so a part at
|
|
2455
|
+
// sp+20 is placed at sp+24 and the frame grows), and never as ONE byte, which declared alone is
|
|
2456
|
+
// a `u8` scalar whose address nothing takes — kept in a register, it drops the frame byte and
|
|
2457
|
+
// the store the target made. Such a part joins the one below it, and the lowest one byte joins
|
|
2458
|
+
// the one above: fewer parts only ever read as the one object does, so this costs a split,
|
|
2459
|
+
// never a value.
|
|
2460
|
+
const merged: { from: number; to: number }[] = [];
|
|
2461
|
+
for (const p of parts) {
|
|
2462
|
+
const last = merged.at(-1);
|
|
2463
|
+
if (last !== undefined && (p.from % alignment !== 0 || p.to - p.from === 1 || last.to - last.from === 1)) {
|
|
2464
|
+
last.to = p.to;
|
|
2465
|
+
} else {
|
|
2466
|
+
merged.push({ ...p });
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
return merged.length > 1 && merged.every((p) => starts.has(p.from)) ? merged : whole;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
/** Why the one object's bytes, accessed at two widths, cannot be spelled as a union of per-width
|
|
2473
|
+
* views, or null. A view names an ELEMENT, so every access must sit at a multiple of its own width
|
|
2474
|
+
* from the object's start and every view must tile the object; and it names it at a constant
|
|
2475
|
+
* index, so a runtime index into the object — whose element width a view could carry, but whose
|
|
2476
|
+
* spelling nothing here builds — keeps the refusal, and so does a qualifier on the accesses
|
|
2477
|
+
* (`publishedToWriter`), which a member access of an unqualified union cannot carry. */
|
|
2478
|
+
function unionRefusal({
|
|
2479
|
+
from,
|
|
2480
|
+
to,
|
|
2481
|
+
uses,
|
|
2482
|
+
indexed,
|
|
2483
|
+
}: {
|
|
2484
|
+
from: number;
|
|
2485
|
+
to: number;
|
|
2486
|
+
uses: FrameUses;
|
|
2487
|
+
indexed: boolean;
|
|
2488
|
+
}): string | null {
|
|
2489
|
+
if (indexed) {
|
|
2490
|
+
return 'a runtime index reaches them too, which no view of a union is spelled for';
|
|
2491
|
+
}
|
|
2492
|
+
if (uses.publishedToWriter.size > 0) {
|
|
2493
|
+
return 'the address is stored where a writer may hold it, and a view of a union carries no per-access qualifier';
|
|
2494
|
+
}
|
|
2495
|
+
for (const m of uses.members) {
|
|
2496
|
+
if ((m.at - from) % m.width !== 0) {
|
|
2497
|
+
return `the ${m.width}-byte access at [sp,#${m.at}) is not aligned to its width, so no view of a union names it`;
|
|
2498
|
+
}
|
|
2499
|
+
if ((to - from) % m.width !== 0) {
|
|
2500
|
+
return `the ${to - from} bytes are not a whole number of ${m.width}-byte elements, so no view of a union tiles them`;
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
return null;
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
/** The views a union object's accesses use, as `laddr`'s `views` attr: one entry per view, its
|
|
2507
|
+
* element width in bytes, NEGATIVE for a signed narrow view — which views, by the rule
|
|
2508
|
+
* raise/structs.ts declares a struct member's union by (ir/types.ts `unionViewSigns`). A word's
|
|
2509
|
+
* one view is `s32` whichever sign it carries, so it is entered unsigned. Widest first, unsigned
|
|
2510
|
+
* before signed. */
|
|
2511
|
+
function unionViews(irBlocks: readonly Block[], base: Value): number[] {
|
|
2512
|
+
const byWidth = new Map<number, { isLoad: boolean; signed: boolean; stamped: boolean }[]>();
|
|
2513
|
+
for (const op of irBlocks.flatMap((b) => b.ops)) {
|
|
2514
|
+
if ((op.opcode === 'load' || op.opcode === 'store') && op.operands[0] === base) {
|
|
2515
|
+
const w = op.attrs.width as number;
|
|
2516
|
+
(byWidth.get(w) ?? byWidth.set(w, []).get(w)!).push({
|
|
2517
|
+
isLoad: op.opcode === 'load',
|
|
2518
|
+
signed: (op.attrs.signed as boolean) ?? false,
|
|
2519
|
+
stamped: op.opcode === 'store' && op.attrs.signed !== undefined,
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
return [...byWidth]
|
|
2524
|
+
.sort(([a], [b]) => b - a)
|
|
2525
|
+
.flatMap(([w, accesses]) => unionViewSigns(w, accesses).map((sg) => (sg && w !== 4 ? -w : w)));
|
|
1614
2526
|
}
|
|
1615
2527
|
|
|
1616
2528
|
/** One object's shape: `count` elements of `width` bytes. */
|
|
@@ -1640,8 +2552,10 @@ function slotKeys(usedSlotOffsets: ReadonlySet<number>, off: number, width: numb
|
|
|
1640
2552
|
* AN OBJECT OVER A SLOT IS REFUSED LAST, after the one-object answer is asked: that answer
|
|
1641
2553
|
* routes every word of the local area through `laddr`, so no slot is left for the object to
|
|
1642
2554
|
* 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
|
-
*
|
|
2555
|
+
* [sp,#0] whose first member is a word stored `str rN, [sp]`, or a by-value struct parameter
|
|
2556
|
+
* spilled word by word and read through `mov rD, sp` where nothing escapes. The other per-object
|
|
2557
|
+
* refusals here go through `shapeRefused`, for the same reason: where nothing escapes, each of
|
|
2558
|
+
* them asks for that answer too (`chooseFrameModel`). */
|
|
1645
2559
|
function objectShapes({
|
|
1646
2560
|
objects,
|
|
1647
2561
|
uses,
|
|
@@ -1667,30 +2581,63 @@ function objectShapes({
|
|
|
1667
2581
|
for (const [off, acc] of uses.accesses) {
|
|
1668
2582
|
const byIndex = uses.indexed.get(off) ?? [];
|
|
1669
2583
|
const temps = uses.returnTemps.get(off);
|
|
2584
|
+
const passed = uses.byValueReads.get(off);
|
|
2585
|
+
if (passed !== undefined) {
|
|
2586
|
+
// A struct passed BY VALUE (`foldByValueArgs`): this function writes and reads its members,
|
|
2587
|
+
// and the call reads it whole. A struct-return temp passed on, an index, or any other use of
|
|
2588
|
+
// the address is not modelled.
|
|
2589
|
+
const type = passed[0].type;
|
|
2590
|
+
const at = `the object at [sp,#${off}) is passed by value as ${structSpelling(type)}`;
|
|
2591
|
+
if (temps !== undefined) {
|
|
2592
|
+
fail(`${at}, and is also the storage a call returns a struct into — not modelled`);
|
|
2593
|
+
}
|
|
2594
|
+
if (passed.some((p) => !typeEquals(p.type, type))) {
|
|
2595
|
+
fail(`${at} to calls declared to take different types`);
|
|
2596
|
+
}
|
|
2597
|
+
if (byIndex.length > 0) {
|
|
2598
|
+
fail(`${at}, and this function also indexes it — only its members' accesses are modelled`);
|
|
2599
|
+
}
|
|
2600
|
+
if (uses.useCount.get(off) !== acc.length + passed.length) {
|
|
2601
|
+
fail(`${at}, and its address is also used another way — only its members' accesses are modelled`);
|
|
2602
|
+
}
|
|
2603
|
+
const size = type.kind === 'struct' ? type.size! : 0;
|
|
2604
|
+
const past = uses.fixed.find(
|
|
2605
|
+
(f) => uses.accessOps.get(f.op)?.off === off && (f.at < off || f.at + f.width > off + size),
|
|
2606
|
+
);
|
|
2607
|
+
if (past !== undefined) {
|
|
2608
|
+
fail(`${at}, and this function accesses [sp,#${past.at}) of width ${past.width}, past the struct`);
|
|
2609
|
+
}
|
|
2610
|
+
extent.set(off, { width: 1, count: size });
|
|
2611
|
+
aggregateAt.set(off, type);
|
|
2612
|
+
continue;
|
|
2613
|
+
}
|
|
1670
2614
|
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.
|
|
2615
|
+
// The callee writes the whole struct and this function names it as the call's destination,
|
|
2616
|
+
// and READS it: each load is of bytes inside the struct, which the struct's declaration
|
|
2617
|
+
// types (raise/aggmembers.ts reads a word's members, structure.ts spells a member access).
|
|
2618
|
+
// A write, an index, or any other use of the address is not modelled.
|
|
1677
2619
|
const callee = temps[0].attrs.target as string;
|
|
1678
2620
|
const type = temps[0].results[0].type;
|
|
1679
2621
|
const spelling = type.kind === 'struct' ? (type.declared ?? `struct ${type.name}`) : typeToString(type);
|
|
2622
|
+
const at = `the object at [sp,#${off}) is where \`${callee}\` returns ${spelling}`;
|
|
1680
2623
|
if (temps.some((t) => !typeEquals(t.results[0].type, type))) {
|
|
1681
2624
|
fail(`the object at [sp,#${off}) is the struct-return storage of calls declared to return different types`);
|
|
1682
2625
|
}
|
|
1683
|
-
if (acc.
|
|
2626
|
+
if (acc.some((a) => !a.isLoad) || byIndex.length > 0) {
|
|
1684
2627
|
fail(
|
|
1685
|
-
|
|
1686
|
-
'
|
|
2628
|
+
`${at}, and this function also ${byIndex.length > 0 ? 'indexes' : 'writes'} it — only a read of a ` +
|
|
2629
|
+
'returned struct is modelled',
|
|
1687
2630
|
);
|
|
1688
2631
|
}
|
|
1689
|
-
if (uses.useCount.get(off) !== temps.length) {
|
|
1690
|
-
fail(
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
2632
|
+
if (uses.useCount.get(off) !== temps.length + acc.length) {
|
|
2633
|
+
fail(`${at}, and its address is also used another way — only the call it is returned by is modelled`);
|
|
2634
|
+
}
|
|
2635
|
+
const size = returnedSize(temps[0]);
|
|
2636
|
+
const past = uses.fixed.find(
|
|
2637
|
+
(f) => uses.accessOps.get(f.op)?.off === off && (f.at < off || f.at + f.width > off + size),
|
|
2638
|
+
);
|
|
2639
|
+
if (past !== undefined) {
|
|
2640
|
+
fail(`${at}, and this function reads [sp,#${past.at}) of width ${past.width}, past the struct`);
|
|
1694
2641
|
}
|
|
1695
2642
|
extent.set(off, { width: 1, count: returnedSize(temps[0]) });
|
|
1696
2643
|
aggregateAt.set(off, type);
|
|
@@ -1721,26 +2668,18 @@ function objectShapes({
|
|
|
1721
2668
|
// …asked for the one object instead, which sizes no object of its own
|
|
1722
2669
|
continue;
|
|
1723
2670
|
}
|
|
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
|
-
}
|
|
2671
|
+
// An indexed access reads THROUGH A CAST on the storage declared below, at its own width
|
|
2672
|
+
// and signedness, as an access to the one object does (`keepAsOneObject`), so the accesses
|
|
2673
|
+
// wider than a byte must be of one width, one alias set (`indexedWidth`).
|
|
2674
|
+
const index = indexedWidth(byIndex);
|
|
2675
|
+
if (index.refusal !== null) {
|
|
2676
|
+
model.shapeRefused(`a runtime index into the object at [sp,#${off}) ${index.refusal}`);
|
|
1739
2677
|
}
|
|
1740
2678
|
// STORAGE, NOT A TYPE. The reservation says how many bytes the frame holds and the
|
|
1741
2679
|
// conjuncts above say they are all this object's, so the declaration commits to an EXTENT
|
|
1742
2680
|
// and to nothing else: `u8 name[n]` over the declared range, unsigned bytes because no
|
|
1743
2681
|
// access named an element type and inventing one is the guess this refuses everywhere else.
|
|
2682
|
+
// An indexed access names only its own width, which its cast spells where it reads.
|
|
1744
2683
|
//
|
|
1745
2684
|
// THE EXTENT IS A ROUNDED ONE, stated because it is not a defect. agbcc reserves the
|
|
1746
2685
|
// local area in whole words, so a source object of 13, 14, 15 or 16 bytes all reserve
|
|
@@ -2058,8 +2997,9 @@ function stampObjects(
|
|
|
2058
2997
|
): void {
|
|
2059
2998
|
for (const [off, ops] of objects) {
|
|
2060
2999
|
const { width, count } = extent.get(off)!;
|
|
2061
|
-
const signed = uses.accesses.get(off)!.some((a) => a.signed);
|
|
2062
3000
|
const aggregate = aggregateAt.get(off);
|
|
3001
|
+
// a declared struct's storage is bytes, whatever signedness its members are read at
|
|
3002
|
+
const signed = aggregate === undefined && uses.accesses.get(off)!.some((a) => a.signed);
|
|
2063
3003
|
for (const op of ops) {
|
|
2064
3004
|
const qualified = uses.published.has(off) || uses.filledFrom.has(off);
|
|
2065
3005
|
op.attrs = {
|
|
@@ -2092,8 +3032,8 @@ function gcd(a: number, b: number): number {
|
|
|
2092
3032
|
return b === 0 ? Math.abs(a) : gcd(b, a % b);
|
|
2093
3033
|
}
|
|
2094
3034
|
|
|
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). */
|
|
3035
|
+
/** The size of the struct a call stamped `sret` returns through its argument 0 (frontend/call-plan.ts
|
|
3036
|
+
* types the call's result as that struct, for every frontend whose lowering passes the pointer). */
|
|
2097
3037
|
function returnedSize(call: Op): number {
|
|
2098
3038
|
const t = call.results[0].type;
|
|
2099
3039
|
if (t.kind !== 'struct' || t.size === undefined) {
|
|
@@ -2102,8 +3042,108 @@ function returnedSize(call: Op): number {
|
|
|
2102
3042
|
return t.size;
|
|
2103
3043
|
}
|
|
2104
3044
|
|
|
3045
|
+
/** The block parameters every incoming value of which is a local's address or another such
|
|
3046
|
+
* parameter: the greatest set closed under that, so a loop-carried address is one too. Each maps to
|
|
3047
|
+
* the frame offsets its incoming addresses name, through such parameters. The entry block's are the
|
|
3048
|
+
* function's parameters, and are left out. */
|
|
3049
|
+
function mergedFrameAddresses(irBlocks: readonly Block[]): Map<Value, ReadonlySet<number>> {
|
|
3050
|
+
const laddrOff = new Map<Value, number>();
|
|
3051
|
+
for (const op of irBlocks.flatMap((b) => b.ops)) {
|
|
3052
|
+
if (op.opcode === 'laddr') {
|
|
3053
|
+
laddrOff.set(op.results[0], op.attrs.off as number);
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
const incoming = new Map<Value, Value[]>();
|
|
3057
|
+
for (const b of irBlocks.slice(1)) {
|
|
3058
|
+
for (const p of b.params) {
|
|
3059
|
+
incoming.set(p, []);
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
for (const op of irBlocks.flatMap((b) => b.ops)) {
|
|
3063
|
+
for (const succ of op.successors) {
|
|
3064
|
+
succ.block.params.forEach((p, i) => incoming.get(p)?.push(succ.args[i]));
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
const merged = new Set([...incoming].filter(([, vs]) => vs.length > 0).map(([p]) => p));
|
|
3068
|
+
for (let changed = true; changed;) {
|
|
3069
|
+
changed = false;
|
|
3070
|
+
for (const p of merged) {
|
|
3071
|
+
if (incoming.get(p)!.some((v) => !laddrOff.has(v) && !merged.has(v))) {
|
|
3072
|
+
merged.delete(p);
|
|
3073
|
+
changed = true;
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
const out = new Map<Value, ReadonlySet<number>>();
|
|
3078
|
+
for (const p of merged) {
|
|
3079
|
+
const offs = new Set<number>();
|
|
3080
|
+
const seen = new Set<Value>([p]);
|
|
3081
|
+
const work = [p];
|
|
3082
|
+
while (work.length > 0) {
|
|
3083
|
+
for (const v of incoming.get(work.pop()!)!) {
|
|
3084
|
+
if (laddrOff.has(v)) {
|
|
3085
|
+
offs.add(laddrOff.get(v)!);
|
|
3086
|
+
} else if (!seen.has(v)) {
|
|
3087
|
+
seen.add(v);
|
|
3088
|
+
work.push(v);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
out.set(p, offs);
|
|
3093
|
+
}
|
|
3094
|
+
return out;
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
/** A STRUCT RETURNED THROUGH A MERGE OF ONE LOCAL'S ADDRESS IS THAT LOCAL. gcc rotates a loop that
|
|
3098
|
+
* calls a struct-returning function, `while (lo > 0) { qr = ldiv(lo, 10); … }`, so `addiu a0,sp,16`
|
|
3099
|
+
* runs once ahead of the loop and once in the delay slot of its back branch, and hoists it into
|
|
3100
|
+
* both arms of `if (x) a = f(x); else a = f(x + 1);` ahead of one shared `jal`. The call then
|
|
3101
|
+
* reads a block parameter, but every path hands it the same frame address, so the call is handed
|
|
3102
|
+
* that address: an `laddr` of it, minted ahead of the call, which the frame-object audit types as
|
|
3103
|
+
* the struct (`declaredStructs`) and `refuseSretOutsideFrame` accepts. A merge of two offsets is
|
|
3104
|
+
* two objects, and is left for the audit to refuse. Run before the audit, on every frontend whose call
|
|
3105
|
+
* lowering passes the hidden pointer. */
|
|
3106
|
+
export function nameMergedSretObjects(irBlocks: Block[]): void {
|
|
3107
|
+
const merged = mergedFrameAddresses(irBlocks);
|
|
3108
|
+
for (const b of irBlocks) {
|
|
3109
|
+
b.ops = b.ops.flatMap((op) => {
|
|
3110
|
+
const offs = op.opcode === 'call' && op.attrs.sret === true ? merged.get(op.operands[0]) : undefined;
|
|
3111
|
+
if (offs === undefined || offs.size !== 1) {
|
|
3112
|
+
return [op];
|
|
3113
|
+
}
|
|
3114
|
+
const at = mkValue(T.unk(32));
|
|
3115
|
+
op.operands = [at, ...op.operands.slice(1)];
|
|
3116
|
+
return [mkOp('laddr', { results: [at], attrs: { off: [...offs][0] } }), op];
|
|
3117
|
+
});
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
/** A STRUCT RETURNED THROUGH MEMORY IS SPELLED `local = f(..)`, which hands the callee that local's
|
|
3122
|
+
* address: the hidden pointer must be a frame object the audit accepted. A global or a pointer
|
|
3123
|
+
* handed over directly — `gS = f(..)` (agbcc, ido, gcc), IDO's `*p = f(..)`, and the caller's own
|
|
3124
|
+
* hidden pointer forwarded by `return f(..)` — is another spelling, which nothing here states. A
|
|
3125
|
+
* merge of frame addresses never reaches here: one that names one address on every path was handed
|
|
3126
|
+
* that address's `laddr` before the audit (`nameMergedSretObjects`), and a merge of two names no one
|
|
3127
|
+
* storage, which the audit refuses first (`addressFlow`: `a phi merges the frame objects`). Asked
|
|
3128
|
+
* of every frontend whose call lowering passes the hidden pointer
|
|
3129
|
+
* (`CallLowering.memoryReturn`). */
|
|
3130
|
+
export function refuseSretOutsideFrame(name: string, irBlocks: readonly Block[], target: TargetDescription): void {
|
|
3131
|
+
const laddrs = new Set(
|
|
3132
|
+
irBlocks.flatMap((b) => b.ops.filter((op) => op.opcode === 'laddr').map((op) => op.results[0])),
|
|
3133
|
+
);
|
|
3134
|
+
for (const op of irBlocks.flatMap((b) => b.ops)) {
|
|
3135
|
+
if (op.opcode === 'call' && op.attrs.sret === true && !laddrs.has(op.operands[0])) {
|
|
3136
|
+
throw new FrontendUnsupportedError(
|
|
3137
|
+
`cannot lift '${name}': \`${op.attrs.target as string}\` returns struct ${typeToString(op.results[0].type)} through the pointer in ${target.argRegs[0]}, ` +
|
|
3138
|
+
'and that pointer is not the address of a local of this frame — a struct returned into a global or ' +
|
|
3139
|
+
'through a pointer is not modelled',
|
|
3140
|
+
);
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
|
|
2105
3145
|
/** THE CALLER-SIDE SEAM. A frontend calls the audit THROUGH this record rather than through the
|
|
2106
|
-
* binding
|
|
3146
|
+
* `auditFrameObjects` binding, so a process outside core can put a wrapped `gates` table in front of a real
|
|
2107
3147
|
* lift (`pnpm bench gates --pass frame-objects`): a module-namespace binding is read-only and
|
|
2108
3148
|
* cannot be swapped (`apps/benchmark/src/run/gate-census.ts`, WHAT PUTS A PASS IN THE REGISTRY). */
|
|
2109
3149
|
export const FRAME_OBJECT_AUDIT: { run: (audit: FrameObjectAudit) => FrameObjectVerdict } = {
|