@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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// AN EARLY RETURN THAT THE JOIN PAST EARLY RETURNS CANNOT SPELL: a return of the value its branch
|
|
2
|
+
// tested equal to a constant, where the object still returns the value and not the constant.
|
|
3
|
+
//
|
|
4
|
+
// On a compiler whose cse folds a branch-proven constant only into the branch's FALL-THROUGH arm
|
|
5
|
+
// (target.ts `foldsBranchConstantOnTakenEdge` false: gcc 2.x at -O1), `if (x == 0) return x;`
|
|
6
|
+
// compiles to `return 0`, and so does the same test spelled `if (x != 0) ; else return x;`. The join
|
|
7
|
+
// writes the early return one of those two ways. A return of `x` the object left unfolded was
|
|
8
|
+
// therefore the jump-target arm of its branch in the source: the `else` of `if (x != 0) { ...tail }
|
|
9
|
+
// else { return x; }`, which is the copies' spelling. So this mark publishes the copies.
|
|
10
|
+
//
|
|
11
|
+
// THE CONSTANT IS NOT REMATERIALISED ON THE EDGE. Read off the lifted IR: the return block's value
|
|
12
|
+
// is the very SSA value the comparison reads, not a constant. A delay slot that set the return
|
|
13
|
+
// register to the constant (`beqz v0,L; move v0,zero`) lifts as a `const` returned on that edge, and
|
|
14
|
+
// is no mark.
|
|
15
|
+
//
|
|
16
|
+
// MEASURED on the probe pair `corpus/probe-rettail-pg-{copies,join}.c` (unfolded-branch-constant.test.ts)
|
|
17
|
+
// and on Mario Party 3's `func_8010BD28_472F98_mgmode`, which it is the shape of: gcc2.7.2 -O1, the
|
|
18
|
+
// copies `nop` in the `beqz`'s slot and the join `move v0,zero`. There the join loses the copies'
|
|
19
|
+
// match (0 -> 1 line off, really compiled, 2026-11-06).
|
|
20
|
+
//
|
|
21
|
+
// AN `if` SITE ONLY. The premise is an `if`'s two arms, one of them the fall-through. A switch's case
|
|
22
|
+
// arms, read as a switch (`tree`) or a jump table (`switch`), are each a jump target, a case label,
|
|
23
|
+
// so no return in one folds the switch value at -O1 whatever the source wrote, and an unfolded one
|
|
24
|
+
// says nothing. Compiled at gcc2.7.2 -O1: `switch (x) { case 0: return x; case 5: p[1] = 2; break; }`
|
|
25
|
+
// followed by its tail, written with the join, keeps `move v0,a0` in the case-0 jump's slot, and
|
|
26
|
+
// the join and the copies both compile byte-exact. A switch the structurer reads as `if`s (three
|
|
27
|
+
// cases of that function) is still read, the residue of the rule: no reading of it is exact.
|
|
28
|
+
//
|
|
29
|
+
// A MARK IS EVIDENCE, NOT PROOF, as structure/cross-jumped-tail.ts says of its own: it moves which
|
|
30
|
+
// verified reading is the default, never what is lifted. Read only where the target says its cse
|
|
31
|
+
// leaves the taken edge unfolded; elsewhere (`true`, or absent) an unfolded return says nothing.
|
|
32
|
+
import type { Block, Fn, Op, Value } from '../ir/core';
|
|
33
|
+
import type { JoinPastReturnsSite } from './structure';
|
|
34
|
+
|
|
35
|
+
const succsOf = (b: Block): Block[] => [...new Set(b.ops[b.ops.length - 1]?.successors.map((s) => s.block) ?? [])];
|
|
36
|
+
|
|
37
|
+
/** Does an early return of the `if` site `site` give back, unfolded, the value its own branch tested
|
|
38
|
+
* equal to a constant? The return block holds only its `ret`, has one predecessor, and that
|
|
39
|
+
* predecessor's `cond_br` takes it on the equal side of an `icmp_eq`/`icmp_ne` of that value against
|
|
40
|
+
* a `const`. */
|
|
41
|
+
export function unfoldedBranchConstant(fn: Fn, site: JoinPastReturnsSite): boolean {
|
|
42
|
+
if (site.kind !== 'if') {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const def = new Map<Value, Op>();
|
|
46
|
+
const preds = new Map<Block, Block[]>(fn.blocks.map((b) => [b, []]));
|
|
47
|
+
for (const b of fn.blocks) {
|
|
48
|
+
for (const op of b.ops) {
|
|
49
|
+
for (const r of op.results) {
|
|
50
|
+
def.set(r, op);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const s of succsOf(b)) {
|
|
54
|
+
preds.get(s)?.push(b);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const seen = new Set<Block>();
|
|
58
|
+
const stack = [site.branch];
|
|
59
|
+
while (stack.length) {
|
|
60
|
+
const x = stack.pop()!;
|
|
61
|
+
if (x === site.join || seen.has(x)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
seen.add(x);
|
|
65
|
+
stack.push(...succsOf(x));
|
|
66
|
+
}
|
|
67
|
+
for (const r of seen) {
|
|
68
|
+
const ret = r.ops.length === 1 ? r.ops[0] : undefined;
|
|
69
|
+
const value = ret?.opcode === 'ret' && ret.operands.length === 1 ? ret.operands[0] : undefined;
|
|
70
|
+
const from = preds.get(r) ?? [];
|
|
71
|
+
if (value === undefined || from.length !== 1 || fn.asmLayout?.get(r) === undefined) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const br = from[0].ops[from[0].ops.length - 1];
|
|
75
|
+
const cmp = br?.opcode === 'cond_br' ? def.get(br.operands[0]) : undefined;
|
|
76
|
+
if (cmp === undefined || br.successors.length !== 2 || br.successors[0].block === br.successors[1].block) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const equalSide = cmp.opcode === 'icmp_eq' ? 0 : cmp.opcode === 'icmp_ne' ? 1 : -1;
|
|
80
|
+
if (equalSide < 0 || br.successors[equalSide].block !== r || cmp.operands.length !== 2) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const [a, b] = cmp.operands;
|
|
84
|
+
const isConst = (v: Value) => def.get(v)?.opcode === 'const';
|
|
85
|
+
if ((a === value && isConst(b)) || (b === value && isConst(a))) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
package/src/symbols.ts
CHANGED
|
@@ -189,6 +189,12 @@ export interface SymbolInfo {
|
|
|
189
189
|
* DECLARATION differs — `#define name body` instead of `extern T name;` — which is why the
|
|
190
190
|
* body is carried rather than reconstructed. */
|
|
191
191
|
macroBody?: string;
|
|
192
|
+
/** This name is a FILE-SCOPE GLOBAL REGISTER VARIABLE, and this is the register's `__asm__` name
|
|
193
|
+
* (`target.globalRegisterVariables`): the lift reads and writes a callee-saved register no
|
|
194
|
+
* prologue saved as this global. `gName` and `gName = v` mean what they mean for an `extern`, and
|
|
195
|
+
* `&gName` is an error, as for any register variable. Only the DECLARATION differs —
|
|
196
|
+
* `register T name __asm__("r8");` instead of `extern T name;`. */
|
|
197
|
+
globalRegister?: string;
|
|
192
198
|
}
|
|
193
199
|
|
|
194
200
|
/** THE one reading of {@link SymbolInfo.dims} for spelling C, shared by the access side
|
|
@@ -554,19 +560,48 @@ export function pointeeFields(pointee: SymbolPointee | undefined): DeclaredField
|
|
|
554
560
|
return declaredFields(pointee.layout);
|
|
555
561
|
}
|
|
556
562
|
|
|
557
|
-
/**
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
563
|
+
/** A name C can spell. Splat's `func_X.NON_MATCHING` labels are sized data aliases that are not. */
|
|
564
|
+
const C_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
565
|
+
|
|
566
|
+
/** THE one object an address names, decided once per address so every lookup that lands there
|
|
567
|
+
* spells the same name. Two names for one object inside one function are not a style problem:
|
|
568
|
+
* the compiler treats distinct symbols as non-aliasing and forwards a store made through one past
|
|
569
|
+
* a store made through the other.
|
|
570
|
+
*
|
|
571
|
+
* Usually the canonical `[0]`. The exception is an unsized data label ranked first over the sized
|
|
572
|
+
* object that starts at the same address (sa3's `gUnknown_08358528` over the 540-byte
|
|
573
|
+
* `Map_zone_1_act_hub_fg0`, pokeemerald's `gMultiBootProgram_BerryGlitchFix_End` over
|
|
574
|
+
* `gMultiBootProgram_PokemonColosseum_Start`): the object is the sized one, because only it has
|
|
575
|
+
* an extent, so both an exact hit and an interior hit name it. Never where any alias is code: a
|
|
576
|
+
* sized data alias there is a label over the function's bytes (MIPS `func_X.NON_MATCHING`, which
|
|
577
|
+
* splat maps rank behind an unsized `gcc2_compiled.` marker), not an object. */
|
|
578
|
+
function objectAt(infos: readonly SymbolInfo[]): SymbolInfo {
|
|
579
|
+
const head = infos[0];
|
|
580
|
+
if (head.kind !== 'data' || head.size !== undefined || infos.some((i) => i.kind === 'code')) {
|
|
581
|
+
return head;
|
|
582
|
+
}
|
|
583
|
+
return infos.find((i) => i.kind === 'data' && i.size !== undefined && C_IDENTIFIER.test(i.name)) ?? head;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** Kind-aware two-probe lookup for a pool-loaded 32-bit value. Exact match first (any kind), which
|
|
587
|
+
* names {@link objectAt}'s object; on miss, `value & ~1` — accepted ONLY when the hit is code,
|
|
588
|
+
* because ELF function addresses are stored with the Thumb bit cleared while a Thumb code pointer
|
|
589
|
+
* in a pool is odd. An exact odd-DATA hit therefore wins over a masked code hit (odd data
|
|
590
|
+
* addresses are real).
|
|
591
|
+
*
|
|
592
|
+
* The masked probe asks for the CODE alias at that address, not for the canonical `[0]`: a
|
|
593
|
+
* table-end or segment marker can share its address with the function that follows it, and the
|
|
594
|
+
* provider may rank the marker first (pokeemerald's `gMysteryEventScriptCmdTableEnd` over
|
|
595
|
+
* `GameCubeMultiBoot_Hash`). An odd pool word there is still the function's Thumb pointer. */
|
|
561
596
|
export function lookupSymbol(map: SymbolMap, value: number): SymbolInfo | null {
|
|
562
|
-
const exact = map.get(value)
|
|
563
|
-
if (exact) {
|
|
564
|
-
return exact;
|
|
597
|
+
const exact = map.get(value);
|
|
598
|
+
if (exact !== undefined && exact.length > 0) {
|
|
599
|
+
return objectAt(exact);
|
|
565
600
|
}
|
|
566
601
|
if ((value & 1) === 1) {
|
|
567
|
-
const
|
|
568
|
-
if (
|
|
569
|
-
return
|
|
602
|
+
const code = map.get(value & ~1)?.find((info) => info.kind === 'code');
|
|
603
|
+
if (code) {
|
|
604
|
+
return code;
|
|
570
605
|
}
|
|
571
606
|
}
|
|
572
607
|
return null;
|
|
@@ -574,10 +609,11 @@ export function lookupSymbol(map: SymbolMap, value: number): SymbolInfo | null {
|
|
|
574
609
|
|
|
575
610
|
/** Interior attribution: the data symbol whose `[address, address+size)` range contains
|
|
576
611
|
* `value` strictly inside (offset > 0 — exact bases go through `lookupSymbol`). Only
|
|
577
|
-
* complete-typed globals carry a size, so unsized arrays never attribute.
|
|
612
|
+
* complete-typed globals carry a size, so unsized arrays never attribute. The symbol at each
|
|
613
|
+
* base is {@link objectAt}'s, the same one an exact hit on that base names. */
|
|
578
614
|
export function lookupInterior(map: SymbolMap, value: number): { info: SymbolInfo; offset: number } | null {
|
|
579
615
|
for (const [addr, infos] of map) {
|
|
580
|
-
const info = infos
|
|
616
|
+
const info = objectAt(infos);
|
|
581
617
|
if (info.kind !== 'data' || info.size === undefined) {
|
|
582
618
|
continue;
|
|
583
619
|
}
|
|
@@ -590,9 +626,16 @@ export function lookupInterior(map: SymbolMap, value: number): { info: SymbolInf
|
|
|
590
626
|
|
|
591
627
|
/** The `SymbolInfo` keys a project's DWARF can only carry because the symbol's DEFINITION was
|
|
592
628
|
* compiled from C. Everything else in the map survives a function that is still `INCLUDE_ASM`:
|
|
593
|
-
* its `.symtab` entry exists (the asm defines the label), and
|
|
594
|
-
*
|
|
595
|
-
*
|
|
629
|
+
* its `.symtab` entry exists (the asm defines the label), and most globals it reads are typed by
|
|
630
|
+
* other code that references them. Listed here, once, so {@link asIfUndecompiled} and any later
|
|
631
|
+
* definition-derived fact (a signature, a local's type, a register location) stay in sync.
|
|
632
|
+
*
|
|
633
|
+
* KNOWN GAP: "other code" is not guaranteed. agbcc writes a DWARF entry for an extern global only
|
|
634
|
+
* in a compilation unit that references it, so a global defined in asm (a NOTYPE symbol) that only
|
|
635
|
+
* the row's own body references takes its type from the row's decompiled C. Nothing here strips
|
|
636
|
+
* that type, because the map does not record which compilation unit typed a symbol. kleod has 10
|
|
637
|
+
* such globals across 6 rows, among them PauseMenuScreenHandler's `gUnk_081166F8[4][4]`. The
|
|
638
|
+
* CLI's repro reads the same ELF and sees the same types. */
|
|
596
639
|
const DEFINITION_DERIVED_KEYS = ['declared', 'signature'] as const satisfies readonly (keyof SymbolInfo)[];
|
|
597
640
|
|
|
598
641
|
/** The map a user actually has while decompiling `fn` — i.e. with `fn` still an `INCLUDE_ASM`
|