@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.
Files changed (68) hide show
  1. package/package.json +1 -1
  2. package/src/backend/c.ts +1 -0
  3. package/src/backend/cfamily.ts +33 -12
  4. package/src/backend/cpp.ts +1 -0
  5. package/src/backend/lines.ts +10 -0
  6. package/src/backend/pascal.ts +17 -5
  7. package/src/cdecl/parse.ts +21 -0
  8. package/src/contracts.ts +73 -20
  9. package/src/frontend/arm-body.ts +155 -0
  10. package/src/frontend/asmdata.ts +2 -1
  11. package/src/frontend/call-plan.ts +197 -45
  12. package/src/frontend/callee-body.ts +370 -0
  13. package/src/frontend/device-pins.ts +18 -8
  14. package/src/frontend/frame-objects.ts +504 -61
  15. package/src/frontend/frontend.ts +20 -0
  16. package/src/frontend/jump-table.ts +35 -20
  17. package/src/frontend/mips.ts +226 -8
  18. package/src/frontend/ppc.ts +26 -2
  19. package/src/frontend/register-struct-call.ts +1 -86
  20. package/src/frontend/ssa.ts +263 -1
  21. package/src/frontend/stackargs.ts +95 -17
  22. package/src/frontend/thumb-entry.ts +340 -0
  23. package/src/frontend/thumb.ts +410 -61
  24. package/src/ir/alias.ts +29 -8
  25. package/src/ir/bits.ts +1081 -66
  26. package/src/ir/core.ts +154 -4
  27. package/src/ir/entry.ts +3 -5
  28. package/src/ir/opcodes.ts +28 -0
  29. package/src/ir/parse.ts +1 -0
  30. package/src/ir/simplify.ts +61 -10
  31. package/src/ir/verify.ts +16 -0
  32. package/src/l3/argbase.ts +2 -0
  33. package/src/l3/ast.ts +294 -2
  34. package/src/l3/basecse.ts +35 -10
  35. package/src/l3/caseendgoto.ts +223 -0
  36. package/src/l3/coalesce.ts +42 -14
  37. package/src/l3/constinit.ts +34 -6
  38. package/src/l3/dce.ts +52 -8
  39. package/src/l3/ptrcell.ts +2 -0
  40. package/src/l3/regspell.ts +2 -0
  41. package/src/l3/reindex.ts +13 -4
  42. package/src/l3/scopebase.ts +2 -0
  43. package/src/l3/tailmerge.ts +2 -0
  44. package/src/pipeline.ts +507 -23
  45. package/src/proto-context.ts +135 -1
  46. package/src/raise/const.ts +25 -0
  47. package/src/raise/declared-volatile.ts +1 -1
  48. package/src/raise/divpow2.ts +192 -0
  49. package/src/raise/offsetnames.ts +412 -10
  50. package/src/raise/pre-recovery.ts +8 -5
  51. package/src/raise/pre-structure.ts +24 -5
  52. package/src/raise/reroll.ts +4 -24
  53. package/src/raise/shortcircuit.ts +192 -23
  54. package/src/raise/unpre.ts +594 -0
  55. package/src/rank-variations.ts +97 -4
  56. package/src/rank.ts +258 -43
  57. package/src/structure/analysis.ts +56 -10
  58. package/src/structure/cross-jumped-tail.ts +216 -0
  59. package/src/structure/globalaccess.ts +46 -1
  60. package/src/structure/structure.ts +4221 -368
  61. package/src/structure/switch-recover.ts +345 -26
  62. package/src/structure/unfolded-branch-constant.ts +90 -0
  63. package/src/symbols.ts +52 -15
  64. package/src/target.ts +269 -8
  65. package/src/trace.ts +28 -14
  66. package/src/variation-definitions.ts +207 -6
  67. package/src/variation-gates.ts +2 -0
  68. package/src/variation-tokens.ts +63 -2
package/src/symbols.ts CHANGED
@@ -560,19 +560,48 @@ export function pointeeFields(pointee: SymbolPointee | undefined): DeclaredField
560
560
  return declaredFields(pointee.layout);
561
561
  }
562
562
 
