@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
package/src/ir/core.ts
CHANGED
|
@@ -59,8 +59,57 @@ export interface Fn {
|
|
|
59
59
|
/** L1 SIDE DATA (see {@link LocalObjects}); set by a frontend that recovered one, `undefined` on
|
|
60
60
|
* every other `Fn`. REQUIRED-but-possibly-undefined for exactly the reason `writeOrder` is. */
|
|
61
61
|
localObjects: LocalObjects | undefined;
|
|
62
|
+
/** L1 SIDE DATA (see {@link AsmLayout}); set by a frontend, `undefined` on parsed IR.
|
|
63
|
+
* REQUIRED-but-possibly-undefined for exactly the reason `writeOrder` is. */
|
|
64
|
+
asmLayout: AsmLayout | undefined;
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
/** One block's place in the assembly a frontend lifted it from ({@link AsmLayout}). */
|
|
68
|
+
export interface AsmBlockLayout {
|
|
69
|
+
/** the block's position among the function's blocks in the asm text, first block 0 */
|
|
70
|
+
readonly index: number;
|
|
71
|
+
/** the block's instructions without its control transfer, one string per instruction as the
|
|
72
|
+
* asm spells it (`mnemonic op,op`), followed by `<sym>` (`<sym+0xN>` with an addend) where a
|
|
73
|
+
* relocation fills in a symbol the operands do not name (frontend/ssa.ts `recordAsmLayout`). A
|
|
74
|
+
* MIPS delay slot is part of the block it runs in. */
|
|
75
|
+
readonly body: readonly string[];
|
|
76
|
+
/** the positions in `body` of the instructions that make a call, as the frontend's ISA reads them
|
|
77
|
+
* (Thumb `bl`/`blx` but not a far-jump relay, MIPS `jal`/`jalr`/`bal`, PPC `bl`/`bctrl`/`blrl`) */
|
|
78
|
+
readonly calls: readonly number[];
|
|
79
|
+
/** the block ends in an unconditional jump to a label: neither a fall-through, nor a return,
|
|
80
|
+
* nor a conditional or indirect branch */
|
|
81
|
+
readonly endsInJump: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** WHERE THE COMPILER PUT EACH BLOCK, and what it holds besides its branch: a measurement only the
|
|
85
|
+
* frontend can make. The IR keeps the CFG and drops the order, since `fn.blocks` is a list passes
|
|
86
|
+
* are free to rewrite (raise/reroll.ts moves a block ahead of another, and inserts one).
|
|
87
|
+
*
|
|
88
|
+
* The reader is the return-tail chooser (structure/cross-jumped-tail.ts). Where the compiler
|
|
89
|
+
* cross-jumps return tails (target.ts `crossJumpsReturnTails`), the layout tells a tail the source
|
|
90
|
+
* wrote once from one it wrote into every arm, and the CFG cannot. Keyed by Block OBJECT, so a
|
|
91
|
+
* clone re-keys it (cli/src/report.ts `structuredCloneFn`). A block a pass created has no entry,
|
|
92
|
+
* and every reader takes a missing entry as "no evidence". Recorded by the Thumb and MIPS
|
|
93
|
+
* frontends only: no mark is read on mwcc (it declares neither `crossJumpsReturnTails` nor an
|
|
94
|
+
* unfolded taken-edge constant), so frontend/ppc.ts records none until one is, and a PPC lift's
|
|
95
|
+
* `asmLayout` is undefined.
|
|
96
|
+
*
|
|
97
|
+
* AN ENTRY DESCRIBES THE ASM BLOCK THE IR BLOCK WAS LIFTED FROM, whatever raising later did to the
|
|
98
|
+
* IR block. A return sunk into it, or ops a fold absorbed (shortcircuit, tailsink, divpow2), leave
|
|
99
|
+
* the entry as the frontend wrote it. So `endsInJump` and `body` say what the compiler emitted,
|
|
100
|
+
* not what the block holds now: over the agbcc lifts of the 620 functions the join re-spells
|
|
101
|
+
* (2026-11-05), 786 of 10520 blocks have an entry saying they end in a jump and no longer end in a
|
|
102
|
+
* `br`. The return-tail chooser reads exactly that, the asm terminator. A reader of the current
|
|
103
|
+
* block reads its ops.
|
|
104
|
+
*
|
|
105
|
+
* BUT ON MIPS THE ASM IS THE NORMALISED ASM. frontend/mips.ts records the layout after
|
|
106
|
+
* `normaliseBranchLikely`, which gives a likely branch's slot a block of its own and retargets an
|
|
107
|
+
* unconditional `b T` whose slot copies the word at T-4 to that word, emptying the slot.
|
|
108
|
+
* Such a `b`'s entry has `body` ending in `nop` where the compiler emitted the copied word
|
|
109
|
+
* (`func_80A42DB4_jp`'s `b` at 0x80A42DCC, whose slot is `sw zero,0(a3)`), so every MIPS mark
|
|
110
|
+
* reads the normalised body, not the emitted one. */
|
|
111
|
+
export type AsmLayout = Map<Block, AsmBlockLayout>;
|
|
112
|
+
|
|
64
113
|
/** Where a function-scope static lives, which decides how its definition is spelled: `rodata` is
|
|
65
114
|
* `const` with an initializer, `data` has an initializer, `bss` has none. */
|
|
66
115
|
export type LocalObjectSection = 'rodata' | 'data' | 'bss';
|
|
@@ -356,6 +405,19 @@ export function mkOp(opcode: Opcode, o: Partial<Op> = {}): Op {
|
|
|
356
405
|
};
|
|
357
406
|
}
|
|
358
407
|
|
|
408
|
+
/** An op's attributes as one comparable key, keys sorted (so insertion order never tells two
|
|
409
|
+
* equal attribute sets apart), leaving out the attribute `drop` names. */
|
|
410
|
+
export function attrsKey(op: Op, drop: string | null = null): string {
|
|
411
|
+
return JSON.stringify(
|
|
412
|
+
Object.entries(op.attrs ?? {})
|
|
413
|
+
.filter(([k]) => k !== drop)
|
|
414
|
+
.sort(([a], [b]) => a.localeCompare(b)),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** One op's identity apart from its operands: opcode, result count and attributes. */
|
|
419
|
+
export const opShape = (op: Op): string => `${op.opcode}/${op.results.length}/${attrsKey(op)}`;
|
|
420
|
+
|
|
359
421
|
// ── a call's callee: a NAME (`attrs.target`) or, for a call through a register, a VALUE ─────────
|
|
360
422
|
//
|
|
361
423
|
// An INDIRECT call carries `indirect: true` and no `target`, and calls the function whose address
|
|
@@ -438,7 +500,7 @@ export function successorsOf(b: Block): Block[] {
|
|
|
438
500
|
* keeps a walk of its own rather than calling this one — it descends through EVERY op's successors,
|
|
439
501
|
* which is the same relation on verified IR and a wider one on the hand-built IR its own note is
|
|
440
502
|
* about. */
|
|
441
|
-
export function reachableBlocks(fn:
|
|
503
|
+
export function reachableBlocks(fn: { readonly blocks: readonly Block[] }): Set<Block> {
|
|
442
504
|
const seen = new Set<Block>();
|
|
443
505
|
const entry = fn.blocks[0];
|
|
444
506
|
if (entry === undefined) {
|
|
@@ -484,7 +546,7 @@ export function reachForward(
|
|
|
484
546
|
}
|
|
485
547
|
|
|
486
548
|
/** Predecessor map for the whole function's CFG. */
|
|
487
|
-
export function predecessors(fn:
|
|
549
|
+
export function predecessors(fn: { readonly blocks: readonly Block[] }): Map<Block, Block[]> {
|
|
488
550
|
const preds = new Map<Block, Block[]>();
|
|
489
551
|
for (const b of fn.blocks) {
|
|
490
552
|
preds.set(b, []);
|
|
@@ -503,7 +565,7 @@ export function predecessors(fn: Fn): Map<Block, Block[]> {
|
|
|
503
565
|
* `verify` needs it to check def-dominates-use, `ir/loops.ts` to tell a back-edge from a
|
|
504
566
|
* forward one, and `raise/latch.ts` to tell a latch from a preheader. */
|
|
505
567
|
|
|
506
|
-
export function dominators(fn:
|
|
568
|
+
export function dominators(fn: { readonly blocks: readonly Block[] }): Map<Block, Set<Block>> {
|
|
507
569
|
const preds = predecessors(fn);
|
|
508
570
|
const all = new Set(fn.blocks);
|
|
509
571
|
const dom = new Map<Block, Set<Block>>();
|
|
@@ -536,6 +598,94 @@ export function dominators(fn: Fn): Map<Block, Set<Block>> {
|
|
|
536
598
|
return dom;
|
|
537
599
|
}
|
|
538
600
|
|
|
601
|
+
/** Whether `h` dominates `t`, exactly as `dominators(fn).get(t).has(h)` answers it: the
|
|
602
|
+
* same data-flow (dom(b) = {b} ∪ ⋂ dom(preds), the entry alone, every other block starting from all
|
|
603
|
+
* of them), iterated to the same greatest fixed point, over one bit per block instead of a `Set`
|
|
604
|
+
* per block. raise/shortcircuit.ts's `naturalLoopBodies` asks it once per scan of a function its fold
|
|
605
|
+
* keeps changing, where the sets cost a large function's lift most of its raising time. */
|
|
606
|
+
export function dominance(fn: { readonly blocks: readonly Block[] }): (h: Block, t: Block) => boolean {
|
|
607
|
+
const n = fn.blocks.length;
|
|
608
|
+
const words = Math.ceil(n / 32);
|
|
609
|
+
const index = new Map<Block, number>();
|
|
610
|
+
fn.blocks.forEach((b, i) => index.set(b, i));
|
|
611
|
+
const preds = predecessors(fn);
|
|
612
|
+
const dom = fn.blocks.map((_, i) => {
|
|
613
|
+
const bits = new Uint32Array(words);
|
|
614
|
+
if (i === 0) {
|
|
615
|
+
bits[0] = 1;
|
|
616
|
+
} else {
|
|
617
|
+
bits.fill(0xffffffff);
|
|
618
|
+
if (n % 32 !== 0) {
|
|
619
|
+
bits[words - 1] = 2 ** (n % 32) - 1;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return bits;
|
|
623
|
+
});
|
|
624
|
+
const next = new Uint32Array(words);
|
|
625
|
+
for (let changed = true; changed;) {
|
|
626
|
+
changed = false;
|
|
627
|
+
for (let i = 1; i < n; i++) {
|
|
628
|
+
const ps = preds.get(fn.blocks[i])!;
|
|
629
|
+
if (ps.length === 0) {
|
|
630
|
+
next.fill(0);
|
|
631
|
+
} else {
|
|
632
|
+
next.set(dom[index.get(ps[0])!]);
|
|
633
|
+
for (let k = 1; k < ps.length; k++) {
|
|
634
|
+
const dp = dom[index.get(ps[k])!];
|
|
635
|
+
for (let w = 0; w < words; w++) {
|
|
636
|
+
next[w] &= dp[w];
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
next[i >>> 5] |= 1 << (i & 31);
|
|
641
|
+
const prev = dom[i];
|
|
642
|
+
let same = true;
|
|
643
|
+
for (let w = 0; w < words; w++) {
|
|
644
|
+
if (next[w] !== prev[w]) {
|
|
645
|
+
same = false;
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (!same) {
|
|
650
|
+
prev.set(next);
|
|
651
|
+
changed = true;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return (h, t) => {
|
|
656
|
+
const hi = index.get(h);
|
|
657
|
+
const ti = index.get(t);
|
|
658
|
+
return hi !== undefined && ti !== undefined && (dom[ti][hi >>> 5] & (1 << (hi & 31))) !== 0;
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Each block's IMMEDIATE dominator: of its strict `dominators`, which form a chain, the nearest —
|
|
663
|
+
* the one every other dominates, so the one with the most dominators of its own. Read off those
|
|
664
|
+
* sets rather than computed again, so the tree and the sets cannot disagree; but off the sets of
|
|
665
|
+
* the blocks the entry reaches (`reachableBlocks`) alone, because `dominators` counts an edge
|
|
666
|
+
* out of an unreachable block as a way in, and one such edge into a loop's join leaves the join
|
|
667
|
+
* dominated by nothing but itself. The entry, and every unreachable block, have none.
|
|
668
|
+
* `ir/bits.ts`'s `edgeFacts` walks this tree up from a point to read what the branches above it
|
|
669
|
+
* prove there. */
|
|
670
|
+
export function immediateDominators(fn: { readonly blocks: readonly Block[] }): Map<Block, Block> {
|
|
671
|
+
const live = reachableBlocks(fn);
|
|
672
|
+
const blocks = fn.blocks.filter((b) => live.has(b));
|
|
673
|
+
const dom = dominators({ blocks });
|
|
674
|
+
const out = new Map<Block, Block>();
|
|
675
|
+
for (const b of blocks.slice(1)) {
|
|
676
|
+
let near: Block | undefined;
|
|
677
|
+
for (const d of dom.get(b)!) {
|
|
678
|
+
if (d !== b && (near === undefined || dom.get(d)!.size > dom.get(near)!.size)) {
|
|
679
|
+
near = d;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (near !== undefined) {
|
|
683
|
+
out.set(b, near);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return out;
|
|
687
|
+
}
|
|
688
|
+
|
|
539
689
|
/** A block with NO BODY OF ITS OWN: it declares no parameters and holds a single op, which is
|
|
540
690
|
* therefore its terminator. It computes nothing and it binds nothing. Three sites ask this and
|
|
541
691
|
* each adds its own clause (`forwardingTarget` below wants a `br` carrying no args;
|
|
@@ -605,7 +755,7 @@ export function forwardingTarget(b: Block): Block {
|
|
|
605
755
|
* a CFG fact a parameter for one caller's extra edges — so it stays where it is, and what this
|
|
606
756
|
* paragraph buys is that the next reader looking for a fourth copy knows which of the two the
|
|
607
757
|
* third one is. */
|
|
608
|
-
export function mergeClasses(fn:
|
|
758
|
+
export function mergeClasses(fn: { readonly blocks: readonly Block[] }): Map<Value, readonly Value[]> {
|
|
609
759
|
const parent = new Map<Value, Value>();
|
|
610
760
|
const find = (v: Value): Value => {
|
|
611
761
|
let r = v;
|
package/src/ir/entry.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// `base + c` as the same load off `base` at its offset plus `c` (`str[0]` against `str[i]` at
|
|
22
22
|
// `i = 0`). Each is an identity of the integer operators, so turning it on for guard fusion is a
|
|
23
23
|
// spelling decision, not a soundness one.
|
|
24
|
-
import { Op, Value } from './core';
|
|
24
|
+
import { Op, Value, attrsKey } from './core';
|
|
25
25
|
import { spelledWhenDead } from './discipline';
|
|
26
26
|
import { NEGATED_ICMP } from './opcodes';
|
|
27
27
|
|
|
@@ -100,8 +100,6 @@ export function sameAtEntry(
|
|
|
100
100
|
}
|
|
101
101
|
return { base, off, atEntry };
|
|
102
102
|
};
|
|
103
|
-
const attrsOf = (d: Op, drop: string | null) =>
|
|
104
|
-
JSON.stringify(Object.fromEntries(Object.entries(d.attrs ?? {}).filter(([k]) => k !== drop)));
|
|
105
103
|
// memoised per (x, y, side): a value its own consumer reads twice would otherwise double the work
|
|
106
104
|
// at every level
|
|
107
105
|
const memo = new Map<string, boolean>();
|
|
@@ -115,9 +113,9 @@ export function sameAtEntry(
|
|
|
115
113
|
if (normalize && da.opcode === 'load' && db.opcode === 'load') {
|
|
116
114
|
const x = address(da, atEntry);
|
|
117
115
|
const y = address(db, true);
|
|
118
|
-
return x.off === y.off &&
|
|
116
|
+
return x.off === y.off && attrsKey(da, 'off') === attrsKey(db, 'off') && same(x.base, x.atEntry, y.base);
|
|
119
117
|
}
|
|
120
|
-
if (
|
|
118
|
+
if (attrsKey(da) !== attrsKey(db)) {
|
|
121
119
|
return false;
|
|
122
120
|
}
|
|
123
121
|
if (da.operands.every((o, i) => same(o, atEntry, db.operands[i]))) {
|
package/src/ir/opcodes.ts
CHANGED
|
@@ -204,6 +204,12 @@ export const OPCODES = {
|
|
|
204
204
|
// - a load/store through an off-0 SCALAR gaddr → a bare global `gSym` / `gSym = v`;
|
|
205
205
|
// - an indexed or non-zero-offset AGGREGATE access → the address-cast `((T *)&gSym)[i]`;
|
|
206
206
|
// - any other use (e.g. `&gSym` passed to a call) → the `{k:'addr'}` L3 node, printed `&gSym`.
|
|
207
|
+
// Optional `fromMap: true` marks a name asmlift READ OFF THE SYMBOL MAP (a promoted numeric pool
|
|
208
|
+
// word, an interior attribution, a named walk) rather than one the asm spells by relocation.
|
|
209
|
+
// With it, `mapAddr` is the map address the name was read at: a name the map places at several
|
|
210
|
+
// addresses (a file static per unit) then denotes that one, and two such reads at different
|
|
211
|
+
// addresses, one of them read off the map, are two objects under one spelling, which
|
|
212
|
+
// raise/offsetnames.ts `cellSpelledTwice` declines. Two relocations of one name are the asm's own.
|
|
207
213
|
gaddr: { operands: 0, results: 1, requiredAttrs: ['sym'] },
|
|
208
214
|
// The address of a FRAME-LOCAL object — gaddr's local twin, for the address-taken stack local
|
|
209
215
|
// (`mov rD, sp` feeding a DMA register or a callee). `off` is the byte offset inside the frame's
|
|
@@ -408,6 +414,28 @@ export const NEGATED_ICMP: Readonly<Record<string, Opcode>> = Object.fromEntries
|
|
|
408
414
|
]),
|
|
409
415
|
);
|
|
410
416
|
|
|
417
|
+
/** The comparison each `icmp_*` is with its operands SWAPPED — `a < b` is `b > a`; `==` and `!=`
|
|
418
|
+
* are their own. Not a negation: `NEGATED_ICMP` keeps the operands and flips the answer, this
|
|
419
|
+
* keeps the answer and flips the operands. Seated here for the reason `NEGATED_ICMP` is, and
|
|
420
|
+
* expanded both ways from its pairs the same way, so `swap(swap(c)) === c` by construction; a test
|
|
421
|
+
* asserts it covers the icmp family (test/pattern.test.ts). */
|
|
422
|
+
const ICMP_SWAP_PAIRS: readonly (readonly [Opcode, Opcode])[] = [
|
|
423
|
+
['icmp_slt', 'icmp_sgt'],
|
|
424
|
+
['icmp_sle', 'icmp_sge'],
|
|
425
|
+
['icmp_ult', 'icmp_ugt'],
|
|
426
|
+
['icmp_ule', 'icmp_uge'],
|
|
427
|
+
];
|
|
428
|
+
export const SWAPPED_ICMP: Readonly<Record<string, Opcode>> = {
|
|
429
|
+
icmp_eq: 'icmp_eq',
|
|
430
|
+
icmp_ne: 'icmp_ne',
|
|
431
|
+
...Object.fromEntries(
|
|
432
|
+
ICMP_SWAP_PAIRS.flatMap(([a, b]) => [
|
|
433
|
+
[a, b],
|
|
434
|
+
[b, a],
|
|
435
|
+
]),
|
|
436
|
+
),
|
|
437
|
+
};
|
|
438
|
+
|
|
411
439
|
/** The ops whose `operands[0]` is a memory-access BASE: `load base`, `store base, value`,
|
|
412
440
|
* `aload base, index`, `astore base, index, value` (the operand roles are in the registry above).
|
|
413
441
|
* Authored rather than derived — no registry field records the operand ROLE, and the only other
|
package/src/ir/parse.ts
CHANGED
package/src/ir/simplify.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Peer of `pattern/engine.ts`'s `dce`: general, opcode-agnostic, and callable by anything that has
|
|
4
4
|
// just changed the CFG.
|
|
5
|
-
import { Block, Fn, Successor, Value, replaceAllUsesWith } from './core';
|
|
5
|
+
import { Block, Fn, Op, Successor, Value, replaceAllUsesWith } from './core';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Remove block params that are really TRIVIAL PHIS — those whose incoming args, across every
|
|
@@ -55,12 +55,7 @@ export function simplifyTrivialPhis(fn: Fn, onRemoved?: (param: Value) => void):
|
|
|
55
55
|
if (collapsed === null) {
|
|
56
56
|
continue; // a genuine join, or unreachable (no in-edges at all)
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
onRemoved?.(param);
|
|
60
|
-
b.params.splice(i, 1);
|
|
61
|
-
for (const s of incoming) {
|
|
62
|
-
s.args.splice(i, 1);
|
|
63
|
-
}
|
|
58
|
+
retireParam(fn, b, i, collapsed, incoming, onRemoved);
|
|
64
59
|
removed++;
|
|
65
60
|
changed = true;
|
|
66
61
|
}
|
|
@@ -77,14 +72,70 @@ export function simplifyTrivialPhis(fn: Fn, onRemoved?: (param: Value) => void):
|
|
|
77
72
|
* fixpoint and the non-mutating check below so the two cannot come to disagree about which params
|
|
78
73
|
* are trivial.
|
|
79
74
|
*
|
|
75
|
+
* `same` is the equality the args are held to. Identity by default, which is the trivial phi
|
|
76
|
+
* proper; `raise/unpre.ts` passes structural congruence (`ir/entry.ts` `sameAtEntry`) and gets
|
|
77
|
+
* back the FIRST arg as the class's representative, which it recomputes rather than reads. The
|
|
78
|
+
* self-reference waiver is the same for both, and so is its argument above: the first dynamic
|
|
79
|
+
* entry arrives on an edge that hands the representative's class.
|
|
80
|
+
*
|
|
80
81
|
* `=== null` IS THE ONLY SAFE TEST. A genuine collapse value is never null, but it CAN be
|
|
81
82
|
* `undefined` where an edge carries fewer args than the block has params — a malformed graph the
|
|
82
83
|
* verifier catches elsewhere, and one today's callers pass straight through rather than treat as
|
|
83
84
|
* "not trivial". A truthiness test would silently change that.
|
|
84
85
|
*/
|
|
85
|
-
function trivialPhiValue(
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
export function trivialPhiValue(
|
|
87
|
+
incoming: readonly Successor[],
|
|
88
|
+
i: number,
|
|
89
|
+
param: Value,
|
|
90
|
+
same: (a: Value, b: Value) => boolean = (a, b) => a === b,
|
|
91
|
+
): Value | null {
|
|
92
|
+
const args = incoming.map((s) => s.args[i]).filter((v) => v !== param);
|
|
93
|
+
if (args.length === 0) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const first = args[0];
|
|
97
|
+
return args.every((v) => v === first || same(v, first)) ? first : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Retire block param `i` of `b` for `value`: every use reads `value`, and the param and each
|
|
101
|
+
* in-edge's arg for it are spliced out. `incoming` is every successor that targets `b` — the
|
|
102
|
+
* SUCCESSOR objects, not their `args`, which `replaceAllUsesWith` replaces. `onRemoved` runs after
|
|
103
|
+
* the uses move and before the splice. */
|
|
104
|
+
export function retireParam(
|
|
105
|
+
fn: Fn,
|
|
106
|
+
b: Block,
|
|
107
|
+
i: number,
|
|
108
|
+
value: Value,
|
|
109
|
+
incoming: readonly Successor[],
|
|
110
|
+
onRemoved?: (param: Value) => void,
|
|
111
|
+
): void {
|
|
112
|
+
const param = b.params[i];
|
|
113
|
+
replaceAllUsesWith(fn, param, value);
|
|
114
|
+
onRemoved?.(param);
|
|
115
|
+
b.params.splice(i, 1);
|
|
116
|
+
for (const s of incoming) {
|
|
117
|
+
s.args.splice(i, 1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Every in-edge, indexed by target: the successor, the block it leaves and the terminator that
|
|
122
|
+
* carries it. ONE pass over the function, for a caller whose CFG does not change while it reads the
|
|
123
|
+
* index (params and args may: the successor objects stay). */
|
|
124
|
+
export function inEdgesOf(fn: Fn): Map<Block, { succ: Successor; from: Block; op: Op }[]> {
|
|
125
|
+
const out = new Map<Block, { succ: Successor; from: Block; op: Op }[]>();
|
|
126
|
+
for (const from of fn.blocks) {
|
|
127
|
+
for (const op of from.ops) {
|
|
128
|
+
for (const succ of op.successors) {
|
|
129
|
+
const list = out.get(succ.block);
|
|
130
|
+
if (list === undefined) {
|
|
131
|
+
out.set(succ.block, [{ succ, from, op }]);
|
|
132
|
+
} else {
|
|
133
|
+
list.push({ succ, from, op });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
88
139
|
}
|
|
89
140
|
|
|
90
141
|
/**
|
package/src/ir/verify.ts
CHANGED
|
@@ -211,6 +211,7 @@ export function verify(fn: Fn): void {
|
|
|
211
211
|
// second as `u8 name[count]`, so a typed element there would be a type nothing pinned —
|
|
212
212
|
// the exact answer the audit refuses to invent. (One BYTE encodes like a real `u8`
|
|
213
213
|
// scalar either way; the declaration is the same one, so nothing has to tell them apart.)
|
|
214
|
+
// A part's element, where its own accesses pin one, rides beside them as `elem` below.
|
|
214
215
|
if (op.opcode === 'laddr') {
|
|
215
216
|
const count = op.attrs.count;
|
|
216
217
|
if (typeof count !== 'number' || !Number.isInteger(count) || count < 1) {
|
|
@@ -229,6 +230,21 @@ export function verify(fn: Fn): void {
|
|
|
229
230
|
if (op.attrs.part === true && count < 2) {
|
|
230
231
|
throw new VerifyError(`'laddr' part must be storage of two or more bytes, got ${count}`);
|
|
231
232
|
}
|
|
233
|
+
// …whose ELEMENT its accesses may agree on (frame-objects.ts `stampPartElements`): a
|
|
234
|
+
// width that tiles the part, negative for a signed narrow one, never the default `u8`
|
|
235
|
+
const elem = op.attrs.elem;
|
|
236
|
+
if (
|
|
237
|
+
elem !== undefined &&
|
|
238
|
+
(op.attrs.part !== true ||
|
|
239
|
+
op.attrs.views !== undefined ||
|
|
240
|
+
![2, 4, -1, -2].includes(elem as number) ||
|
|
241
|
+
count % Math.abs(elem as number) !== 0)
|
|
242
|
+
) {
|
|
243
|
+
throw new VerifyError(
|
|
244
|
+
`'laddr' elem is a part's element width (2, 4; -1, -2 signed) tiling its ${count} bytes, ` +
|
|
245
|
+
`got ${String(elem)}${op.attrs.part === true ? '' : ' on storage that is no part'}`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
232
248
|
// …and storage accessed at two widths over one byte names the VIEWS a union of it
|
|
233
249
|
// declares (frame-objects.ts `unionViews`): one per element width in bytes, negative
|
|
234
250
|
// for a signed narrow one, each tiling the storage — so at least two, distinct, of a
|