@asmlift/core 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +33 -12
- package/src/backend/cpp.ts +1 -0
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +17 -5
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +73 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +2 -1
- package/src/frontend/call-plan.ts +197 -45
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/device-pins.ts +18 -8
- package/src/frontend/frame-objects.ts +504 -61
- package/src/frontend/frontend.ts +20 -0
- package/src/frontend/jump-table.ts +35 -20
- package/src/frontend/mips.ts +226 -8
- package/src/frontend/ppc.ts +26 -2
- package/src/frontend/register-struct-call.ts +1 -86
- package/src/frontend/ssa.ts +263 -1
- package/src/frontend/stackargs.ts +95 -17
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +410 -61
- package/src/ir/alias.ts +29 -8
- package/src/ir/bits.ts +1081 -66
- package/src/ir/core.ts +154 -4
- package/src/ir/entry.ts +3 -5
- package/src/ir/opcodes.ts +28 -0
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/verify.ts +16 -0
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +294 -2
- package/src/l3/basecse.ts +35 -10
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +34 -6
- package/src/l3/dce.ts +52 -8
- package/src/l3/ptrcell.ts +2 -0
- package/src/l3/regspell.ts +2 -0
- package/src/l3/reindex.ts +13 -4
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/tailmerge.ts +2 -0
- package/src/pipeline.ts +507 -23
- package/src/proto-context.ts +135 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +1 -1
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/offsetnames.ts +412 -10
- package/src/raise/pre-recovery.ts +8 -5
- package/src/raise/pre-structure.ts +24 -5
- package/src/raise/reroll.ts +4 -24
- package/src/raise/shortcircuit.ts +192 -23
- package/src/raise/unpre.ts +594 -0
- package/src/rank-variations.ts +97 -4
- package/src/rank.ts +258 -43
- package/src/structure/analysis.ts +56 -10
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +46 -1
- package/src/structure/structure.ts +4221 -368
- package/src/structure/switch-recover.ts +345 -26
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +52 -15
- package/src/target.ts +269 -8
- package/src/trace.ts +28 -14
- package/src/variation-definitions.ts +207 -6
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +63 -2
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
// drops the rest, a mask. A function that reads past them — stores the whole word, compares it,
|
|
26
26
|
// returns it where its own declaration says it returns a value — declines; one whose return
|
|
27
27
|
// nothing declares returns no value instead, where that guessed return is the only such reader.
|
|
28
|
+
import { demandedBits } from '../ir/bits';
|
|
28
29
|
import {
|
|
29
30
|
type Block,
|
|
30
31
|
CALL_RETURN_OBJECT,
|
|
@@ -396,89 +397,3 @@ function foldFrameHome(fn: Fn, { block, local, size, word, loaded }: Image): voi
|
|
|
396
397
|
}
|
|
397
398
|
|
|
398
399
|
const FULL = 0xffffffff;
|
|
399
|
-
|
|
400
|
-
/** THE BITS OF `v` ANY READER NEEDS, as a mask: a store of W bytes needs the low 8W bits of the
|
|
401
|
-
* value it stores, a constant mask and the bitwise ops pass their readers' bits through, a shift
|
|
402
|
-
* by a constant moves them, an add, a subtract or a multiply needs every bit up to its readers'
|
|
403
|
-
* highest, since a carry only travels up, and a join needs what its parameter's readers need.
|
|
404
|
-
* Every other reader — a compare, a call, an address — needs all 32, and so does a join that
|
|
405
|
-
* reaches itself again. A `ret` needs none where the function's own declaration returns nothing. */
|
|
406
|
-
function demandedBits(fn: Fn, root: Value, voidOwn: boolean): number {
|
|
407
|
-
const defs = defOpMap(fn);
|
|
408
|
-
const memo = new Map<Value, number>();
|
|
409
|
-
const constOf = (x: Value): number | undefined => {
|
|
410
|
-
const d = defs.get(x);
|
|
411
|
-
return d?.opcode === 'const' && typeof d.attrs.value === 'number' ? d.attrs.value >>> 0 : undefined;
|
|
412
|
-
};
|
|
413
|
-
const upTo = (d: number): number => (d === 0 ? 0 : d >= 0x80000000 ? FULL : (2 ** (32 - Math.clz32(d)) - 1) >>> 0);
|
|
414
|
-
const of = (v: Value): number => {
|
|
415
|
-
const known = memo.get(v);
|
|
416
|
-
if (known !== undefined) {
|
|
417
|
-
return known;
|
|
418
|
-
}
|
|
419
|
-
memo.set(v, FULL); // a reader reached again through a join needs everything
|
|
420
|
-
let need = 0;
|
|
421
|
-
for (const b of fn.blocks) {
|
|
422
|
-
for (const op of b.ops) {
|
|
423
|
-
for (const e of op.successors) {
|
|
424
|
-
e.args.forEach((a, i) => {
|
|
425
|
-
if (a === v) {
|
|
426
|
-
need |= of(e.block.params[i]);
|
|
427
|
-
}
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
if (!op.operands.includes(v)) {
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
const out = () => (op.results.length === 1 ? of(op.results[0]) : FULL);
|
|
434
|
-
const imm = typeof op.attrs.imm === 'number' && op.operands.length === 1 ? op.attrs.imm : undefined;
|
|
435
|
-
const other = op.operands.length === 2 ? op.operands[op.operands[0] === v ? 1 : 0] : undefined;
|
|
436
|
-
switch (op.opcode) {
|
|
437
|
-
case 'store':
|
|
438
|
-
need |=
|
|
439
|
-
op.operands[0] === v || typeof op.attrs.width !== 'number' || op.attrs.width >= 4
|
|
440
|
-
? FULL
|
|
441
|
-
: (1 << (8 * op.attrs.width)) - 1;
|
|
442
|
-
break;
|
|
443
|
-
case 'ret':
|
|
444
|
-
need |= voidOwn ? 0 : FULL;
|
|
445
|
-
break;
|
|
446
|
-
case 'and': {
|
|
447
|
-
const c = other === undefined || other === v ? undefined : constOf(other);
|
|
448
|
-
need |= c === undefined ? out() : out() & c;
|
|
449
|
-
break;
|
|
450
|
-
}
|
|
451
|
-
case 'or':
|
|
452
|
-
case 'xor':
|
|
453
|
-
case 'not':
|
|
454
|
-
need |= other === v ? FULL : out();
|
|
455
|
-
break;
|
|
456
|
-
case 'add':
|
|
457
|
-
case 'sub':
|
|
458
|
-
case 'mul':
|
|
459
|
-
case 'neg':
|
|
460
|
-
need |= other === v ? FULL : upTo(out());
|
|
461
|
-
break;
|
|
462
|
-
case 'shl':
|
|
463
|
-
need |= imm === undefined ? FULL : out() >>> imm;
|
|
464
|
-
break;
|
|
465
|
-
case 'shr_u':
|
|
466
|
-
need |= imm === undefined ? FULL : (out() << imm) >>> 0;
|
|
467
|
-
break;
|
|
468
|
-
case 'shr_s': {
|
|
469
|
-
const d = out();
|
|
470
|
-
need |=
|
|
471
|
-
imm === undefined ? FULL : ((d << imm) | (imm > 0 && d >>> (32 - imm) !== 0 ? 0x80000000 : 0)) >>> 0;
|
|
472
|
-
break;
|
|
473
|
-
}
|
|
474
|
-
default:
|
|
475
|
-
need |= FULL;
|
|
476
|
-
}
|
|
477
|
-
need >>>= 0;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
memo.set(v, need);
|
|
481
|
-
return need;
|
|
482
|
-
};
|
|
483
|
-
return of(root);
|
|
484
|
-
}
|
package/src/frontend/ssa.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// `irBlocks`, args left empty — phi wiring appends them), then call `markFilled(b)`. When all
|
|
17
17
|
// blocks are filled, call `finish()` to remove trivial phis.
|
|
18
18
|
import {
|
|
19
|
+
type AsmLayout,
|
|
19
20
|
Block,
|
|
20
21
|
Fn,
|
|
21
22
|
Op,
|
|
@@ -65,6 +66,11 @@ export interface SsaBuilder {
|
|
|
65
66
|
*
|
|
66
67
|
* One of the three call verbs `CallDeclarations.lower` owns ({@link noteCall}). */
|
|
67
68
|
readGuessedArg(reg: string, b: number): Value;
|
|
69
|
+
/** Record that the call being lowered in block `b` reads `v` from argument register `reg` it
|
|
70
|
+
* guessed it takes, for {@link readAfterEveryReachingWrite} alone: the call is an instruction
|
|
71
|
+
* reading what the register's last write left there, as a sibling path's call may be, while
|
|
72
|
+
* {@link readGuessedArg} makes the read carry no claim for `finish` to judge. */
|
|
73
|
+
noteGuessedArgRead(reg: string, b: number, v: Value): void;
|
|
68
74
|
/** The value `reg` holds at the END of block `b` once `b` is filled, as the edges out of it carry
|
|
69
75
|
* it; `null` before. A lookup, not a read: it records no read for `finish` to judge. */
|
|
70
76
|
readAtEnd(reg: string, b: number): Value | null;
|
|
@@ -125,6 +131,46 @@ export interface SsaBuilder {
|
|
|
125
131
|
/** Whether block `b` WROTE `reg` since its last call so far (since its start if it made none).
|
|
126
132
|
* Only a write counts: a read leaves a definition behind but is not one (`writeVar`). */
|
|
127
133
|
writtenSinceCall(reg: string, b: number): boolean;
|
|
134
|
+
/** How many ops block `b` held when it last wrote `reg` so far, or `undefined` where `b` has not
|
|
135
|
+
* written it: every op at or past that index came after the write. A copy (`add r1, r5, #0`) names
|
|
136
|
+
* no new value, so this is how a question about one WRITE of a register is asked, where its value
|
|
137
|
+
* was read before the write too (frontend/call-plan.ts `leftover`). */
|
|
138
|
+
lastWriteOp(reg: string, b: number): number | undefined;
|
|
139
|
+
/** Whether an instruction READ `reg` after EACH write of it that reaches block `b` and whose value
|
|
140
|
+
* `passes`, on ANY path out of that write but the ones into `b`, the first read on that path being
|
|
141
|
+
* one whose value `accept` takes. A write no read follows on any path was
|
|
142
|
+
* made for `b`'s use, so one is enough to answer no. A call destroying `reg` is no write of this
|
|
143
|
+
* function's, and neither is the entry's value where a path from the entry writes neither, so they
|
|
144
|
+
* do not answer no; the entry's value read after (as `accept` takes it), or read by a lowered op
|
|
145
|
+
* at all (as `unlowered` takes it: no write placed that read), counts as a write read. Where a
|
|
146
|
+
* call destroyed `reg` on EVERY path in, it holds what a callee left there and the answer is yes.
|
|
147
|
+
* The question is about the write, not its value: a copy (`add r4, r0, #0`) reads `r0` and lowers
|
|
148
|
+
* to no op, so no op-level question sees that read, and it counts here; a copy made in an earlier
|
|
149
|
+
* block names the value it copies, read before that write too, and is no read of it. Any path out
|
|
150
|
+
* of the write, because a MIPS compiler hoists one call's argument into the delay slot of the
|
|
151
|
+
* branch that picks it (`bnez v0, L; move a0, zero`), so the write reaches the other call unread
|
|
152
|
+
* on the path between. `b`'s own reads are not asked: what a read there that lowers to no op says
|
|
153
|
+
* differs by compiler (frontend/call-plan.ts `leftover`). Only the frontend's own reads count
|
|
154
|
+
* ({@link readVar}), and a guessed call's argument read the planner records for it
|
|
155
|
+
* ({@link noteGuessedArgRead}: a sibling path's call reads the write); a lookup this construction
|
|
156
|
+
* makes, or the planner's own probe ({@link readGuessedArg} alone), is none. A block not
|
|
157
|
+
* filled yet is passed through looking for the write, as {@link holdsValue} does; a path out of
|
|
158
|
+
* a write that reaches one has reads nobody has seen yet, and the write's value answers for it
|
|
159
|
+
* as `unlowered` takes it (IDO reloads `lw a0, 24(sp)` in a branch's delay slot for the arm laid
|
|
160
|
+
* out after the call). A write of a value `answersIfRead` takes (an earlier call's result, copied
|
|
161
|
+
* or reloaded) answers yes where it was read after, and is otherwise none: such a value is the
|
|
162
|
+
* callee's, saved across a call as often as handed to one. `undefined` where no write of this
|
|
163
|
+
* function's is left to judge and no entry value read: the question has no answer here. */
|
|
164
|
+
readAfterEveryReachingWrite(
|
|
165
|
+
reg: string,
|
|
166
|
+
b: number,
|
|
167
|
+
ask: {
|
|
168
|
+
passes: (v: Value) => boolean;
|
|
169
|
+
accept: (v: Value) => boolean;
|
|
170
|
+
unlowered: (v: Value) => boolean;
|
|
171
|
+
answersIfRead: (v: Value) => boolean;
|
|
172
|
+
},
|
|
173
|
+
): boolean | undefined;
|
|
128
174
|
/** Whether EVERY path into this point of block `b` wrote `reg` (or had a call destroy it) after
|
|
129
175
|
* leaving the last block `stop` names: {@link writtenSinceCall} asked of `b` so far and, where `b`
|
|
130
176
|
* did not write it, of each predecessor in turn. A path reaching a `stop` block, the entry, or a
|
|
@@ -610,6 +656,57 @@ export function clobberedByCall(target: {
|
|
|
610
656
|
return target.callerSaved.filter((r) => r !== target.returnReg);
|
|
611
657
|
}
|
|
612
658
|
|
|
659
|
+
/** One asm block as {@link recordAsmLayout} reads it: its instructions without its control transfer,
|
|
660
|
+
* and whether that transfer is an unconditional jump to a label. `undefined` for a block the
|
|
661
|
+
* frontend made up, which has no place in the asm. An instruction's `reloc` is the relocation that
|
|
662
|
+
* fills it, where the operands do not name the symbol (disasm.ts `DisasmInstr.reloc`). */
|
|
663
|
+
export interface AsmBlockText {
|
|
664
|
+
readonly body: readonly {
|
|
665
|
+
readonly mnemonic: string;
|
|
666
|
+
readonly ops: readonly string[];
|
|
667
|
+
readonly reloc?: { readonly sym: string; readonly addend: number };
|
|
668
|
+
}[];
|
|
669
|
+
readonly endsInJump: boolean;
|
|
670
|
+
/** which instructions of `body` make a call, which only the frontend's ISA knows */
|
|
671
|
+
readonly isCall: (ins: AsmBlockText['body'][number]) => boolean;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/** One instruction of {@link AsmBlockText} as the layout record spells it: `mnemonic op,op`, then the
|
|
675
|
+
* symbol its relocation fills in, if any. In a relocatable object's disassembly the operands of a
|
|
676
|
+
* global's load or store, or of a call, read the same whichever symbol it is: gcc 2.7.2's `lui
|
|
677
|
+
* at,0x0` / `sw a2,0(at)` for `gT = c` and for `gW = c`, and `jal 0 <f>` for every callee. Spelled
|
|
678
|
+
* without the symbol, two arms that store to different globals would read as identical bodies. */
|
|
679
|
+
function layoutText(ins: AsmBlockText['body'][number]): string {
|
|
680
|
+
const text = ins.ops.length ? `${ins.mnemonic} ${ins.ops.join(',')}` : ins.mnemonic;
|
|
681
|
+
if (ins.reloc === undefined) {
|
|
682
|
+
return text;
|
|
683
|
+
}
|
|
684
|
+
const addend = ins.reloc.addend === 0 ? '' : `+0x${ins.reloc.addend.toString(16)}`;
|
|
685
|
+
return `${text} <${ins.reloc.sym}${addend}>`;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** The layout record (ir/core.ts `AsmLayout`): asm block `i` is `irBlocks[i]`, which is how every
|
|
689
|
+
* frontend's builder numbers them, and its index is its position among the blocks that have a
|
|
690
|
+
* place in the asm. */
|
|
691
|
+
export function recordAsmLayout(
|
|
692
|
+
irBlocks: readonly Block[],
|
|
693
|
+
asmBlocks: readonly (AsmBlockText | undefined)[],
|
|
694
|
+
): AsmLayout {
|
|
695
|
+
const layout: AsmLayout = new Map();
|
|
696
|
+
let index = 0;
|
|
697
|
+
asmBlocks.forEach((ab, i) => {
|
|
698
|
+
if (ab !== undefined) {
|
|
699
|
+
layout.set(irBlocks[i], {
|
|
700
|
+
index: index++,
|
|
701
|
+
body: ab.body.map(layoutText),
|
|
702
|
+
calls: ab.body.flatMap((ins, at) => (ab.isCall(ins) ? [at] : [])),
|
|
703
|
+
endsInJump: ab.endsInJump,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
return layout;
|
|
708
|
+
}
|
|
709
|
+
|
|
613
710
|
export function makeSsaBuilder(
|
|
614
711
|
name: string,
|
|
615
712
|
blockCount: number,
|
|
@@ -670,6 +767,7 @@ export function makeSsaBuilder(
|
|
|
670
767
|
slotHomes: undefined,
|
|
671
768
|
paramEvidence: undefined,
|
|
672
769
|
localObjects: undefined,
|
|
770
|
+
asmLayout: undefined,
|
|
673
771
|
};
|
|
674
772
|
|
|
675
773
|
const defs: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
@@ -696,6 +794,16 @@ export function makeSsaBuilder(
|
|
|
696
794
|
|
|
697
795
|
// `preds` lists an entry per CFG EDGE; these are the distinct predecessor BLOCKS.
|
|
698
796
|
const distinctPreds = (b: number): number[] => [...new Set(preds[b])];
|
|
797
|
+
// the distinct successor blocks of each, built on first ask (`readAfterEveryReachingWrite`)
|
|
798
|
+
let successorsMemo: number[][] | undefined;
|
|
799
|
+
const successors = (): number[][] => {
|
|
800
|
+
if (successorsMemo === undefined) {
|
|
801
|
+
const succ: number[][] = irBlocks.map(() => []);
|
|
802
|
+
irBlocks.forEach((_, s) => distinctPreds(s).forEach((p) => succ[p].push(s)));
|
|
803
|
+
successorsMemo = succ;
|
|
804
|
+
}
|
|
805
|
+
return successorsMemo;
|
|
806
|
+
};
|
|
699
807
|
|
|
700
808
|
// CALLER-SAVED CLOBBER, for guessed call arities (see trimClobberedCallArgs). Tracked HERE
|
|
701
809
|
// because every register write in every frontend already goes through `writeVar`: a frontend
|
|
@@ -744,6 +852,13 @@ export function makeSsaBuilder(
|
|
|
744
852
|
// every ISA with no frontend code. Per block, because the consumer asks a per-EDGE question.
|
|
745
853
|
const writeCount: number[] = irBlocks.map(() => 0);
|
|
746
854
|
const lastWriteAt: Array<Map<string, number>> = irBlocks.map(() => new Map());
|
|
855
|
+
// the ops block `b` held at each register's last write in it (`lastWriteOp`)
|
|
856
|
+
const lastWriteOp: Array<Map<string, number>> = irBlocks.map(() => new Map());
|
|
857
|
+
// the value an instruction last read of each register block `b` read since its last write or the
|
|
858
|
+
// last call destroying it there (since the block's start where neither), and the value it first
|
|
859
|
+
// read of each before writing it or a call destroying it: `readAfterEveryReachingWrite`
|
|
860
|
+
const readSinceWrite: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
861
|
+
const readBeforeWrite: Array<Map<string, Value>> = irBlocks.map(() => new Map());
|
|
747
862
|
const writeOrder: WriteOrder = { lastWrite: new Map(), writes: new Map() };
|
|
748
863
|
fn.writeOrder = writeOrder;
|
|
749
864
|
|
|
@@ -935,6 +1050,8 @@ export function makeSsaBuilder(
|
|
|
935
1050
|
decidedLocal[b].add(reg);
|
|
936
1051
|
defs[b].set(reg, v);
|
|
937
1052
|
lastWriteAt[b].set(reg, writeCount[b]++);
|
|
1053
|
+
lastWriteOp[b].set(reg, irBlocks[b].ops.length);
|
|
1054
|
+
readSinceWrite[b].delete(reg);
|
|
938
1055
|
};
|
|
939
1056
|
const readVar = (reg: string, b: number): Value => {
|
|
940
1057
|
if (globalRegs.has(reg)) {
|
|
@@ -948,8 +1065,16 @@ export function makeSsaBuilder(
|
|
|
948
1065
|
}
|
|
949
1066
|
const v = readAny(reg, b);
|
|
950
1067
|
noteStaleCandidate(reg, b, v);
|
|
1068
|
+
noteRead(reg, b, v);
|
|
951
1069
|
return v;
|
|
952
1070
|
};
|
|
1071
|
+
// an instruction's read, for `readAfterEveryReachingWrite`
|
|
1072
|
+
const noteRead = (reg: string, b: number, v: Value): void => {
|
|
1073
|
+
readSinceWrite[b].set(reg, v);
|
|
1074
|
+
if (!lastWriteOp[b].has(reg) && !clobberedLocal[b].has(reg) && !readBeforeWrite[b].has(reg)) {
|
|
1075
|
+
readBeforeWrite[b].set(reg, v);
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
953
1078
|
/** The lookup, with no claim attached. The FRONTEND's read is what asserts "this register holds
|
|
954
1079
|
* this value here"; the lookups this construction makes on its own — a single predecessor's
|
|
955
1080
|
* definition, a phi's operand at each in-edge — are how that one read is answered, not further
|
|
@@ -1644,6 +1769,7 @@ export function makeSsaBuilder(
|
|
|
1644
1769
|
irBlocks,
|
|
1645
1770
|
readVar,
|
|
1646
1771
|
readGuessedArg: readAny, // see the interface: the trim is this read's answer, not a refusal
|
|
1772
|
+
noteGuessedArgRead: noteRead,
|
|
1647
1773
|
readAtEnd: (reg: string, b: number) => (filled[b] ? readAny(reg, b) : null),
|
|
1648
1774
|
writeVar,
|
|
1649
1775
|
frameWrites,
|
|
@@ -1655,6 +1781,79 @@ export function makeSsaBuilder(
|
|
|
1655
1781
|
entryValue,
|
|
1656
1782
|
holdsEntryValue,
|
|
1657
1783
|
writtenSinceCall: (reg: string, b: number) => writtenSinceCall[b].has(reg),
|
|
1784
|
+
lastWriteOp: (reg: string, b: number) => lastWriteOp[b].get(reg),
|
|
1785
|
+
readAfterEveryReachingWrite: (reg, b, { passes, accept, unlowered, answersIfRead }) => {
|
|
1786
|
+
const writes = (at: number) => lastWriteOp[at].has(reg) || clobberedLocal[at].has(reg);
|
|
1787
|
+
// the blocks whose last write of `reg` reaches `b`, and whether a path from the entry wrote none.
|
|
1788
|
+
// `b` is judged up to here: met again round a loop, its end is no write that reaches here yet
|
|
1789
|
+
const writers: number[] = [];
|
|
1790
|
+
let fromEntry = false;
|
|
1791
|
+
const back = new Set<number>([b]);
|
|
1792
|
+
const walkBack = (at: number): void => {
|
|
1793
|
+
for (const p of distinctPreds(at)) {
|
|
1794
|
+
if (back.has(p)) {
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
back.add(p);
|
|
1798
|
+
if (filled[p] && writes(p)) {
|
|
1799
|
+
writers.push(p);
|
|
1800
|
+
} else if (preds[p].length === 0) {
|
|
1801
|
+
fromEntry = true;
|
|
1802
|
+
} else {
|
|
1803
|
+
walkBack(p);
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
walkBack(b);
|
|
1808
|
+
const succs = (at: number): number[] => successors()[at].filter((s) => s !== b);
|
|
1809
|
+
// forward from a write of `written` (none for the entry's): the first read on each path, until
|
|
1810
|
+
// another write ends it
|
|
1811
|
+
const ahead = (from: number, seen: Set<number>, written: Value | undefined): boolean =>
|
|
1812
|
+
succs(from).some((s) => {
|
|
1813
|
+
if (seen.has(s)) {
|
|
1814
|
+
return false;
|
|
1815
|
+
}
|
|
1816
|
+
seen.add(s);
|
|
1817
|
+
if (!filled[s]) {
|
|
1818
|
+
return written !== undefined && unlowered(written);
|
|
1819
|
+
}
|
|
1820
|
+
const read = readBeforeWrite[s].get(reg);
|
|
1821
|
+
if (read !== undefined) {
|
|
1822
|
+
return accept(read);
|
|
1823
|
+
}
|
|
1824
|
+
return !writes(s) && ahead(s, seen, written);
|
|
1825
|
+
});
|
|
1826
|
+
const readAfter = (w: number): boolean => {
|
|
1827
|
+
const read = readSinceWrite[w].get(reg);
|
|
1828
|
+
return read !== undefined ? accept(read) : ahead(w, new Set(), defs[w].get(reg));
|
|
1829
|
+
};
|
|
1830
|
+
// a call's destroying it is no write of this function's: it neither counts nor answers; and a
|
|
1831
|
+
// write of a value `answersIfRead` takes answers only where it was read after
|
|
1832
|
+
const own = writers.filter((w) => {
|
|
1833
|
+
const v = defs[w].get(reg)!;
|
|
1834
|
+
return !clobberedLocal[w].has(reg) && passes(v) && (!answersIfRead(v) || readAfter(w));
|
|
1835
|
+
});
|
|
1836
|
+
// nor does the entry's value unread: nothing wrote it for the call. Read, it answers by its
|
|
1837
|
+
// value too (`unlowered`): no write of this function's made it, so no path out of one places
|
|
1838
|
+
// the read, and a path from the entry into a block not lowered yet holds reads nobody has seen
|
|
1839
|
+
const param = irBlocks[0].params.find((v) => paramReg.get(v) === reg);
|
|
1840
|
+
const entryRead =
|
|
1841
|
+
fromEntry &&
|
|
1842
|
+
((param !== undefined && unlowered(param)) ||
|
|
1843
|
+
(readBeforeWrite[0].has(reg) ? accept(readBeforeWrite[0].get(reg)!) : ahead(0, new Set(), undefined)));
|
|
1844
|
+
// where a call destroyed it on every path in, it holds what a callee left there and nothing
|
|
1845
|
+
// this function named: no argument, whatever an earlier lookup's phi of the bytes before
|
|
1846
|
+
// those calls says (counted, sbk func_800B6E5C_A6D0C's a1 and a2 would be minted as entry
|
|
1847
|
+
// parameters the trim cuts from the call but leaves in the signature)
|
|
1848
|
+
if (!fromEntry && writers.length > 0 && writers.every((w) => clobberedLocal[w].has(reg))) {
|
|
1849
|
+
return true;
|
|
1850
|
+
}
|
|
1851
|
+
// no write of this function's left to judge, and no entry value read: nothing to answer
|
|
1852
|
+
if (own.length === 0 && !entryRead) {
|
|
1853
|
+
return undefined;
|
|
1854
|
+
}
|
|
1855
|
+
return own.every(readAfter);
|
|
1856
|
+
},
|
|
1658
1857
|
writtenOnEveryPath: (reg, b, stop, visit) => {
|
|
1659
1858
|
// `b` is judged up to here on the first visit; met again round a loop, it is the end of `b`,
|
|
1660
1859
|
// which a back edge brings, and a cycle adds no path the first visit did not judge
|
|
@@ -1702,6 +1901,7 @@ export function makeSsaBuilder(
|
|
|
1702
1901
|
for (const r of clobbers) {
|
|
1703
1902
|
clobberedLocal[b].add(r);
|
|
1704
1903
|
decidedLocal[b].add(r);
|
|
1904
|
+
readSinceWrite[b].delete(r);
|
|
1705
1905
|
}
|
|
1706
1906
|
imageLocal[b] = new Set();
|
|
1707
1907
|
},
|
|
@@ -2108,6 +2308,27 @@ export function fallbackArgc(
|
|
|
2108
2308
|
* ahead of the idiom and `bl h; mov r0,#1; bl g` in a case lifts `g(1)` — the gap rule's
|
|
2109
2309
|
* `setUp` walks through calls and would count r3's copy, which `h` destroyed. */
|
|
2110
2310
|
parked?: (reg: string) => boolean;
|
|
2311
|
+
/** how many argument words the callee's own body reads at most (frontend/callee-body.ts),
|
|
2312
|
+
* asked only where it can answer something. It is an upper bound on what the callee READS, and
|
|
2313
|
+
* a source call passes more than that whenever a parameter goes unused or a K&R call hands
|
|
2314
|
+
* extra arguments — `setx(a + 1, 4)` to a body reading only its first, `rd1(a, b, 7)` to one
|
|
2315
|
+
* reading r0 — so it never caps what the caller set up for the call: such a register stays
|
|
2316
|
+
* counted, and a value above a gap still decides the gap below it (`decide`). It answers only
|
|
2317
|
+
* for registers at or past it, whose being arguments changes nothing the callee computes:
|
|
2318
|
+
* - one that is a `leftover` stops the count: a write something else read since, which the
|
|
2319
|
+
* call found there (`gH = 5; gA = rnd()` leaves the store's 5 in r0, which the guess took as
|
|
2320
|
+
* `rnd(5)`). A copy made for the call is none, whatever read its value before
|
|
2321
|
+
* (`g4(a, b, c, d); un(a, b, c, d)` copies a..d back from their homes for `un`);
|
|
2322
|
+
* - a gap starting there, under a register that is a leftover or that no proof makes an
|
|
2323
|
+
* argument, stops the count at the gap rather than refuse.
|
|
2324
|
+
* A gap BELOW the bound refuses as ever, whatever lies above it: the callee may read it, and it
|
|
2325
|
+
* holds this function's own incoming value or nothing. `gp = 3; gq = rd1(a)` leaves the store's 3
|
|
2326
|
+
* in r1 above r0's a, and stopping there would drop the `a` rd1 reads. */
|
|
2327
|
+
bound?: () => number | undefined;
|
|
2328
|
+
/** whether what reaches argument register `reg` was written for something other than the call,
|
|
2329
|
+
* which read it since, and left there (frontend/call-plan.ts `leftover`). Asked only at or past
|
|
2330
|
+
* `bound`. */
|
|
2331
|
+
leftover?: (reg: string) => boolean;
|
|
2111
2332
|
},
|
|
2112
2333
|
): number {
|
|
2113
2334
|
const accept = opts.accept ?? (() => true);
|
|
@@ -2121,9 +2342,16 @@ export function fallbackArgc(
|
|
|
2121
2342
|
const top = parkedAt < 0 ? argRegs.length : parkedAt;
|
|
2122
2343
|
// the run of argument registers the caller set up from `from`, each past a declaration's
|
|
2123
2344
|
// stated words asked whether it still holds the incoming value
|
|
2345
|
+
// whether argument register `k` lies at or past what the callee's own body reads
|
|
2346
|
+
const unread = (k: number): boolean => {
|
|
2347
|
+
const b = opts.bound?.();
|
|
2348
|
+
return b !== undefined && k >= b;
|
|
2349
|
+
};
|
|
2350
|
+
// past the bound, a leftover is no argument (`bound`): leftover first, the cheaper question
|
|
2351
|
+
const notPassed = (k: number): boolean => opts.leftover?.(argRegs[k]) === true && unread(k);
|
|
2124
2352
|
const run = (from: number): number => {
|
|
2125
2353
|
let n = from;
|
|
2126
|
-
while (n < top && setUp(n)) {
|
|
2354
|
+
while (n < top && setUp(n) && !notPassed(n)) {
|
|
2127
2355
|
// PAST A DECLARATION'S STATED WORDS, A REGISTER STILL HOLDING ITS INCOMING VALUE REFUSES. A
|
|
2128
2356
|
// read of it (a home store, a compare, a store to a global) leaves the entry parameter behind
|
|
2129
2357
|
// as a definition, so it reads as set up, and it is exactly as undecidable as the gap below: the
|
|
@@ -2151,7 +2379,13 @@ export function fallbackArgc(
|
|
|
2151
2379
|
}
|
|
2152
2380
|
return n;
|
|
2153
2381
|
};
|
|
2382
|
+
// where the run stopped at a register set up but no argument (`bound`), every one above is past
|
|
2383
|
+
// the bound too, and the count ends there
|
|
2384
|
+
const stoppedPast = (n: number): boolean => n < top && setUp(n);
|
|
2154
2385
|
let n = run(floor);
|
|
2386
|
+
if (stoppedPast(n)) {
|
|
2387
|
+
return n;
|
|
2388
|
+
}
|
|
2155
2389
|
// A GAP BELOW A REGISTER THAT HOLDS A VALUE refuses, unless the frontend decides it: then the run
|
|
2156
2390
|
// goes on through the gap and the register above it, and the next gap is asked the same.
|
|
2157
2391
|
//
|
|
@@ -2176,15 +2410,30 @@ export function fallbackArgc(
|
|
|
2176
2410
|
`value and ${argRegs[gapAt]} holds none — an argument register left at its incoming value and one ` +
|
|
2177
2411
|
`the call does not pass look the same here, so the argument count is not decidable`,
|
|
2178
2412
|
);
|
|
2413
|
+
// the gap and the register above it are past what the callee reads: no argument it can tell
|
|
2414
|
+
// from a missing one, so the count stops below the gap as at a gapless call
|
|
2415
|
+
if (opts.leftover?.(argRegs[k]) === true && unread(gapAt)) {
|
|
2416
|
+
return gapAt;
|
|
2417
|
+
}
|
|
2179
2418
|
if (!opts.decide?.(argRegs.slice(n, k), argRegs[k], refuse)) {
|
|
2419
|
+
if (unread(gapAt)) {
|
|
2420
|
+
return gapAt;
|
|
2421
|
+
}
|
|
2180
2422
|
refuse();
|
|
2181
2423
|
}
|
|
2182
2424
|
n = run(k);
|
|
2425
|
+
const stopped = stoppedPast(n);
|
|
2183
2426
|
for (let above = k + 1; above < n; above++) {
|
|
2184
2427
|
if (!opts.decide?.([], argRegs[above], refuse)) {
|
|
2428
|
+
if (unread(above)) {
|
|
2429
|
+
return above;
|
|
2430
|
+
}
|
|
2185
2431
|
refuse();
|
|
2186
2432
|
}
|
|
2187
2433
|
}
|
|
2434
|
+
if (stopped) {
|
|
2435
|
+
return n;
|
|
2436
|
+
}
|
|
2188
2437
|
k = n;
|
|
2189
2438
|
}
|
|
2190
2439
|
}
|
|
@@ -2909,8 +3158,21 @@ export function abiSortEntryParams(
|
|
|
2909
3158
|
assertTrueEntry(entryHasPreds);
|
|
2910
3159
|
const rank = (v: Value): number => slots.slotOf(paramReg.get(v) ?? '') ?? NOT_AN_ARGUMENT;
|
|
2911
3160
|
entry.params.sort((x, y) => rank(x) - rank(y));
|
|
3161
|
+
for (const v of entry.params) {
|
|
3162
|
+
const r = rank(v);
|
|
3163
|
+
if (r !== NOT_AN_ARGUMENT) {
|
|
3164
|
+
argRanks.set(v, r);
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
2912
3167
|
}
|
|
2913
3168
|
|
|
3169
|
+
/** The ABI argument rank each entry parameter arrived in, as {@link abiSortEntryParams} ranked it. */
|
|
3170
|
+
const argRanks = new WeakMap<Value, number>();
|
|
3171
|
+
|
|
3172
|
+
/** The argument rank (registers counted, then incoming stack words) entry parameter `v` arrived in,
|
|
3173
|
+
* or undefined for a live-in that is no argument slot, or a value no entry sort ranked. */
|
|
3174
|
+
export const entryArgRank = (v: Value): number | undefined => argRanks.get(v);
|
|
3175
|
+
|
|
2914
3176
|
function assertTrueEntry(entryHasPreds: boolean): void {
|
|
2915
3177
|
if (entryHasPreds) {
|
|
2916
3178
|
throw new Error('internal: the entry-parameter rule needs an entry block with no predecessors');
|
|
@@ -39,8 +39,9 @@ export interface StackArgsEscape {
|
|
|
39
39
|
/** A call, and what its callee's DECLARATION says it takes on the stack. `declared` is the block
|
|
40
40
|
* `[0, 4*(n - argRegs))` as an ascending offset list, or null when nothing declares this call —
|
|
41
41
|
* an indirect call, a callee with no prototype, or one whose arity fits in registers. The code
|
|
42
|
-
* alone cannot say where a null declaration's block ENDS, so such a call is
|
|
43
|
-
*
|
|
42
|
+
* alone cannot say where a null declaration's block ENDS, so such a call is handed a stack word
|
|
43
|
+
* only where its callee's body says where it starts (`bodyReads`); otherwise it lifts only when
|
|
44
|
+
* `loadedWordBound` clears every word that could open one. */
|
|
44
45
|
export interface StackArgsCall<C> {
|
|
45
46
|
readonly kind: 'call';
|
|
46
47
|
/** The frontend's own handle for this call — the key of `OutgoingArgs.blocks`. */
|
|
@@ -51,6 +52,11 @@ export interface StackArgsCall<C> {
|
|
|
51
52
|
/** The parameters the declaration lists, for the refusal messages — fewer than its words when a
|
|
52
53
|
* 64-bit parameter takes two or a hidden struct-return pointer takes one. Absent, one per word. */
|
|
53
54
|
readonly params?: number;
|
|
55
|
+
/** Where nothing declares the call: one past the highest word of its incoming stack area the
|
|
56
|
+
* callee's own body is proven to load (frontend/callee-body.ts `CalleeStackWords`), a LOWER bound
|
|
57
|
+
* on its stack words — asked, since it may lift the callee, only where [sp,#0] is stored and never
|
|
58
|
+
* loaded (`analyzeOutgoingArgs`). Absent, or answering 0 or nothing, the body proves nothing. */
|
|
59
|
+
readonly bodyReads?: () => number | undefined;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
/** One basic block's events. Where control goes afterwards is read out of `preds`, so a block is
|
|
@@ -209,8 +215,33 @@ export function loadedWordBound<C>(
|
|
|
209
215
|
//
|
|
210
216
|
// A LICENSED CALL CONSUMES ITS BLOCK, which is what lets a function make several calls: the
|
|
211
217
|
// callee reads those words, so they stop being pending after it, exactly as a reload would end
|
|
212
|
-
// them. The deletion is driven by the DECLARATION alone, never by the
|
|
213
|
-
// cannot depend on its own outcome.
|
|
218
|
+
// them. The deletion is driven by the DECLARATION (or the body's reads, below) alone, never by the
|
|
219
|
+
// licence, so the fixpoint cannot depend on its own outcome.
|
|
220
|
+
//
|
|
221
|
+
// THE CALLEE'S BODY STANDS IN FOR A MISSING DECLARATION, on one side only. A compiled callee loads
|
|
222
|
+
// incoming stack word k only as its argument |argRegs|+k, so a body proven to load word n-1
|
|
223
|
+
// (`bodyReads` n) takes at least n stack words — a LOWER bound, as a declaration is, and never an
|
|
224
|
+
// upper one: a parameter it does not load, or one it loads past the straight-line entry it was
|
|
225
|
+
// scanned over, is a word it takes all the same. The CODE closes the block from above: the call is
|
|
226
|
+
// licensed only where what reaches it staged, on every path (must) and on some path (may, beneath
|
|
227
|
+
// the loaded-word bound), is exactly `[0, 4n)` — by CONTIGUITY a conforming caller stores every
|
|
228
|
+
// argument word of a call, so a word past the body's reads that it does not stage is no argument.
|
|
229
|
+
// Anything else is the call it was before the witness existed: unlicensed, and refused by (a) or
|
|
230
|
+
// (b) wherever they refused it — so the witness only ever turns a decline into a lift, and only at
|
|
231
|
+
// a call [sp,#0] reached staged, which (b) refused.
|
|
232
|
+
/** THE BODY'S LICENCE, the one rule both ISAs that stage stack arguments decide by (this file's
|
|
233
|
+
* dataflow for Thumb, frontend/mips.ts `lowerJal` for O32 over its SSA reaching definitions): a
|
|
234
|
+
* call nothing declares, whose callee's body loads `n` incoming stack words, takes exactly those
|
|
235
|
+
* `n` where the words staged for it are exactly `[0, 4n)` from the start of the outgoing area —
|
|
236
|
+
* `may` the words that reach it pending on SOME path (beneath the loaded-word bound), and `must`
|
|
237
|
+
* whether a word reaches it pending on EVERY path. Pending means stored and not consumed since: a
|
|
238
|
+
* load, or an earlier call whose block held the word, ends it. Offsets are relative to the area's
|
|
239
|
+
* start (0 on agbcc, 16 on O32). */
|
|
240
|
+
export function bodyBlockAgrees(n: number, may: Iterable<number>, must: (off: number) => boolean): boolean {
|
|
241
|
+
const staged = [...may].sort((x, y) => x - y);
|
|
242
|
+
return n > 0 && staged.length === n && staged.every((o, i) => o === 4 * i && must(o));
|
|
243
|
+
}
|
|
244
|
+
|
|
214
245
|
export function analyzeOutgoingArgs<C>({
|
|
215
246
|
blocks: asmBlocks,
|
|
216
247
|
preds,
|
|
@@ -221,6 +252,26 @@ export function analyzeOutgoingArgs<C>({
|
|
|
221
252
|
loadedWordBound: bound,
|
|
222
253
|
}: StackArgsInput<C>): OutgoingArgs<C> {
|
|
223
254
|
const refuse = (blocker: string): OutgoingArgs<C> => ({ blocker, blocks: new Map(), area: 0 });
|
|
255
|
+
// the block the callee's body proves it reads into, for an undeclared call (`bodyReads`), asked
|
|
256
|
+
// only where [sp,#0] is stored and never loaded: every block a licence names starts there, and
|
|
257
|
+
// a load of it puts the loaded-word bound at 0, so no word reaches any call beneath it. A fact of
|
|
258
|
+
// the code, not of the dataflow, so the fixpoint below never decides which calls ask.
|
|
259
|
+
const events = asmBlocks.flatMap((ab) => ab.events);
|
|
260
|
+
const asksBodies =
|
|
261
|
+
events.some((e) => e.kind === 'store' && e.off === 0) && !events.some((e) => e.kind === 'load' && e.off === 0);
|
|
262
|
+
const bodyWords = new Map<StackArgsCall<C>, number[]>();
|
|
263
|
+
const bodyBlock = (ev: StackArgsCall<C>): number[] => {
|
|
264
|
+
if (ev.declared !== null || ev.bodyReads === undefined || !asksBodies) {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
if (!bodyWords.has(ev)) {
|
|
268
|
+
bodyWords.set(
|
|
269
|
+
ev,
|
|
270
|
+
Array.from({ length: ev.bodyReads() ?? 0 }, (_, i) => 4 * i),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
return bodyWords.get(ev)!;
|
|
274
|
+
};
|
|
224
275
|
// EVERY block, not the entry-reachable ones: a call in dead code stages nothing, so the
|
|
225
276
|
// dataflow below never finds its block and it refuses — which is the verdict it had before
|
|
226
277
|
// consumption existed. An unreachable `bl` is not evidence about the frame either way, and
|
|
@@ -235,6 +286,7 @@ export function analyzeOutgoingArgs<C>({
|
|
|
235
286
|
return { blocker: null, blocks: new Map(), area: 0 };
|
|
236
287
|
}
|
|
237
288
|
const say = (offs: readonly number[]) => offs.map((o) => `[sp,#${o}]`).join(', ');
|
|
289
|
+
const sayAll = (offs: readonly number[]) => (offs.length === 0 ? 'none' : say(offs));
|
|
238
290
|
const declaredAs = (ev: StackArgsCall<C>, offs: readonly number[]) => {
|
|
239
291
|
const words = argRegs + offs.length;
|
|
240
292
|
return ev.params === undefined || ev.params === words
|
|
@@ -331,7 +383,7 @@ export function analyzeOutgoingArgs<C>({
|
|
|
331
383
|
mayAt.set(ev, belowBound(may));
|
|
332
384
|
mustAt.set(ev, new Set(must));
|
|
333
385
|
storedAt.set(ev, new Set(stored));
|
|
334
|
-
for (const o of ev.declared ??
|
|
386
|
+
for (const o of ev.declared ?? bodyBlock(ev)) {
|
|
335
387
|
may.delete(o);
|
|
336
388
|
must.delete(o);
|
|
337
389
|
}
|
|
@@ -392,6 +444,13 @@ export function analyzeOutgoingArgs<C>({
|
|
|
392
444
|
for (const ev of calls) {
|
|
393
445
|
const offs = ev.declared;
|
|
394
446
|
if (offs === null) {
|
|
447
|
+
// the body's witness: licensed on exact agreement, else left to (a) and (b) as before
|
|
448
|
+
const body = bodyBlock(ev);
|
|
449
|
+
const must = mustAt.get(ev) ?? new Set<number>();
|
|
450
|
+
if (bodyBlockAgrees(body.length, mayAt.get(ev) ?? [], (o) => must.has(o))) {
|
|
451
|
+
blocks.set(ev.call, body);
|
|
452
|
+
area = Math.max(area, 4 * body.length);
|
|
453
|
+
}
|
|
395
454
|
continue;
|
|
396
455
|
}
|
|
397
456
|
const may = mayAt.get(ev) ?? new Set<number>();
|
|
@@ -429,7 +488,7 @@ export function analyzeOutgoingArgs<C>({
|
|
|
429
488
|
const clash = offs.find((o) => o >= lowestEscape);
|
|
430
489
|
if (clash !== undefined) {
|
|
431
490
|
return refuse(
|
|
432
|
-
`callee \`${ev.callee}\` is declared with ${declaredAs(ev, offs)}, so [sp,#${clash}] is its outgoing stack argument — ` +
|
|
491
|
+
`callee \`${ev.callee}\` ${ev.declared === null ? `loads ${offs.length} incoming stack words in its body` : `is declared with ${declaredAs(ev, offs)}`}, so [sp,#${clash}] is its outgoing stack argument — ` +
|
|
433
492
|
`but the frame address [sp,#${lowestEscape}] escapes this function, so the words from there up are an object's, and the two name the same word`,
|
|
434
493
|
);
|
|
435
494
|
}
|
|
@@ -481,6 +540,33 @@ export function analyzeOutgoingArgs<C>({
|
|
|
481
540
|
);
|
|
482
541
|
}
|
|
483
542
|
}
|
|
543
|
+
// (b)'s question, asked of one call (see (b) below): the refusal, or null where nothing staged
|
|
544
|
+
// there could open an argument block. A call the body's witness was asked of and did not license
|
|
545
|
+
// is asked it BEFORE (a), so its decline names both witnesses; (a) would refuse it anyway, or (b)
|
|
546
|
+
// would, so only the message moves.
|
|
547
|
+
const pendingAtUndeclared = (ev: StackArgsCall<C>): string | null => {
|
|
548
|
+
if (ev.declared !== null || blocks.has(ev.call)) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
const may = mayAt.get(ev) ?? new Set<number>();
|
|
552
|
+
const stored = storedAt.get(ev) ?? new Set<number>();
|
|
553
|
+
const body = bodyBlock(ev);
|
|
554
|
+
const why =
|
|
555
|
+
body.length === 0
|
|
556
|
+
? ''
|
|
557
|
+
: ` (its body loads ${body.length} incoming stack word${body.length === 1 ? '' : 's'}, ${say(body)}, ` +
|
|
558
|
+
`but the words staged for it are ${sayAll(asc(may))}, on every path ${sayAll(asc(mustAt.get(ev) ?? []))})`;
|
|
559
|
+
const k = asc(may).find((o) => prefixStored(o, stored));
|
|
560
|
+
return k === undefined
|
|
561
|
+
? null
|
|
562
|
+
: `the store to [sp,#${k}] reaches \`bl ${ev.callee}\` unread with its lower slots supplied — it may be that call's outgoing stack argument${why}`;
|
|
563
|
+
};
|
|
564
|
+
for (const ev of calls) {
|
|
565
|
+
const refusal = bodyBlock(ev).length > 0 ? pendingAtUndeclared(ev) : null;
|
|
566
|
+
if (refusal !== null) {
|
|
567
|
+
return refuse(refusal);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
484
570
|
// (a) — a store never reloaded ANYWHERE, with its lower slots supplied, is an argument's
|
|
485
571
|
// signature: an outgoing argument is read by the CALLEE, never by the caller. Its real theorem
|
|
486
572
|
// is the layout one (the area sits at the BOTTOM of localArea, disjoint from the locals, so no
|
|
@@ -504,17 +590,9 @@ export function analyzeOutgoingArgs<C>({
|
|
|
504
590
|
// histogram keys on, and scanning a Set in insertion order named whichever offset the code stored
|
|
505
591
|
// FIRST instead (pokeemerald's `PickLotteryCornerTicket` stores [sp,#4] before [sp,#0]).
|
|
506
592
|
for (const ev of calls) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const may = mayAt.get(ev) ?? new Set<number>();
|
|
511
|
-
const stored = storedAt.get(ev) ?? new Set<number>();
|
|
512
|
-
for (const k of asc(may)) {
|
|
513
|
-
if (prefixStored(k, stored)) {
|
|
514
|
-
return refuse(
|
|
515
|
-
`the store to [sp,#${k}] reaches \`bl ${ev.callee}\` unread with its lower slots supplied — it may be that call's outgoing stack argument`,
|
|
516
|
-
);
|
|
517
|
-
}
|
|
593
|
+
const refusal = pendingAtUndeclared(ev);
|
|
594
|
+
if (refusal !== null) {
|
|
595
|
+
return refuse(refusal);
|
|
518
596
|
}
|
|
519
597
|
}
|
|
520
598
|
// NOTHING LEFT OVER. The exclusion above is per OFFSET, so it would also excuse a store to a
|