563
- /** Kind-aware two-probe lookup for a pool-loaded 32-bit value. Exact match first (any kind);
564
- * on miss, `value & ~1` — accepted ONLY when the hit is code, because ELF function addresses
565
- * are stored with the Thumb bit cleared while a Thumb code pointer in a pool is odd. An exact
566
- * 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. */
567
596
  export function lookupSymbol(map: SymbolMap, value: number): SymbolInfo | null {
568
- const exact = map.get(value)?.[0];
569
- if (exact) {
570
- return exact;
597
+ const exact = map.get(value);
598
+ if (exact !== undefined && exact.length > 0) {
599
+ return objectAt(exact);
571
600
  }
572
601
  if ((value & 1) === 1) {
573
- const masked = map.get(value & ~1)?.[0];
574
- if (masked?.kind === 'code') {
575
- return masked;
602
+ const code = map.get(value & ~1)?.find((info) => info.kind === 'code');
603
+ if (code) {
604
+ return code;
576
605
  }
577
606
  }
578
607
  return null;
@@ -580,10 +609,11 @@ export function lookupSymbol(map: SymbolMap, value: number): SymbolInfo | null {
580
609
 
581
610
  /** Interior attribution: the data symbol whose `[address, address+size)` range contains
582
611
  * `value` strictly inside (offset > 0 — exact bases go through `lookupSymbol`). Only
583
- * 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. */
584
614
  export function lookupInterior(map: SymbolMap, value: number): { info: SymbolInfo; offset: number } | null {
585
615
  for (const [addr, infos] of map) {
586
- const info = infos[0];
616
+ const info = objectAt(infos);
587
617
  if (info.kind !== 'data' || info.size === undefined) {
588
618
  continue;
589
619
  }
@@ -596,9 +626,16 @@ export function lookupInterior(map: SymbolMap, value: number): { info: SymbolInf
596
626
 
597
627
  /** The `SymbolInfo` keys a project's DWARF can only carry because the symbol's DEFINITION was
598
628
  * compiled from C. Everything else in the map survives a function that is still `INCLUDE_ASM`:
599
- * its `.symtab` entry exists (the asm defines the label), and its globals are typed by the OTHER
600
- * translation units that declare them. Listed here, once, so {@link asIfUndecompiled} and any
601
- * 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. */
602
639
  const DEFINITION_DERIVED_KEYS = ['declared', 'signature'] as const satisfies readonly (keyof SymbolInfo)[];
603
640
 
604
641
  /** The map a user actually has while decompiling `fn` — i.e. with `fn` still an `INCLUDE_ASM`
package/src/target.ts CHANGED
@@ -5,7 +5,9 @@
5
5
  // What each datum drives:
6
6
  // • id → frontend dispatch (registry.ts)
7
7
  // • compiler → idiom gating (patternApplies) + the report
8
- // • argRegs / returnReg → entry-param ordering and return-value read in the frontends
8
+ // • argRegs / returnReg → entry-param ordering and return-value read in the frontends; argRegs
9
+ // also says whether an ARM body's argument scan counts this target's words
10
+ // (frontend/arm-body.ts `scansArgumentsOf`, read by frontend/callee-body.ts)
9
11
  // • capabilities.hwDivide → gates the MIPS hardware-divide decode (mips.ts), the soft-division
10
12
  // pre-recovery pass, and idiom gating; a `div` on a target declaring no divider degrades to
11
13
  // a loud opaque (exercised by packages/cli/test/matching/divmul.test.ts). `hwFloat` → idiom
@@ -45,7 +47,7 @@
45
47
  // `aggregateReturn`, `largestAlignment`, `enumBytes`, `bitfieldPacking` and
46
48
  // `byValueAggregateArgWords` (aggregate.ts, for frontend/call-plan.ts — which the Thumb, MIPS
47
49
  // and PowerPC lowerings share — frontend/thumb.ts, frontend/mips.ts, frontend/ppc.ts and
48
- // frontend/frame-objects.ts),
50
+ // frontend/frame-objects.ts; `enumBytes` is the structurer's too, through StructureOptions),
49
51
  // `hoistsSingleSetArm` (raise/narrowlocal.ts and raise/retsink.ts), `arrayShapeFromStride`
50
52
  // (raise/globalshape.ts, run on the LIFTED fn), `eightByteReturnScratch`
51
53
  // (frontend/thumb.ts, which reads the epilogue), `callThunks` (frontend/thumb.ts, which
@@ -701,7 +703,7 @@ export interface TargetDescription {
701
703
  oneWordFrameIsTheCapturedObject?: boolean;
702
704
  // A frame address this function hands out — in an argument register at a call, or stored to
703
705
  // memory — is the address of a LOCAL, so its receiver may read the frame from there up and no
704
- // word from there up is a call's outgoing argument. Its one reader is the outgoing-argument
706
+ // word from there up is a call's outgoing argument. Its first reader is the outgoing-argument
705
707
  // analysis (`frontend/stackargs.ts`, the `escape` event): a store never reloaded HERE is read
706
708
  // THERE. Absent ⇒ a handed-out frame address explains nothing.
707
709
  //
@@ -719,8 +721,30 @@ export interface TargetDescription {
719
721
  // flow-insensitively, it would set aside `qh5`'s fifth `h5` word and lift `h5(1, 2, 3, 4)`. The
720
722
  // cost is false alarms that decline loud (`if (x) gt(&s); else h(y);`); where the analysis also
721
723
  // reads the event whole-function, it only refuses or excuses a word no call takes
722
- // (stackargs.ts). `oneWordFrameIsTheCapturedObject` is the flow-insensitive licence, sound only
723
- // where no block copy fits.
724
+ // (stackargs.ts). `oneWordFrameIsTheCapturedObject` is the flow-insensitive licence for any
725
+ // receiver, sound only where no block copy fits.
726
+ //
727
+ // Its SECOND reader is the Thumb frame model's [sp,#0] capture (`frontend/thumb.ts`, beside
728
+ // `noCallTakesAStackWord`), which reads the universal whole-function at offset 0: where the
729
+ // frame base is handed as the hidden pointer of a callee DECLARED to return a struct through
730
+ // memory, it is a local's address, so no word of the frame is any call's outgoing argument and
731
+ // the area is empty — whatever the other calls are (sret2knr: `u8 rnd();` beside two temps).
732
+ // This premise is read only where some call may take a stack word: where none does there is
733
+ // no outgoing area to confuse, and an escaping frame base is a local's address on any target,
734
+ // whoever receives it (a DMA source published to the device's registers, `dmafill_wh`).
735
+ // The residue cannot reach it: a block copy's base goes to `memcpy`, no struct-returning
736
+ // callee. That reading is agbcc's layout, and is measured beyond `f0`: thumb.h:573
737
+ // STARTING_FRAME_OFFSET 0 and :620-621 add `current_function_outgoing_args_size` to every
738
+ // elimination to sp (:628 ACCUMULATE_OUTGOING_ARGS); function.c:2629 puts STACK_DYNAMIC_OFFSET
739
+ // past the same size; calls.c:1598-1609 precomputes into a temp every argument containing a
740
+ // call whenever the call has stack arguments, so a struct result is never built straight into
741
+ // an outgoing word. Compiled at the rows' flags: `mk(..)` by value as the 5th argument, as the
742
+ // 1st followed by four ints, `mk(..).a` as the 5th, a 20-byte struct by value, and a 5-argument
743
+ // K&R `rnd(1, 2, 3, 4, 5)` beside a temp each put the temp at sp+4 or higher, with [sp,#0] the
744
+ // outgoing word. A frame whose sp moves between two accesses is refused before this is read
745
+ // (thumb.ts `frameDepthBlocker`). Hand-written asm that builds a struct return at [sp,#0] and
746
+ // also stores a stack argument there declines; one that hands that word, unstored, to an
747
+ // undeclared callee as its fifth argument loses it from the callee's guessed arity.
724
748
  escapedFrameAddressNamesALocal?: boolean;
725
749
  // WHERE AN O32 FUNCTION KEEPS ITS LOCALS in the one frame `addiu sp,sp,-N` reserves — the area
726
750
  // the MIPS frontend hands the frame-object audit as `ownedLocals`/`declaredLocals`. Below it
@@ -1099,6 +1123,16 @@ export interface TargetDescription {
1099
1123
  // (proto-context.ts). ABSENT ⇒ unmeasured,
1100
1124
  // and an aggregate with an enum member is not sized.
1101
1125
  enumBytes?: number;
1126
+ // Whether a plain `char` is signed, read where a context declares a global `char` and the lift
1127
+ // asks whether a narrow read of it agrees with that declaration (structure.ts
1128
+ // `storageDisagrees`). The compiler's default, with no flag; `targetFor` applies the flags that
1129
+ // change it (`-funsigned-char`/`-fsigned-char`, IDO's `-signed`, mwcc's `-char`). MEASURED,
1130
+ // `s32 g(void){ return gC; }` over `extern char gC;` at each toolchain's canonical flags: agbcc
1131
+ // `ldrb` and ido7.1 `lbu` (unsigned), gcc2.7.2 `lb` (signed; gcc2.7.2kmc's canonical
1132
+ // `-funsigned-char` makes it `lbu`), mwcc_242_81 `lbz` + `extsb` (signed). ABSENT ⇒ unmeasured,
1133
+ // and a narrow read of a declared plain `char` agrees with neither sign, so it is spelled
1134
+ // through its address.
1135
+ plainCharSigned?: boolean;
1102
1136
  // How a struct's bitfields are placed (`aggregate.ts`). 'contiguous' — each at the bit after the
1103
1137
  // one before, straddling a byte or a word of its declared type, with the next member that is not
1104
1138
  // a bitfield at the first byte past it that its alignment allows. agbcc: thumb.h defines no
@@ -1149,6 +1183,23 @@ export interface TargetDescription {
1149
1183
  // ABSENT ⇒ false: a sign-extending load says nothing about the qualifier, and a device read is
1150
1184
  // pinned, and a read of a declared object placed, whatever its extension.
1151
1185
  volatileReadsExtendInRegister?: boolean;
1186
+ // Does this compiler RELOAD a cell stored through one spelling and read back through another —
1187
+ // two names, or a name and a numeric address, for the same byte? MEASURED at -O2, each probe
1188
+ // storing 5 to a word, 7 to one of its bytes, and reading the word back: IDO 7.1 reloads
1189
+ // (`D_80100000 = 5; ((u8 *)0x80100000)[1] = 7; return D_80100000;` is `sb; jr ra; lw v0`);
1190
+ // agbcc (`sRandCount = 5; ((u8 *)&sUnknown)[4] = 7; return sRandCount;` is `strb; mov r0,
1191
+ // #5`) and gcc 2.7.2 (`sb; jr ra; li v0, 5`) forward the stored value. IDO also reloads across
1192
+ // two externs, two statics, an array element beside a scalar, and a word-wide literal beside
1193
+ // a name. It reloads only a read that FOLLOWS the store: a read placed before a byte store
1194
+ // through another name is sunk past it (`v0 = D_80100004; ((u8 *)&D_80100000)[5] = 7;
1195
+ // return v0;` is `sb; jr ra; lw v0`). So where it holds, a pair of spellings in one block
1196
+ // outside any loop, the store first, is faithful lifted as-is, whether the asm carries both or
1197
+ // the map chose one; any other order, and every pair where it does not hold (an asm that went
1198
+ // back to memory proves the source did not spell the byte two ways), raise/offsetnames.ts
1199
+ // `cellSpelledTwice` declines.
1200
+ //
1201
+ // ABSENT ⇒ false: the compiler forwards (or is unmeasured, which declines the same pairs).
1202
+ reloadsAcrossSpellings?: boolean;
1152
1203
  // Does this compiler, AT THESE FLAGS, keep a plain read whose value nothing uses? A FLAG fact,
1153
1204
  // never written in a description: `targetFor` sets it from the optimisation level
1154
1205
  // (`keepsUnusedReadsAt`), so a target at other flags has it absent. Where it holds, a read
@@ -1200,6 +1251,120 @@ export interface TargetDescription {
1200
1251
  // load decides only where this is `false`. ABSENT ⇒ unmeasured, and read as `true`, which only
1201
1252
  // refuses more.
1202
1253
  discardedReadMayFillArgRegister?: boolean;
1254
+ // DOES THE COMPILER CROSS-JUMP RETURN TAILS? Where two arms end in the same code before the
1255
+ // same return, does it keep ONE copy and jump the other arm into it? Where it does, the source
1256
+ // may have written the tail into every arm (`h(10, 0); return;` in each) and the object still
1257
+ // holds it once. So a join in the CFG is no evidence the source wrote one. What tells the two
1258
+ // spellings apart is where the surviving copy sits, and which arms became one block. Read by
1259
+ // the return-tail chooser (pipeline.ts `structureReturnTailReading`): it publishes the copies
1260
+ // where the target's layout shows that merge (structure/cross-jumped-tail.ts), and never reads
1261
+ // the layout where this is absent.
1262
+ //
1263
+ // gcc 2.x's jump2 pass does it: jump.c, in the `JUMP_CROSS_JUMP` run of `jump_optimize` that
1264
+ // toplev.c makes at `optimize > 0`.
1265
+ // - An unconditional jump is first cross-jumped into the code before its own target label
1266
+ // (`find_cross_jump (insn, JUMP_LABEL (insn), 1)`).
1267
+ // - Only then is it tried against the other jumps to that label, and that search runs only
1268
+ // `if (INSN_UID (JUMP_LABEL (insn)) < max_uid)`.
1269
+ // - `do_cross_jump` keeps the LATER copy and jumps to a label before it. `get_label_before`
1270
+ // creates that label, so a jump already redirected is never searched for siblings again.
1271
+ // So the copies spelling leaves three shapes in the object (structure/cross-jumped-tail.ts):
1272
+ // - an arm that does not use the tail stays laid out after the surviving copy, and that copy
1273
+ // ends in a jump over it;
1274
+ // - identical arm bodies stay separate blocks, where the join's one original label would have
1275
+ // merged them;
1276
+ // - `find_cross_jump` compares backward and stops at the first pair that differs, so a call
1277
+ // the tail opens with can be cut from the argument setup each arm keeps.
1278
+ // MEASURED on two probe pairs, `corpus/probe-rettail-{pa,pb}-{copies,join}.c`: a three-arm `if`
1279
+ // and a seven-case `switch`. Each is compiled at each toolchain's canonical flags by
1280
+ // `scripts/regen-rettail-probes.ts`, and cross-jumped-tail.test.ts reads the objects:
1281
+ // - agbcc: the `pa` copies keep the tail mid-construct, ending in `b`, with the else arm laid
1282
+ // out after it. The `pb` copies hold the tail's store ONCE, in six case bodies; the join
1283
+ // has three bodies.
1284
+ // - gcc2.7.2 (-O1) and gcc2.7.2kmc (-O2): `pb` is the same, one store, six bodies against
1285
+ // three. The `pa` copies and join compile byte-identical, because the fall-through arm's
1286
+ // call merged too.
1287
+ // - ido7.1 and mwcc_242_81: NO. The `pb` copies keep all seven stores, each before its own
1288
+ // return, and the `pa` copies keep three calls.
1289
+ // So `true` on ARMV4T_AGBCC and on MIPS_GCC. Each toolchain MIPS_GCC serves owes its own pair
1290
+ // (the KEYED BY DESCRIPTION note at `spillSlotOrder`), and both are committed. ABSENT means no
1291
+ // such merge: the chooser's size bound alone decides. mwcc_233_163n and mwcc_247_107 are
1292
+ // unprobed, and PPC_MWCC claims nothing for them. Read only where the join is the default
1293
+ // (`joinPastReturnsByDefault`), which both descriptions declaring this field leave out, so the
1294
+ // marks decide no shipped default; the probe pairs still pin each one.
1295
+ crossJumpsReturnTails?: boolean;
1296
+ // DOES THE COMPILER FOLD THE CONSTANT A BRANCH PROVED INTO A RETURN ON THE BRANCH'S TAKEN EDGE?
1297
+ // After `if (x == 0)`, a `return x` the jump reaches is `return 0` where it does. gcc 2.x's cse
1298
+ // records the branch's equivalence on the NOT-taken edge of every jump (`record_jump_equiv
1299
+ // (insn, 0)`, cse.c:7597), and on the taken edge only along a path it follows (`record_jump_equiv
1300
+ // (insn, 1)`, cse.c:8545). It follows one only under -fcse-follow-jumps, which toplev.c:3606-3607
1301
+ // sets at `optimize >= 2` (-fcse-skip-blocks skips a block without recording the edge:
1302
+ // `cseFollowsJumpsUnder`). (Line numbers are agbcc's gcc 2.9 tree; for
1303
+ // 2.7.2 the probe below is the evidence.) So where this is `false`, a return of the tested value
1304
+ // left UNFOLDED in the object was the jump-target arm of its branch in the source: an `else`.
1305
+ // The join past early returns spells it `if (x == 0) return x;`, the then arm, which folds.
1306
+ // Read by the return-tail chooser (pipeline.ts `structureReturnTailReading`), which publishes the
1307
+ // copies at such a return (structure/unfolded-branch-constant.ts), and only where the join is
1308
+ // the default (`joinPastReturnsByDefault`), which MIPS_GCC, the one description declaring this
1309
+ // field, leaves out: so it decides no shipped default, and the probes below still pin it.
1310
+ // `false` only by the flags (`targetFor`), never declared.
1311
+ //
1312
+ // MEASURED on the probe pair `corpus/probe-rettail-pg-{copies,join}.c`, the `return gB` after a
1313
+ // `gB` tested against 0, compiled by scripts/regen-rettail-probes.ts. The `beqz`'s delay slot:
1314
+ // - gcc2.7.2 at -O1 (canonical): copies `nop`, join `move v0,zero`. With the arms of the
1315
+ // copies flipped (the return the then arm), and with `-O1 -fcse-follow-jumps`, `move v0,zero`.
1316
+ // - gcc2.7.2 at -O2, and gcc2.7.2kmc at -O2 (canonical): `move v0,zero` under every spelling.
1317
+ // So `true` on MIPS_GCC, and `false` from the flags where cse follows no jump (level 1 without
1318
+ // -fcse-follow-jumps, or -fno-cse-follow-jumps: `cseFollowsJumpsUnder`). Absent elsewhere: agbcc
1319
+ // at -O2 re-loads `gB` for the copies' `else` and does not fold it, which no value of this field
1320
+ // says; ido7.1 and mwcc are unprobed.
1321
+ foldsBranchConstantOnTakenEdge?: boolean;
1322
+ // IS THE JOIN PAST EARLY RETURNS THE DEFAULT READING HERE? Not a compiler behaviour but a MEASURED
1323
+ // DEFAULT, kept beside the behaviours because it is per compiler and read by the chooser they
1324
+ // feed (pipeline.ts `structureReturnTailReading`), which the size bound and the marks above
1325
+ // steer only where this is `true`. ABSENT ⇒ the return-tail copies are published, as before the
1326
+ // join was a default, and the join is still structured and kept in the ranked fan as
1327
+ // `/ret-join`. A compiler the held-out measurement has not reached publishes the copies first,
1328
+ // and the join only where the copies refuse, so it lifts every function it lifted before and
1329
+ // may lift one the old default declined (pipeline.ts `copiesByDefault`).
1330
+ // docs/level-tower.md (A MEASURED DEFAULT IS THE ONE EXCEPTION) says why a field whose readings
1331
+ // split site by site may still be declared here, and what re-measures it.
1332
+ //
1333
+ // MEASURED over held-out real functions, every function of a project outside the 620 the marks
1334
+ // were chosen on, both readings really compiled against the project's own objects (2026-11-06),
1335
+ // and over 160 generated functions written with the copies (2026-11-09). `true` only where the
1336
+ // size bound with every mark loses no match the copies have on every measured set:
1337
+ // - mwcc_233_163n (Pikmin at its own flags, 2733 functions lifted, 66 re-spelled): 0 lost,
1338
+ // 3 gained (two `actByEvent` methods 4/22 -> 0/20, `stickSlot__6PelletFi` 3/28 -> 0/26);
1339
+ // the generated functions: every source identical to the copies';
1340
+ // - mwcc_242_81 (ac-decomp and Mario Party 4, every unit compiled at this build although 9 of
1341
+ // Mario Party 4's rows are mwcc_247_107; 141 re-spelled): 0 lost, 0 gained; the generated
1342
+ // functions: every source identical to the copies'.
1343
+ // So `true` on PPC_MWCC (mwcc_242_81) and PPC_MWCC_233; PPC_MWCC_247 (mwcc_247_107) leaves it
1344
+ // out, no held-out unit measured at that build. Left out where it loses, the copies' default
1345
+ // given back with the join's gains:
1346
+ // - gcc2.7.2kmc: held out (Snowboard Kids 2, 78 re-spelled) 0 lost, 2 gained
1347
+ // (`func_8003513C_35D3C`, `func_80045564_46164`); generated, 3 lost (the copies match, the
1348
+ // join is 2 to 8 lines off) and 5 gained. Given up: those 2 held out and the 5 generated.
1349
+ // - gcc2.7.2: held out (Mario Party 3, all 10019 functions, 150 re-spelled) 0 lost once the
1350
+ // unfolded return is read (`foldsBranchConstantOnTakenEdge`), none gained; generated, 1 lost
1351
+ // (4 lines off) and 2 gained. No mark read from the layout or the unfolded return separates
1352
+ // the 4 generated losses on the two lanes. Given up: the 2 generated, and the held-out and
1353
+ // 620 functions the join brought closer.
1354
+ // - agbcc (pokeemerald, 15062 functions, 963 re-spelled): 9 lost, 18 gained. None of the 9 is
1355
+ // marked: switch and comparison-tree sites, a call cross-jumped across arms that pass it
1356
+ // different arguments, and a third source spelling the copies happen to share. Given up:
1357
+ // 18 held out and 2 of the 620.
1358
+ // - ido7.1 (Animal Forest, 11363 functions, 252 re-spelled): 4 lost, one shape, 18 gained. ido
1359
+ // never cross-jumps, so no cross-jump mark applies. Counting copies of the arm's store does
1360
+ // not separate the readings either: compiled, the join of `func_80A42DB4_jp` ALSO keeps two
1361
+ // `sw zero,0(a3)`, because ido fills a `beql`'s likely slot with a copy of its target's first
1362
+ // word. What differs is the branch the slot copy rides: the copies' arm schedules its own
1363
+ // store into the slot of a plain `b`, the join's copy sits in a `beql`'s. That is the
1364
+ // candidate signal, unmeasured: frontend/mips.ts `normaliseBranchLikely` rewrites both to one
1365
+ // shape (its `likely` and `freed` sets still tell them apart) before the layout is recorded,
1366
+ // so no layout entry carries it. Given up: 18 held out and 7 of the 620.
1367
+ joinPastReturnsByDefault?: boolean;
1203
1368
  };
1204
1369
  }
1205
1370
 
@@ -1335,6 +1500,10 @@ export const ARMV4T_AGBCC: TargetDescription = {
1335
1500
  switchBoundCase: 'taken',
1336
1501
  switchArmsFollowLayout: true,
1337
1502
  switchRequiresFrontLoadedTests: true,
1503
+ // MEASURED on the probe pairs at the field (gcc 2.x jump.c's cross-jump). Read only where the join
1504
+ // is the default (`joinPastReturnsByDefault`), which agbcc leaves out: the join loses 9 of
1505
+ // pokeemerald's matches no mark separates.
1506
+ crossJumpsReturnTails: true,
1338
1507
  hoistsSingleSetArm: true,
1339
1508
  eightByteReturnScratch: 'r2',
1340
1509
  arrayShapeFromStride: true,
@@ -1355,6 +1524,7 @@ export const ARMV4T_AGBCC: TargetDescription = {
1355
1524
  discardedReadMayFillArgRegister: true,
1356
1525
  byValueAggregateArgWords: true,
1357
1526
  enumBytes: 4,
1527
+ plainCharSigned: false,
1358
1528
  bitfieldPacking: 'contiguous',
1359
1529
  roundTripsDoubleLiterals: true,
1360
1530
  // agbcc 2.9 (gcc/varasm.c `assemble_variable`, gcc/thumb.h): an array takes its element's
@@ -1512,6 +1682,7 @@ export const MIPS_IDO: TargetDescription = {
1512
1682
  // `switchAllowsNeqCase: false` — IDO's switch dispatch uses `==`/`<`, never `!=` cases;
1513
1683
  // leaving it permissive mis-recognises `!=`-rooted if-else chains as switches.
1514
1684
  compilerBehaviors: {
1685
+ plainCharSigned: false,
1515
1686
  contractsFloatProducts: false,
1516
1687
  coalesceLoopInit: true,
1517
1688
  preserveDivergentBranchSense: true,
@@ -1519,6 +1690,8 @@ export const MIPS_IDO: TargetDescription = {
1519
1690
  switchAllowsNeqCase: false,
1520
1691
  // MEASURED — the pair at the field compiles to one load of `p[1]` for every local spelling.
1521
1692
  reloadsLocalReread: false,
1693
+ // MEASURED: the probe at the field (`lw v0` after the byte store).
1694
+ reloadsAcrossSpellings: true,
1522
1695
  // MEASURED: the probe at the field.
1523
1696
  narrowStoreConstantTakesLvalueType: true,
1524
1697
  aggregateBoundary: 1,
@@ -1643,6 +1816,7 @@ export const MIPS_GCC: TargetDescription = {
1643
1816
  fpu: O32_FPU,
1644
1817
  capabilities: { endianness: 'big', hwDivide: true, hwFloat: true, flags: false },
1645
1818
  compilerBehaviors: {
1819
+ plainCharSigned: true,
1646
1820
  contractsFloatProducts: false,
1647
1821
  coalesceLoopInit: true,
1648
1822
  preserveDivergentBranchSense: true,
@@ -1682,6 +1856,15 @@ export const MIPS_GCC: TargetDescription = {
1682
1856
  // sub-trees a decline leaves, so a NESTED dispatch comes back as an `if` nest around a `switch`
1683
1857
  // over some of its arms.
1684
1858
  switchRequiresFrontLoadedTests: true,
1859
+ // MEASURED on BOTH toolchains this description serves, the probe pairs at the field.
1860
+ crossJumpsReturnTails: true,
1861
+ // MEASURED at -O2 on both toolchains this description serves, the probe pair at the field;
1862
+ // gcc2.7.2's canonical -O1 clears it (`targetFor`).
1863
+ foldsBranchConstantOnTakenEdge: true,
1864
+ // `joinPastReturnsByDefault` LEFT OUT, so the copies are published and the join is ranked as
1865
+ // `/ret-join`. On held-out real functions it lost none of the copies' matches on either toolchain
1866
+ // this description serves, but on 160 generated functions written with the copies it lost 3 on
1867
+ // gcc2.7.2kmc and 1 on gcc2.7.2, which no mark separates (the field's note).
1685
1868
  // MEASURED on BOTH toolchains this description serves (the note above): one load of `p[1]` for
1686
1869
  // every local spelling of the pair at the field, gcc2.7.2kmc at -O2 and gcc2.7.2 at -O1 alike.
1687
1870
  reloadsLocalReread: false,
@@ -1778,6 +1961,7 @@ export const PPC_MWCC: TargetDescription = {
1778
1961
  // defaults; coalesceLoopInit false until a CW loop fixture says otherwise — the second of the
1779
1962
  // two compiler-wide guesses standing in for the per-function observation named at MIPS_GCC.
1780
1963
  compilerBehaviors: {
1964
+ plainCharSigned: true,
1781
1965
  contractsFloatProducts: true,
1782
1966
  // MEASURED on all three builds in C: an integer, a pointer and a `u16 *` passed to an `s32 *`,
1783
1967
  // `u32` and `s32 *` parameter each fail to compile, and each compiles once cast (see the field).
@@ -1849,12 +2033,31 @@ export const PPC_MWCC: TargetDescription = {
1849
2033
  // MEASURED on all three builds: the probes at the fields.
1850
2034
  forwardsStructReturnDestination: false,
1851
2035
  discardedReadMayFillArgRegister: true,
2036
+ // MEASURED over held-out real functions at mwcc_242_81 and mwcc_233_163n, at the field;
2037
+ // PPC_MWCC_247 leaves it out.
2038
+ joinPastReturnsByDefault: true,
1852
2039
  },
1853
2040
  };
1854
2041
 
2042
+ /** A description's behaviours less `joinPastReturnsByDefault`, for a build that shares the
2043
+ * description but was not measured for that default. */
2044
+ function withoutJoinDefault(d: TargetDescription): TargetDescription['compilerBehaviors'] {
2045
+ const { joinPastReturnsByDefault: _measuredElsewhere, ...behaviors } = d.compilerBehaviors;
2046
+ return behaviors;
2047
+ }
2048
+
1855
2049
  /** CodeWarrior 2.3.3b163n (Pikmin): `PPC_MWCC` but for the one declaration a probe refuted there —
1856
- * it rejects a global register variable ("illegal storage class"), so it has no spelling for one. */
1857
- export const PPC_MWCC_233: TargetDescription = { ...PPC_MWCC, globalRegisterVariables: undefined };
2050
+ * it rejects a global register variable ("illegal storage class"), so it has no spelling for one.
2051
+ * The return-tail default is PPC_MWCC's, measured at this build over Pikmin
2052
+ * (`joinPastReturnsByDefault`). */
2053
+ export const PPC_MWCC_233: TargetDescription = {
2054
+ ...PPC_MWCC,
2055
+ globalRegisterVariables: undefined,
2056
+ };
2057
+
2058
+ /** CodeWarrior 2.4.7b107 (Mario Party 4's other build): `PPC_MWCC` less the return-tail default no
2059
+ * held-out function measured at this build (`joinPastReturnsByDefault`). */
2060
+ export const PPC_MWCC_247: TargetDescription = { ...PPC_MWCC, compilerBehaviors: withoutJoinDefault(PPC_MWCC) };
1858
2061
 
1859
2062
  /** A toolchain: one compiler binary, named as decomp.me names it. Several toolchains may share a
1860
2063
  * description (`MIPS_GCC` serves two), and each keeps its own evidence. */
@@ -1946,7 +2149,7 @@ export const TOOLCHAIN_TARGETS = {
1946
2149
  },
1947
2150
  mwcc_247_107: {
1948
2151
  family: 'mwcc',
1949
- description: PPC_MWCC,
2152
+ description: PPC_MWCC_247,
1950
2153
  },
1951
2154
  } as const satisfies Readonly<Record<string, ToolchainTarget>>;
1952
2155
 
@@ -2038,9 +2241,67 @@ export function targetFor(toolchain: ToolchainId, cflags: readonly string[]): Re
2038
2241
  const { enumBytes: _flagged, ...behaviors } = target.compilerBehaviors;
2039
2242
  target = { ...target, compilerBehaviors: behaviors };
2040
2243
  }
2244
+ if (target.compilerBehaviors.foldsBranchConstantOnTakenEdge === true && !cseFollowsJumpsUnder(profile)) {
2245
+ target = { ...target, compilerBehaviors: { ...target.compilerBehaviors, foldsBranchConstantOnTakenEdge: false } };
2246
+ }
2247
+ const charSigned = plainCharSignedUnder(t.family, profile.slots);
2248
+ if (charSigned !== 'default' && charSigned !== target.compilerBehaviors.plainCharSigned) {
2249
+ const { plainCharSigned: _default, ...behaviors } = target.compilerBehaviors;
2250
+ target = {
2251
+ ...target,
2252
+ compilerBehaviors: charSigned === undefined ? behaviors : { ...behaviors, plainCharSigned: charSigned },
2253
+ };
2254
+ }
2041
2255
  return { toolchain, cflags, target, profile };
2042
2256
  }
2043
2257
 
2258
+ /** Does gcc's cse follow jumps at these flags (`compilerBehaviors.foldsBranchConstantOnTakenEdge`)?
2259
+ * Only a jump cse FOLLOWS gets its taken edge's equivalence: `cse_end_of_basic_block` marks a path
2260
+ * branch TAKEN only under `follow_jumps` (cse.c:8192-8211), and the branch around a block that
2261
+ * `skip_blocks` adds is AROUND, whose block is invalidated, not recorded (cse.c:8223, :8545-8548).
2262
+ * toplev.c:3606-3607 sets `flag_cse_follow_jumps` at `optimize >= 2`, and the switch sets or clears
2263
+ * it by name either way, the level's value standing where it is not given. -O0 is answered `true`:
2264
+ * cse does not run there, which is a different compiler again, and a level no table reads keeps
2265
+ * the declaration. (cse.c line numbers are agbcc's gcc 2.9 tree.)
2266
+ *
2267
+ * MEASURED on `corpus/probe-rettail-pg` at gcc2.7.2, the copies' `beqz` delay slot: `nop` (unfolded)
2268
+ * at -O1, at `-O1 -fcse-skip-blocks`, at `-O2 -fno-cse-follow-jumps` and at `-O2
2269
+ * -fno-cse-follow-jumps -fno-cse-skip-blocks`; `move v0,zero` at -O2, at `-O2 -fno-cse-skip-blocks`
2270
+ * and at `-O1 -fcse-follow-jumps`. The join is `move v0,zero` under all seven. */
2271
+ function cseFollowsJumpsUnder(profile: CodegenProfile): boolean {
2272
+ if (profile.family !== 'gcc' && profile.family !== 'agbcc') {
2273
+ return true;
2274
+ }
2275
+ if (profile.slots.O === '0') {
2276
+ return true;
2277
+ }
2278
+ const flag = profile.slots['-fcse-follow-jumps'];
2279
+ return flag === 'on' ? true : flag === 'off' ? false : profile.slots.O !== '1';
2280
+ }
2281
+
2282
+ /** What the flags say a plain `char` is (`compilerBehaviors.plainCharSigned`): `default` where they
2283
+ * leave it to the compiler. gcc keeps one flag, `flag_signed_char`, that `-fsigned-char` and
2284
+ * `-fno-unsigned-char` set and `-funsigned-char` and `-fno-signed-char` clear, the last one given
2285
+ * winning; the profile keeps the two switches apart, so where both are given and they disagree the
2286
+ * order is lost and the answer is undefined. IDO's `-signed` makes it signed, and mwcc's `-char`
2287
+ * takes `signed` or `unsigned`. */
2288
+ function plainCharSignedUnder(
2289
+ family: FlagFamily,
2290
+ slots: Readonly<Record<string, string>>,
2291
+ ): boolean | undefined | 'default' {
2292
+ if (family === 'ido') {
2293
+ return slots.char === 'signed' ? true : 'default';
2294
+ }
2295
+ if (family === 'mwcc') {
2296
+ return slots.char === 'signed' ? true : slots.char === 'unsigned' ? false : 'default';
2297
+ }
2298
+ const said = [
2299
+ slots['-fsigned-char'] === undefined ? undefined : slots['-fsigned-char'] === 'on',
2300
+ slots['-funsigned-char'] === undefined ? undefined : slots['-funsigned-char'] === 'off',
2301
+ ].filter((x) => x !== undefined);
2302
+ return said.length === 0 ? 'default' : said.every((x) => x === said[0]) ? said[0] : undefined;
2303
+ }
2304
+
2044
2305
  /** Build the structurer's options for a target: the function's own `returnsVoid` plus every
2045
2306
  * `compilerBehaviors` field. The ONE place a target's compiler behaviors flow into the
2046
2307
  * target-agnostic structurer — a new compiler behavior is a field in `compilerBehaviors`, consumed
package/src/trace.ts CHANGED
@@ -7,12 +7,20 @@
7
7
  import { cBackend } from './backend/c';
8
8
  import type { CodegenProfile } from './codegen-flags';
9
9
  import type { AsmData } from './frontend/asmdata';
10
+ import type { CalleeBodies } from './frontend/callee-body';
10
11
  import type { Fn } from './ir/core';
11
12
  import { print } from './ir/print';
12
13
  import { verify } from './ir/verify';
13
14
  import type { LanguageBackend } from './l3/ast';
14
15
  import { DEFAULT_IDIOM_PATTERNS, RewritePattern, applyPattern, dce, patternApplies } from './pattern/engine';
15
- import { type OnGap, liftStamped, raiseRecovered, stampBehaviorsFor, structureChecked, stubResult } from './pipeline';
16
+ import {
17
+ type OnGap,
18
+ liftStamped,
19
+ offsetNameBehaviorsFor,
20
+ raiseRecovered,
21
+ structureReturnTailReading,
22
+ stubResult,
23
+ } from './pipeline';
16
24
  import { type FnProto, type Prototypes, declaresVoidReturn, prototypesFromSymbols } from './proto';
17
25
  import { type ContextGlobals, splitOwnContext } from './proto-context';
18
26
  import { assumedShapes, inferGlobalArrays, orderLicensedGlobals } from './raise/globalshape';
@@ -95,6 +103,7 @@ export interface TraceOptions {
95
103
  symbols?: SymbolMap; // address→symbol map (symbols.ts), as in decompile(); absent ⇒ inert
96
104
  contextGlobals?: ContextGlobals; // the declaration context's globals, as in decompile()
97
105
  ownContextProto?: FnProto; // the context's own entry, as in decompile()
106
+ calleeBodies?: CalleeBodies; // bodies of callees outside the input, as in decompile()
98
107
  onGap?: OnGap; // "strict" (default) | "annotate", as in decompile()
99
108
  /** Source probe at pattern boundaries: the source asmlift would emit there, which the cli report
100
109
  * scores. One call per boundary: pattern N's after-source is pattern N+1's before-source. Absent ⇒
@@ -207,7 +216,7 @@ function traceTower(
207
216
  opts.asmData,
208
217
  opts.symbols,
209
218
  opts.contextGlobals,
210
- undefined,
219
+ { calleeBodies: opts.calleeBodies },
211
220
  ownContext.ownContextProto,
212
221
  );
213
222
  trace.push({ id: 'stage:lift', title: 'Lift (ISA frontend → typed-SSA IR)', irDump: irDump(fn), verified: true });
@@ -234,9 +243,9 @@ function traceTower(
234
243
 
235
244
  // The names the map holds for addresses this machine code built by arithmetic off a named one
236
245
  // (raise/offsetnames.ts), off the same lifted fn and for the same reason as pipeline.ts's
237
- // runTower.
246
+ // runTower — and its one-spelling-per-cell decline, which throws from here (pipeline.ts's 1.6).
238
247
  const named = opts.symbols
239
- ? OFFSET_NAME_PASS.run(fn, opts.symbols, stampBehaviorsFor(target, opts.contextGlobals))
248
+ ? OFFSET_NAME_PASS.run(fn, opts.symbols, offsetNameBehaviorsFor(target, opts.contextGlobals))
240
249
  : [];
241
250
  trace.push({
242
251
  id: 'stage:offsetnames',
@@ -332,16 +341,21 @@ function traceTower(
332
341
  // (4) structure → neutral AST; boundary contract: no unresolved value leaked (strict) or
333
342
  // spelled as a loud ASMLIFT_ERROR marker (annotate) — same onGap option as decompile()
334
343
  const mapSymbols = opts.symbols ? symbolsByName(opts.symbols) : undefined;
335
- const sfn = structureChecked(fn, {
336
- ...structureOptionsFor(target, returnsVoid, prototypes),
337
- spellSwitchFallthrough: (opts.backend ?? cBackend).spellsSwitchFallthrough,
338
- spellSwitchBreak: (opts.backend ?? cBackend).spellsSwitchBreak,
339
- onGap: opts.onGap ?? 'strict',
340
- ...(mapSymbols ? { symbols: mapSymbols } : {}),
341
- ...(opts.contextGlobals ? { contextGlobals: opts.contextGlobals } : {}),
342
- ...(inferredSymbols.size ? { inferredSymbols } : {}),
343
- ...(orderLicensed.size ? { orderLicensedGlobals: orderLicensed } : {}),
344
- });
344
+ const { sfn } = structureReturnTailReading(
345
+ fn,
346
+ {
347
+ ...structureOptionsFor(target, returnsVoid, prototypes),
348
+ spellSwitchFallthrough: (opts.backend ?? cBackend).spellsSwitchFallthrough,
349
+ spellSwitchBreak: (opts.backend ?? cBackend).spellsSwitchBreak,
350
+ spellEarlyReturns: (opts.backend ?? cBackend).spellsEarlyReturn,
351
+ onGap: opts.onGap ?? 'strict',
352
+ ...(mapSymbols ? { symbols: mapSymbols } : {}),
353
+ ...(opts.contextGlobals ? { contextGlobals: opts.contextGlobals } : {}),
354
+ ...(inferredSymbols.size ? { inferredSymbols } : {}),
355
+ ...(orderLicensed.size ? { orderLicensedGlobals: orderLicensed } : {}),
356
+ },
357
+ (t) => backend.emit(t),
358
+ );
345
359
  trace.push({
346
360
  id: 'stage:structure',
347
361
  title: 'Structuring (IR → neutral AST)',