@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.
Files changed (117) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +2 -0
  5. package/src/backend/cfamily.ts +197 -61
  6. package/src/backend/cpp.ts +3 -1
  7. package/src/backend/lines.ts +10 -0
  8. package/src/backend/pascal.ts +47 -16
  9. package/src/cdecl/parse.ts +21 -0
  10. package/src/contracts.ts +246 -36
  11. package/src/declare.ts +69 -20
  12. package/src/frontend/arm-body.ts +155 -0
  13. package/src/frontend/asmdata.ts +9 -2
  14. package/src/frontend/call-plan.ts +1117 -61
  15. package/src/frontend/callee-body.ts +370 -0
  16. package/src/frontend/dead-reads.ts +628 -0
  17. package/src/frontend/device-pins.ts +33 -4
  18. package/src/frontend/emit.ts +4 -3
  19. package/src/frontend/fpu.ts +224 -17
  20. package/src/frontend/frame-objects.ts +1689 -206
  21. package/src/frontend/frontend.ts +84 -3
  22. package/src/frontend/high-half.ts +53 -4
  23. package/src/frontend/jump-table.ts +545 -0
  24. package/src/frontend/local-object.ts +158 -32
  25. package/src/frontend/mips.ts +2296 -217
  26. package/src/frontend/own-struct-return.ts +500 -0
  27. package/src/frontend/ppc.ts +2748 -245
  28. package/src/frontend/register-struct-call.ts +399 -0
  29. package/src/frontend/reloc-symbol.ts +22 -7
  30. package/src/frontend/ssa.ts +1721 -43
  31. package/src/frontend/stackargs.ts +199 -72
  32. package/src/frontend/thumb-entry.ts +340 -0
  33. package/src/frontend/thumb.ts +2400 -325
  34. package/src/ir/alias.ts +65 -2
  35. package/src/ir/bits.ts +1699 -4
  36. package/src/ir/core.ts +254 -9
  37. package/src/ir/discipline.ts +86 -21
  38. package/src/ir/entry.ts +157 -0
  39. package/src/{structure → ir}/loops.ts +6 -2
  40. package/src/ir/opcodes.ts +137 -9
  41. package/src/ir/parse.ts +1 -0
  42. package/src/ir/simplify.ts +61 -10
  43. package/src/ir/types.ts +86 -2
  44. package/src/ir/verify.ts +96 -11
  45. package/src/l3/argbase.ts +2 -0
  46. package/src/l3/ast.ts +582 -18
  47. package/src/l3/basecse.ts +68 -15
  48. package/src/l3/caseendgoto.ts +223 -0
  49. package/src/l3/coalesce.ts +42 -14
  50. package/src/l3/constinit.ts +410 -0
  51. package/src/l3/dce.ts +53 -9
  52. package/src/l3/initfirst.ts +42 -11
  53. package/src/l3/intcell.ts +2 -1
  54. package/src/l3/narrowdecl.ts +77 -36
  55. package/src/l3/pollguard.ts +8 -2
  56. package/src/l3/ptrcell.ts +33 -34
  57. package/src/l3/ptrfield.ts +123 -8
  58. package/src/l3/regspell.ts +11 -4
  59. package/src/l3/reindex.ts +295 -99
  60. package/src/l3/scopebase.ts +2 -0
  61. package/src/l3/storage.ts +98 -1
  62. package/src/l3/symbol-refs.ts +84 -6
  63. package/src/l3/tailmerge.ts +6 -2
  64. package/src/l3/typing.ts +108 -15
  65. package/src/l3/unreduce.ts +6 -5
  66. package/src/l3/volatileptr.ts +17 -54
  67. package/src/mangle.ts +30 -0
  68. package/src/pattern/engine.ts +361 -73
  69. package/src/pipeline.ts +1039 -38
  70. package/src/proto-context.ts +1039 -45
  71. package/src/proto.ts +555 -40
  72. package/src/raise/aggmembers.ts +290 -0
  73. package/src/raise/arrays.ts +5 -1
  74. package/src/raise/const.ts +25 -0
  75. package/src/raise/declared-volatile.ts +207 -31
  76. package/src/raise/divpow2.ts +192 -0
  77. package/src/raise/floathelpers.ts +254 -33
  78. package/src/raise/gvn.ts +3 -3
  79. package/src/raise/latch.ts +162 -6
  80. package/src/raise/memberarrays.ts +10 -4
  81. package/src/raise/offsetnames.ts +422 -36
  82. package/src/raise/pairparams.ts +33 -0
  83. package/src/raise/paramwidth.ts +196 -6
  84. package/src/raise/pre-recovery.ts +107 -21
  85. package/src/raise/pre-structure.ts +73 -0
  86. package/src/raise/recover.ts +12 -5
  87. package/src/raise/reroll.ts +674 -0
  88. package/src/raise/shortcircuit.ts +217 -34
  89. package/src/raise/softdiv.ts +1 -1
  90. package/src/raise/storesign.ts +153 -0
  91. package/src/raise/struct-arrays.ts +30 -11
  92. package/src/raise/structs.ts +396 -33
  93. package/src/raise/truncload.ts +4 -3
  94. package/src/raise/unpre.ts +594 -0
  95. package/src/raise/unthread.ts +400 -0
  96. package/src/rank-declare.ts +118 -12
  97. package/src/rank-variations.ts +186 -35
  98. package/src/rank.ts +536 -68
  99. package/src/runtime-helpers.ts +140 -22
  100. package/src/structure/analysis.ts +217 -8
  101. package/src/structure/arith-ops.ts +23 -0
  102. package/src/structure/cross-jumped-tail.ts +216 -0
  103. package/src/structure/globalaccess.ts +157 -10
  104. package/src/structure/hazards.ts +16 -64
  105. package/src/structure/local-statics.ts +145 -9
  106. package/src/structure/namecoalesce.ts +1 -1
  107. package/src/structure/pointer-spelling.ts +1106 -0
  108. package/src/structure/redundant-test.ts +1 -1
  109. package/src/structure/structure.ts +6192 -1213
  110. package/src/structure/switch-recover.ts +583 -120
  111. package/src/structure/unfolded-branch-constant.ts +90 -0
  112. package/src/symbols.ts +58 -15
  113. package/src/target.ts +1028 -54
  114. package/src/trace.ts +49 -17
  115. package/src/variation-definitions.ts +333 -20
  116. package/src/variation-gates.ts +4 -0
  117. 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
- /** Kind-aware two-probe lookup for a pool-loaded 32-bit value. Exact match first (any kind);
558
- * on miss, `value & ~1` — accepted ONLY when the hit is code, because ELF function addresses
559
- * are stored with the Thumb bit cleared while a Thumb code pointer in a pool is odd. An exact
560
- * odd-DATA hit therefore wins over a masked code hit (odd data addresses are real). */
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)?.[0];
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 masked = map.get(value & ~1)?.[0];
568
- if (masked?.kind === 'code') {
569
- return masked;
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[0];
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 its globals are typed by the OTHER
594
- * translation units that declare them. Listed here, once, so {@link asIfUndecompiled} and any
595
- * later definition-derived fact (a signature, a local's type, a register location) stay in sync. */
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`