@asmlift/core 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- package/src/variation-tokens.ts +68 -3
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';
|
|
@@ -86,6 +135,28 @@ export interface LocalObject {
|
|
|
86
135
|
/** What an object FILE records of where the object sits, absent for an assembler listing, whose
|
|
87
136
|
* `.align` is among its directives. */
|
|
88
137
|
placement?: LocalObjectPlacement;
|
|
138
|
+
/** Every address the initializer holds, in offset order — the words a relocation fills in at
|
|
139
|
+
* link time, which the bytes alone do not carry. Absent for an object holding none. The bytes
|
|
140
|
+
* under one say nothing: the relocation's `addend` is the whole of it. */
|
|
141
|
+
relocs?: readonly LocalObjectReloc[];
|
|
142
|
+
/** The object is the compiler's own, under a label the source never wrote (agbcc's `.LC0`): the
|
|
143
|
+
* constant image a local aggregate's initializer is copied from. `name` is then the label, no
|
|
144
|
+
* declaration spells it, and it reaches the C only as that local's initializer
|
|
145
|
+
* (l3/constinit.ts). */
|
|
146
|
+
anonymous?: true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The bytes of every address an initializer holds ({@link LocalObjectReloc}): a pointer's, on every
|
|
150
|
+
* target whose initializers the frontends relocate (agbcc's `.word sym`, mwcc's `R_PPC_ADDR32`). */
|
|
151
|
+
export const LOCAL_OBJECT_ADDRESS_BYTES = 4;
|
|
152
|
+
|
|
153
|
+
/** One address an initializer holds: the pointer-width word at byte `offset` of the object is
|
|
154
|
+
* `symbol`'s address plus `addend` — an ordinary global's, the only kind a declaration outside the
|
|
155
|
+
* function can name (frontend/reloc-symbol.ts). */
|
|
156
|
+
export interface LocalObjectReloc {
|
|
157
|
+
offset: number;
|
|
158
|
+
symbol: string;
|
|
159
|
+
addend: number;
|
|
89
160
|
}
|
|
90
161
|
|
|
91
162
|
/** What an object file records of where a static sits. */
|
|
@@ -112,6 +183,12 @@ export interface LocalObjectDirectives {
|
|
|
112
183
|
/** every value directive writes a string (`.ascii`), as the compiler writes a string literal —
|
|
113
184
|
* whole, or as the elements of an array or struct of strings */
|
|
114
185
|
string: boolean;
|
|
186
|
+
/** every run of padding among the value directives, as [byte offset, byte length], when there
|
|
187
|
+
* is one: in an array, the elements its brace initializer left out — the compiler writes every
|
|
188
|
+
* element the source spelled, zero or not, as a value, and each run it left out as a `.space`
|
|
189
|
+
* (varasm.c `output_constructor`), wherever the run falls: after the last value, ahead of the
|
|
190
|
+
* first, or between two (a designator, a nested brace's short row). In a struct, alignment too. */
|
|
191
|
+
gaps?: [number, number][];
|
|
115
192
|
}
|
|
116
193
|
|
|
117
194
|
/** The function-scope statics a function DEFINES, keyed by {@link LocalObject.symbol} — the object a
|
|
@@ -135,14 +212,16 @@ export type LocalObjects = ReadonlyMap<string, LocalObject>;
|
|
|
135
212
|
* `deadHome` — THE STORE DOES NOT REACH THE IR. Every slot-modelling frontend spells a word
|
|
136
213
|
* sp-relative store as a write to the SSA key `sp@k` instead of a `store` op (`stackSlotKey`), so
|
|
137
214
|
* a slot nothing reloads has no reader, no op and no value: it simply is not there by L1. MIPS and
|
|
138
|
-
* Thumb lift such a store. PowerPC refuses it, because an unread store may be a call's
|
|
139
|
-
* stack argument
|
|
215
|
+
* Thumb lift such a store. PowerPC refuses it, because an unread slot store may be a call's
|
|
216
|
+
* outgoing stack argument; in a function that makes no call, mwcc's local area takes such a store
|
|
217
|
+
* as a store to a frame object instead of a slot (frontend/ppc.ts `measurePpcFrame`). Either way a
|
|
218
|
+
* PowerPC lift never reports a dead home. A
|
|
140
219
|
* parameter stored to two slots, one of them reloaded, still counts — the dead store happened —
|
|
141
220
|
* and the reader's own gates decide what a parameter with that much traffic may become.
|
|
142
221
|
*
|
|
143
222
|
* It asks NO FRAME PARTITION, and that is the difference from {@link SlotHomes}. A slot home is a
|
|
144
223
|
* DECLARATION RANK, so `sp@40` had to be classified as this function's local before it could be
|
|
145
|
-
* stamped, and MIPS declares no
|
|
224
|
+
* stamped, and MIPS declares no `declaredLocals` and so stamps none. "Stored here and never read back"
|
|
146
225
|
* needs no such classification: it is a statement about the store, true at any offset.
|
|
147
226
|
*
|
|
148
227
|
* `selfRedefined` — THE REGISTER IDENTITY DOES NOT REACH THE IR EITHER. SSA renames, so nothing
|
|
@@ -178,7 +257,7 @@ export type ParamEvidence = ReadonlyMap<Value, ParamObservation>;
|
|
|
178
257
|
* A KEY IS NOT A FRAME COORDINATE, and the frontends differ on exactly that. `sp@40` is a local
|
|
179
258
|
* on one ABI and the caller's fifth argument on another, so the shared stamp asks the frontend's
|
|
180
259
|
* own `LiveInModel.declaredLocals` and refuses an offset outside it — Thumb declares a range,
|
|
181
|
-
* MIPS and PPC declare
|
|
260
|
+
* MIPS and PPC declare none and so stamp nothing. An entry here therefore means "storage
|
|
182
261
|
* this function DECLARES, homed at k", not "some sp-relative key".
|
|
183
262
|
*
|
|
184
263
|
* DECLARES, NOT OWNS, AND THE DIFFERENCE IS AN AGBCC FACT WITH A LIVE DEPENDENCY: `ownedLocals`,
|
|
@@ -283,6 +362,35 @@ export function foldWriteOrder(order: WriteOrder | undefined, from: Block, into:
|
|
|
283
362
|
order.writes.set(into, base + (order.writes.get(from) ?? 0));
|
|
284
363
|
}
|
|
285
364
|
|
|
365
|
+
/** The `laddr` attribute that marks a function's OWN struct return object: the local its hidden
|
|
366
|
+
* struct-return pointer became, or the one its struct returned in a register is built in
|
|
367
|
+
* (frontend/own-struct-return.ts). Neither lives in this frame, so no frame offset names it and
|
|
368
|
+
* its `off` means nothing. */
|
|
369
|
+
export const OWN_RETURN_OBJECT = 'returned';
|
|
370
|
+
|
|
371
|
+
/** The `call` attribute of L1 that says its result is the IMAGE of a struct its callee is declared
|
|
372
|
+
* to return in the return register (frontend/call-plan.ts `registerStructLocal`): a word the
|
|
373
|
+
* frontend reads as any call's, which `liftRegisterStructCalls` (frontend/register-struct-call.ts)
|
|
374
|
+
* turns into the struct assigned to a local and re-read off it. No call carries it past that.
|
|
375
|
+
* `true`, or {@link STRUCT_HOMED} where the frontend saw the image stored to a frame home. */
|
|
376
|
+
export const REGISTER_STRUCT = 'registerStruct';
|
|
377
|
+
|
|
378
|
+
/** The value of {@link REGISTER_STRUCT} on a call whose image the function stores to a frame word
|
|
379
|
+
* (frontend/ppc.ts): mwcc gives a struct the source names a home, read or not, and one the source
|
|
380
|
+
* only returns none — `struct T s = mk(1); return s;` is `bl mk; stw r3,8(r1)`, `return mk(1);` is
|
|
381
|
+
* `bl mk` alone. */
|
|
382
|
+
export const STRUCT_HOMED = 'homed';
|
|
383
|
+
|
|
384
|
+
/** The `laddr` attribute that marks the local a struct returned in a register is assigned to
|
|
385
|
+
* (frontend/register-struct-call.ts), holding the callee's name, which names the local. The
|
|
386
|
+
* struct lives in the return register, not in this frame, so no frame offset names it and its
|
|
387
|
+
* `off` means nothing, as for {@link OWN_RETURN_OBJECT}. */
|
|
388
|
+
export const CALL_RETURN_OBJECT = 'callReturned';
|
|
389
|
+
|
|
390
|
+
/** The `ret` attribute that says it hands back the {@link OWN_RETURN_OBJECT} whole, by value: its
|
|
391
|
+
* operand is that object's address, and the C returns the object. */
|
|
392
|
+
export const RETURNS_OBJECT = 'returnsObject';
|
|
393
|
+
|
|
286
394
|
export function mkValue(type: IrType): Value {
|
|
287
395
|
return { type };
|
|
288
396
|
}
|
|
@@ -297,6 +405,19 @@ export function mkOp(opcode: Opcode, o: Partial<Op> = {}): Op {
|
|
|
297
405
|
};
|
|
298
406
|
}
|
|
299
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
|
+
|
|
300
421
|
// ── a call's callee: a NAME (`attrs.target`) or, for a call through a register, a VALUE ─────────
|
|
301
422
|
//
|
|
302
423
|
// An INDIRECT call carries `indirect: true` and no `target`, and calls the function whose address
|
|
@@ -314,6 +435,15 @@ export function calleeName(op: Op): string | undefined {
|
|
|
314
435
|
return op.opcode === 'call' && typeof op.attrs.target === 'string' ? op.attrs.target : undefined;
|
|
315
436
|
}
|
|
316
437
|
|
|
438
|
+
/** The operands a call passes a struct BY VALUE (frontend/call-plan.ts `foldByValueArgs`): each
|
|
439
|
+
* one's place in the operands and the declared parameter it is. The two differ by more than a
|
|
440
|
+
* hidden return pointer wherever a parameter before it takes two operands, so the parameter is
|
|
441
|
+
* carried rather than re-derived. */
|
|
442
|
+
export function byValueOperands(op: Op): { at: number; param: number }[] {
|
|
443
|
+
const pairs = op.opcode === 'call' ? ((op.attrs.byValue as number[] | undefined) ?? []) : [];
|
|
444
|
+
return Array.from({ length: pairs.length / 2 }, (_, i) => ({ at: pairs[2 * i], param: pairs[2 * i + 1] }));
|
|
445
|
+
}
|
|
446
|
+
|
|
317
447
|
/** A call's arguments, the callee value of an indirect call aside. */
|
|
318
448
|
export function callArgs(op: Op): Value[] {
|
|
319
449
|
return calleeValue(op) === undefined ? op.operands : op.operands.slice(0, -1);
|
|
@@ -370,7 +500,7 @@ export function successorsOf(b: Block): Block[] {
|
|
|
370
500
|
* keeps a walk of its own rather than calling this one — it descends through EVERY op's successors,
|
|
371
501
|
* which is the same relation on verified IR and a wider one on the hand-built IR its own note is
|
|
372
502
|
* about. */
|
|
373
|
-
export function reachableBlocks(fn:
|
|
503
|
+
export function reachableBlocks(fn: { readonly blocks: readonly Block[] }): Set<Block> {
|
|
374
504
|
const seen = new Set<Block>();
|
|
375
505
|
const entry = fn.blocks[0];
|
|
376
506
|
if (entry === undefined) {
|
|
@@ -388,8 +518,35 @@ export function reachableBlocks(fn: Fn): Set<Block> {
|
|
|
388
518
|
return seen;
|
|
389
519
|
}
|
|
390
520
|
|
|
521
|
+
/** The blocks `from` reaches along FORWARD edges — never a back-edge, whose target dominates its
|
|
522
|
+
* source — `from` itself included. A block of `stop` is reached but not walked past.
|
|
523
|
+
*
|
|
524
|
+
* Forward only, so an enclosing loop's next iteration does not count: the exit of a loop nested in
|
|
525
|
+
* another reaches every block of the inner body round the outer back-edge, and a full walk would
|
|
526
|
+
* say it runs the inner loop's own arms. Two passes ask "does a loop's exit run this block":
|
|
527
|
+
* `raise/latch.ts`'s exit-trampoline scope gate and `structure.ts`'s `offExitPathOf` (the
|
|
528
|
+
* duplication policy `codeBreakArm` and `earlyReturnArm` share). */
|
|
529
|
+
export function reachForward(
|
|
530
|
+
from: Block,
|
|
531
|
+
dom: ReadonlyMap<Block, ReadonlySet<Block>>,
|
|
532
|
+
stop: ReadonlySet<Block> = new Set(),
|
|
533
|
+
): Set<Block> {
|
|
534
|
+
const seen = new Set<Block>();
|
|
535
|
+
for (const stack = [from]; stack.length > 0;) {
|
|
536
|
+
const x = stack.pop()!;
|
|
537
|
+
if (seen.has(x)) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
seen.add(x);
|
|
541
|
+
if (!stop.has(x)) {
|
|
542
|
+
stack.push(...successorsOf(x).filter((s) => !dom.get(x)!.has(s)));
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return seen;
|
|
546
|
+
}
|
|
547
|
+
|
|
391
548
|
/** Predecessor map for the whole function's CFG. */
|
|
392
|
-
export function predecessors(fn:
|
|
549
|
+
export function predecessors(fn: { readonly blocks: readonly Block[] }): Map<Block, Block[]> {
|
|
393
550
|
const preds = new Map<Block, Block[]>();
|
|
394
551
|
for (const b of fn.blocks) {
|
|
395
552
|
preds.set(b, []);
|
|
@@ -405,10 +562,10 @@ export function predecessors(fn: Fn): Map<Block, Block[]> {
|
|
|
405
562
|
/** Forward dominators (iterative data-flow). dom(b) = {b} ∪ ⋂ dom(preds).
|
|
406
563
|
*
|
|
407
564
|
* A CFG fact, so it lives beside `predecessors` it is built on rather than with either consumer:
|
|
408
|
-
* `verify` needs it to check def-dominates-use, `
|
|
565
|
+
* `verify` needs it to check def-dominates-use, `ir/loops.ts` to tell a back-edge from a
|
|
409
566
|
* forward one, and `raise/latch.ts` to tell a latch from a preheader. */
|
|
410
567
|
|
|
411
|
-
export function dominators(fn:
|
|
568
|
+
export function dominators(fn: { readonly blocks: readonly Block[] }): Map<Block, Set<Block>> {
|
|
412
569
|
const preds = predecessors(fn);
|
|
413
570
|
const all = new Set(fn.blocks);
|
|
414
571
|
const dom = new Map<Block, Set<Block>>();
|
|
@@ -441,6 +598,94 @@ export function dominators(fn: Fn): Map<Block, Set<Block>> {
|
|
|
441
598
|
return dom;
|
|
442
599
|
}
|
|
443
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
|
+
|
|
444
689
|
/** A block with NO BODY OF ITS OWN: it declares no parameters and holds a single op, which is
|
|
445
690
|
* therefore its terminator. It computes nothing and it binds nothing. Three sites ask this and
|
|
446
691
|
* each adds its own clause (`forwardingTarget` below wants a `br` carrying no args;
|
|
@@ -510,7 +755,7 @@ export function forwardingTarget(b: Block): Block {
|
|
|
510
755
|
* a CFG fact a parameter for one caller's extra edges — so it stays where it is, and what this
|
|
511
756
|
* paragraph buys is that the next reader looking for a fourth copy knows which of the two the
|
|
512
757
|
* third one is. */
|
|
513
|
-
export function mergeClasses(fn:
|
|
758
|
+
export function mergeClasses(fn: { readonly blocks: readonly Block[] }): Map<Value, readonly Value[]> {
|
|
514
759
|
const parent = new Map<Value, Value>();
|
|
515
760
|
const find = (v: Value): Value => {
|
|
516
761
|
let r = v;
|
package/src/ir/discipline.ts
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
// Two kinds of fact meet here. The registry's flags (ir/opcodes.ts `OpSig`: `effects`, `reads`,
|
|
5
5
|
// `traps`) are facts about an OPCODE. A PLACEMENT is a fact about one OP, decided once by the pass
|
|
6
6
|
// that knew it: `call` is its own opcode; `helper` is the stamp `runtime-helpers.ts` `helperOp` puts
|
|
7
|
-
// on a value op the asm computed by calling a runtime routine; `device` is the `volatile`
|
|
8
|
-
//
|
|
7
|
+
// on a value op the asm computed by calling a runtime routine; `device` is the `volatile` a frontend
|
|
8
|
+
// puts on a memory access, decided by two pins — the device pin (frontend/device-pins.ts), for an
|
|
9
|
+
// access of a device register, and the dead-read pin (frontend/dead-reads.ts), for a read at ANY
|
|
10
|
+
// address whose value the lifted function never uses; `declared` is the
|
|
9
11
|
// stamp raise/declared-volatile.ts puts on an access of an object the symbol map declares volatile —
|
|
10
12
|
// a read only where the compiler could have made it of a `volatile` — as the lift is made and again
|
|
11
13
|
// when structuring starts. Every question below takes the op, so a pass
|
|
@@ -50,7 +52,7 @@
|
|
|
50
52
|
// a read the stamp cannot place by byte — a runtime index, an `aload` — of an object the map
|
|
51
53
|
// qualifies only some members of, which may reach a plain member (symbols.ts
|
|
52
54
|
// `declaresVolatile`).
|
|
53
|
-
import type { Op } from './core';
|
|
55
|
+
import type { Block, Op, Value } from './core';
|
|
54
56
|
import { MEM_BASE_OPS, type OpSig, opSig } from './opcodes';
|
|
55
57
|
|
|
56
58
|
/** Why an op must run where the asm ran it. */
|
|
@@ -107,23 +109,37 @@ const PLACEMENT_ANSWERS: Readonly<Record<Placement, PlacementAnswers>> = {
|
|
|
107
109
|
},
|
|
108
110
|
};
|
|
109
111
|
|
|
110
|
-
/**
|
|
111
|
-
|
|
112
|
+
/** Every placement stamp an op carries, the one that places it first. An op can carry two: a read
|
|
113
|
+
* of an object the map declares volatile is `declared` and, kept by the lift's own pin (frontend/
|
|
114
|
+
* dead-reads.ts, frontend/device-pins.ts), `device` too.
|
|
115
|
+
*
|
|
116
|
+
* A declaration that qualifies the object places the access before a lift's own stamp does: both
|
|
117
|
+
* say the access is `volatile`, and the declaration is the spelling the source used, so the
|
|
118
|
+
* access counts and spells as the object's (raise/declared-volatile.ts re-derives it per map, and
|
|
119
|
+
* under a map that declares nothing the lift's stamp places it again — which is why a rebuild
|
|
120
|
+
* carries both, `carryDiscipline`). */
|
|
121
|
+
function stampsOf(op: Op): Placement[] {
|
|
112
122
|
if (op.opcode === 'call') {
|
|
113
|
-
return 'call';
|
|
114
|
-
}
|
|
115
|
-
if (MEM_BASE_OPS.has(op.opcode) && op.attrs.volatile === true) {
|
|
116
|
-
return 'device';
|
|
123
|
+
return ['call'];
|
|
117
124
|
}
|
|
125
|
+
const stamps: Placement[] = [];
|
|
118
126
|
if (MEM_BASE_OPS.has(op.opcode) && op.attrs[DECLARED_VOLATILE] === true) {
|
|
119
|
-
|
|
127
|
+
stamps.push('declared');
|
|
128
|
+
}
|
|
129
|
+
if (MEM_BASE_OPS.has(op.opcode) && op.attrs.volatile === true) {
|
|
130
|
+
stamps.push('device');
|
|
120
131
|
}
|
|
121
132
|
// An `opaque` carries `helper` too, naming the call nothing could fold (`refuseUnmodelledHelpers`);
|
|
122
133
|
// it is no value op, and its `effects` flag already places it.
|
|
123
134
|
if (op.opcode !== 'opaque' && typeof op.attrs.helper === 'string') {
|
|
124
|
-
|
|
135
|
+
stamps.push('helper');
|
|
125
136
|
}
|
|
126
|
-
return
|
|
137
|
+
return stamps;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The placement an op carries, or null for an op that may render wherever its value is used. */
|
|
141
|
+
export function placedAt(op: Op): Placement | null {
|
|
142
|
+
return stampsOf(op)[0] ?? null;
|
|
127
143
|
}
|
|
128
144
|
|
|
129
145
|
const answered = (op: Op, q: 'keptWhenDead' | 'speculationUnsafe' | 'counted'): boolean => {
|
|
@@ -161,12 +177,59 @@ export function effectful(op: Op): boolean {
|
|
|
161
177
|
}
|
|
162
178
|
|
|
163
179
|
/** May a dead result be deleted? Registered, no effect, not control flow, and not an op whose
|
|
164
|
-
* placement keeps it.
|
|
180
|
+
* placement keeps it. A memory read whose value the RAW lift never uses is never asked: the
|
|
181
|
+
* frontend stamps it `volatile` first (frontend/dead-reads.ts), because a compiler deletes every
|
|
182
|
+
* plain read nobody uses, so one still in the target was qualified. What reaches this question
|
|
183
|
+
* plain is a read only asmlift's own rewrites orphaned, or one that pass's gates refused. */
|
|
165
184
|
export function deletableWhenDead(op: Op): boolean {
|
|
166
185
|
const sig = sigOf(op);
|
|
167
186
|
return !!sig && !sig.effects && !sig.terminator && !answered(op, 'keptWhenDead');
|
|
168
187
|
}
|
|
169
188
|
|
|
189
|
+
/** THE DEAD OPS of these blocks: each op `deletableWhenDead` answers yes for whose single result no
|
|
190
|
+
* op outside the set reads, as an operand or as a successor's argument, to a fixed point. The ONE
|
|
191
|
+
* definition of dead: pattern/engine.ts `dce` deletes exactly this set, and the dead-read pin
|
|
192
|
+
* (frontend/dead-reads.ts) asks it of the raw lift before anything is deleted, so the pin's "dead"
|
|
193
|
+
* cannot drift from what the pass would delete.
|
|
194
|
+
*
|
|
195
|
+
* `usesOperands` says whether an op's operands are uses at all. Every op's are, for `dce`; the pin
|
|
196
|
+
* passes the one exception a declaration makes: the `ret` of a function its own prototype declares
|
|
197
|
+
* void, whose operand the structurer suppresses (structure/analysis.ts `indexUses`). */
|
|
198
|
+
export function deadOps(blocks: readonly Block[], usesOperands: (op: Op) => boolean = () => true): Set<Op> {
|
|
199
|
+
const dead = new Set<Op>();
|
|
200
|
+
let changed = true;
|
|
201
|
+
while (changed) {
|
|
202
|
+
changed = false;
|
|
203
|
+
const used = new Set<Value>();
|
|
204
|
+
for (const b of blocks) {
|
|
205
|
+
for (const op of b.ops) {
|
|
206
|
+
if (dead.has(op)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (usesOperands(op)) {
|
|
210
|
+
for (const o of op.operands) {
|
|
211
|
+
used.add(o);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
for (const s of op.successors) {
|
|
215
|
+
for (const a of s.args) {
|
|
216
|
+
used.add(a);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const b of blocks) {
|
|
222
|
+
for (const op of b.ops) {
|
|
223
|
+
if (!dead.has(op) && deletableWhenDead(op) && op.results.length === 1 && !used.has(op.results[0])) {
|
|
224
|
+
dead.add(op);
|
|
225
|
+
changed = true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return dead;
|
|
231
|
+
}
|
|
232
|
+
|
|
170
233
|
/** Must the structurer still SPELL this op when nothing consumes its result — an effect, or a
|
|
171
234
|
* memory read?
|
|
172
235
|
*
|
|
@@ -174,8 +237,9 @@ export function deletableWhenDead(op: Op): boolean {
|
|
|
174
237
|
* the C claim: nothing observes a load nobody reads. This one is the COMPILER claim: an optimizing
|
|
175
238
|
* compiler deletes every dead read it is allowed to delete, so one still in the target is evidence
|
|
176
239
|
* the source's access was `volatile`. A yes here only says the op may not be dropped silently;
|
|
177
|
-
* whether
|
|
178
|
-
*
|
|
240
|
+
* whether the read was `volatile` is decided once, by the dead-read pin (frontend/dead-reads.ts),
|
|
241
|
+
* and the caller spells a read only where it is `qualified` (structure.ts `volatileQualifiable`),
|
|
242
|
+
* dropping one the pin left plain.
|
|
179
243
|
*
|
|
180
244
|
* Its own function and not `orderSensitive`, though the two agree on every registered opcode: they
|
|
181
245
|
* ask different questions, and a registry change that splits them is a decision to make at each. */
|
|
@@ -238,11 +302,12 @@ export function forgetHelperPlacement(op: Op): Op {
|
|
|
238
302
|
return op;
|
|
239
303
|
}
|
|
240
304
|
|
|
241
|
-
/** `attrs`, plus
|
|
242
|
-
* access keeps its `volatile`
|
|
243
|
-
* carries the stamps through here, so a placement decided once is not lost to a rebuild
|
|
244
|
-
* forgot to copy it.
|
|
305
|
+
/** `attrs`, plus every placement stamp of `from`, for an op a pass builds to stand for `from`: a
|
|
306
|
+
* memory access keeps its `volatile` and its `declared` stamp, a value op its `helper`. Every op
|
|
307
|
+
* rebuilder carries the stamps through here, so a placement decided once is not lost to a rebuild
|
|
308
|
+
* that forgot to copy it. EVERY stamp, not only the one that places the op: a read carrying both
|
|
309
|
+
* `declared` and `device` that kept only `declared` would be left plain — and deleted as dead — by
|
|
310
|
+
* a later stamp that no longer finds the declaration. */
|
|
245
311
|
export function carryDiscipline(from: Op, attrs: Op['attrs']): Op['attrs'] {
|
|
246
|
-
|
|
247
|
-
return p === null ? attrs : { ...attrs, ...PLACEMENT_ANSWERS[p].carried(from) };
|
|
312
|
+
return stampsOf(from).reduce((acc, p) => ({ ...acc, ...PLACEMENT_ANSWERS[p].carried(from) }), attrs);
|
|
248
313
|
}
|