@asmlift/core 0.7.0 → 0.8.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 (44) hide show
  1. package/README.md +48 -24
  2. package/package.json +1 -1
  3. package/src/backend/pascal.ts +2 -2
  4. package/src/codegen-flags.ts +640 -0
  5. package/src/frontend/disasm.ts +141 -11
  6. package/src/frontend/high-half.ts +149 -0
  7. package/src/frontend/mips.ts +458 -209
  8. package/src/frontend/ppc.ts +332 -67
  9. package/src/frontend/reloc-symbol.ts +109 -0
  10. package/src/frontend/splat.ts +56 -18
  11. package/src/frontend/ssa.ts +126 -29
  12. package/src/frontend/stackargs.ts +420 -0
  13. package/src/frontend/thumb.ts +207 -230
  14. package/src/ir/core.ts +62 -3
  15. package/src/ir/opcodes.ts +9 -0
  16. package/src/ir/parse.ts +7 -1
  17. package/src/l3/advance.ts +2 -2
  18. package/src/l3/argbase.ts +2 -2
  19. package/src/l3/argcopy.ts +269 -0
  20. package/src/l3/ast.ts +45 -1
  21. package/src/l3/basecse.ts +2 -2
  22. package/src/l3/coalesce.ts +109 -52
  23. package/src/l3/scopebase.ts +4 -4
  24. package/src/l3/tailret.ts +70 -0
  25. package/src/l3/unmerge.ts +2 -2
  26. package/src/l3/unreduce.ts +2 -1
  27. package/src/mangle.ts +49 -0
  28. package/src/pattern/engine.ts +128 -13
  29. package/src/pipeline.ts +22 -11
  30. package/src/raise/extscale.ts +5 -2
  31. package/src/raise/paramwidth.ts +111 -3
  32. package/src/raise/pre-recovery.ts +11 -1
  33. package/src/raise/retsink.ts +8 -4
  34. package/src/raise/tailsink.ts +17 -2
  35. package/src/rank-declare.ts +17 -9
  36. package/src/rank.ts +45 -19
  37. package/src/structure/retspell.ts +95 -0
  38. package/src/structure/structure.ts +12 -3
  39. package/src/structure/switch-recover.ts +1 -1
  40. package/src/target.ts +224 -14
  41. package/src/trace.ts +27 -18
  42. package/src/variation-definitions.ts +52 -2
  43. package/src/variation-gates.ts +3 -0
  44. package/src/variation-tokens.ts +1 -0
package/src/target.ts CHANGED
@@ -42,8 +42,13 @@
42
42
  // This module is browser-pure by contract (no Node APIs, enforced by
43
43
  // test/browser-safe.test.ts): the toolchain paths that COMPILE for these targets
44
44
  // live in @asmlift/toolchains.
45
+ import { type CodegenProfile, type FlagFamily, parseFlags } from './codegen-flags';
45
46
  import type { StructureOptions } from './structure/structure';
46
47
 
48
+ /** What a compiler's OBJECT shows for a narrow declared parameter — see
49
+ * `compilerBehaviors.narrowParamWitness` for the compiled pair behind each value. */
50
+ export type NarrowParamWitness = 'prologue-extension' | 'home-store-and-in-place' | 'none';
51
+
47
52
  export interface TargetDescription {
48
53
  id: string; // the ISA — 'armv4t' / 'mips' / 'ppc'. Selects the frontend (registry.ts).
49
54
  // The COMPILER is a first-class field distinct from the ISA (matching = deoptimize to a specific
@@ -177,6 +182,32 @@ export interface TargetDescription {
177
182
  // compiler behavior is to claim nothing. The evidence a future round needs is one run of
178
183
  // `scripts/regen-select-spelling-probes.ts` retargeted at the compiler in question.
179
184
  hoistsSingleSetArm?: boolean;
185
+ // WHAT, IN THIS COMPILER'S OBJECT, WITNESSES A NARROW DECLARED PARAMETER — the fact
186
+ // raise/paramwidth.ts needs before it may retype `s32 a0` to `s8 a0`. Three answers, because
187
+ // the compilers measured give three, and the pass refuses wherever the object is silent. Each
188
+ // target below carries its own measurement, and raise/paramwidth.ts's header carries the
189
+ // compiled pairs all three readings rest on.
190
+ //
191
+ // • `'prologue-extension'` — the extension's POSITION decides it: a narrow-declared
192
+ // parameter widens at the very top of the function, a body cast widens at its use.
193
+ // • `'home-store-and-in-place'` — the position decides NOTHING, both spellings leading the
194
+ // function, and TWO other facts decide it together: the parameter is stored to an argument
195
+ // home nothing reads back AND widened in its own argument register. Each half alone has a
196
+ // compiled counterexample, so only the PAIR separates a declaration from a body cast.
197
+ //
198
+ // THE HOME STORE IS AN `-O2` OBSERVABLE, AND `-g` IS NOT WHAT REMOVES IT. Measured on
199
+ // `int f(s8 x){ return x; }`: present at `-O2` and at `-O2 -g3`, absent at `-O1`, at `-O0`
200
+ // and at `-g` (which implies `-O0`). So the 42 real af rows that build at
201
+ // `-G 0 -non_shared -Wab,-r4300_mul -mips2 -EB -O2 -g3` DO carry it — checked at exactly
202
+ // those flags — and a target built at `-O1`/`-O0` would have to claim `'none'`. The
203
+ // in-place widening survives every one of those levels, but on its own it decides nothing,
204
+ // so the pass refuses there rather than reading half a pair.
205
+ // • `'none'` — the object does not distinguish the two at all, so no reading of it licenses
206
+ // the narrowing. It is a MEASUREMENT rather than a withholding wherever a target claims it.
207
+ //
208
+ // A compiler that sets nothing here also refuses, which is the right default for one nobody has
209
+ // compiled the pair with (docs/level-tower.md: claim nothing about an unmeasured behavior).
210
+ narrowParamWitness?: NarrowParamWitness;
180
211
  // A subscript over a DECLARED ARRAY OBJECT expands its base ahead of the index, where every
181
212
  // pointer or cast base expands it last — so the instruction order in the target's own assembly
182
213
  // says which of the two the source wrote, and `raise/globalshape.ts` may derive an array shape
@@ -219,6 +250,25 @@ export interface TargetDescription {
219
250
  // says so, but nothing in this bag could express it if they did not. Any behavior that can
220
251
  // differ between two toolchains sharing one description is mis-keyed by construction.
221
252
  spillSlotOrder?: 'ascending' | 'descending' | 'unknown';
253
+ // Arguments past the argument registers are staged into an area this function RESERVES at the
254
+ // BOTTOM of its own frame — `[sp,#0]` upward, one word each — rather than pushed at the call
255
+ // site. It is GCC's ACCUMULATE_OUTGOING_ARGS target macro, and it is what makes an outgoing
256
+ // argument INDISTINGUISHABLE by code alone from a dead local: the words sit inside this
257
+ // frame's reservation and nothing this function does ever reloads one.
258
+ //
259
+ // Absent ⇒ no outgoing area is claimed and the Thumb frontend's stack-argument licence never
260
+ // fires, so a `[sp,#k]` store reaching a call unread declines exactly as it did before that
261
+ // licence existed. Read off the target by the frontend (`frontend/thumb.ts` `declaredCall`),
262
+ // not by the structurer.
263
+ //
264
+ // Set on agbcc, where the layout was read off `gcc/config/arm/thumb.h` and then measured —
265
+ // the corpus's `stkarg` (accepting) and `stkwide` (refusing) rows and kleod's
266
+ // `sub_0804C300` all stage their words at [sp,#0] upward inside the prologue's own
267
+ // reservation. NOT set anywhere else: a push-based caller would stage nothing inside the
268
+ // frame, and `docs/level-tower.md`'s rule for an unmeasured compiler behavior is to claim
269
+ // nothing. No other frontend calls the analysis today, so the field claims a premise rather
270
+ // than changing a verdict — which is the point: a second armv4t compiler must state it.
271
+ stagesOutgoingArgsInFrame?: boolean;
222
272
  // Regime-A switch recovery: accept a RELATIONAL test whose BRANCH admits exactly one scrutinee
223
273
  // value as that case (`cmp r0, #1 / bcc` is `case 0:` of an unsigned switch) rather than as
224
274
  // navigation.
@@ -262,8 +312,8 @@ export interface TargetDescription {
262
312
  // its own pairs (below) and deliberately does NOT declare `switchArmsFollowLayout`, because it
263
313
  // has a scheduler. Declaring this one is not evidence for that one.
264
314
  //
265
- // agbcc declares it, and its own pair of objects says the reading is not vacuous: at
266
- // TOOLCHAIN.agbccFlags the same two-case body is 20 bytes (0x14, ten Thumb instructions)
315
+ // agbcc declares it, and its own pair of objects says the reading is not vacuous: at agbcc's
316
+ // canonical flags the same two-case body is 20 bytes (0x14, ten Thumb instructions)
267
317
  // written either way and is a DIFFERENT object — the `switch` emits `cmp #0x1e; beq` then
268
318
  // `cmp #0x64; bne` before either body, sorted ascending and so in the reverse of the written
269
319
  // order; the ladder emits `cmp #0x64; bne` directly above its own body and reaches `cmp #0x1e`
@@ -313,15 +363,18 @@ export interface TargetDescription {
313
363
  // read past a branch and nothing lifts one to a dominator. The CONVERSE — the asm's read block
314
364
  // is where the source read — is FALSE even here, and no default may be declared as if it held.
315
365
  //
316
- // agbcc (gcc 2.9-arm, -O2) declares TRUE from its own sources plus a compiled pair: gcc's
317
- // Makefile SRCS compiles neither sched.c nor reorg.c and toplev.c never mentions
318
- // flag_schedule_insns, so there is no scheduler; gcse.c calls one_code_hoisting_pass only
319
- // `if (optimize_size)`, which toplev.c sets only for -Os, so at -O2 the hoister is compiled in
320
- // and never runs (a -Os project would NOT get this declaration); and `s = *g; if (c) A(s);
321
- // else B(s);` against `if (c) A(*g); else B(*g);` emits one ldrb + one pool word versus one of
322
- // each PER ARM, moving neither. The two passes that DO move a read between blocks at -O2 —
323
- // loop invariant motion, and the PRE that makes the converse false are refusals the rule
324
- // owes; structure/analysis.ts carries them.
366
+ // agbcc (gcc 2.9-arm) declares TRUE from its own sources plus compiled pairs: gcc's Makefile
367
+ // SRCS compiles neither sched.c nor reorg.c and toplev.c never mentions flag_schedule_insns, so
368
+ // there is no scheduler; and `s = *g; if (c) A(s); else B(s);` against `if (c) A(*g); else
369
+ // B(*g);` emits one ldrb + one pool word versus one of each PER ARM, moving neither. gcse.c calls
370
+ // one_code_hoisting_pass only `if (optimize_size)`, which toplev.c sets only for -Os, so at -O2
371
+ // the hoister never runs. At -Os it runs, and on `if (c) A(*gp); else B(*gp);` it moves only the
372
+ // pool ADDRESS load above the branch: each arm keeps its own dereference, so the read still
373
+ // stays in the block that spelled it. That pair is committed: `corpus/agbcc-hoist-{O2,Os}.s` from
374
+ // `corpus/probe-agbcc-hoist.c`, regenerated by `scripts/regen-flag-pair-probes.ts`, asserted in
375
+ // hoist-level-probes.test.ts. The two passes that DO move a read between blocks at -O2 — loop
376
+ // invariant motion, and the PRE that makes the converse false — are refusals the rule owes;
377
+ // structure/analysis.ts carries them.
325
378
  //
326
379
  // ABSENT ⇒ the rule stands down, where ido/kmc-gcc/mwcc sit: each has a scheduler and none has
327
380
  // been put through that pair. A compiler opts in on its own evidence, never by inheriting.
@@ -405,6 +458,7 @@ export const ARMV4T_AGBCC: TargetDescription = {
405
458
  hoistsSingleSetArm: true,
406
459
  arrayShapeFromStride: true,
407
460
  reloadsLocalReread: true,
461
+ narrowParamWitness: 'prologue-extension',
408
462
  // agbcc: reload walks pseudos ascending handing each global-alloc loser a fresh slot, a user
409
463
  // local's pseudo number is its `expand_decl` position, and the Thumb frame grows UPWARD
410
464
  // (FRAME_GROWS_DOWNWARD is commented out in thumb.h). So the earlier-declared spilled local
@@ -413,6 +467,9 @@ export const ARMV4T_AGBCC: TargetDescription = {
413
467
  // control `synthetic:spillorder_rev` (the same body in the order asmlift already emits, which
414
468
  // must stay a MATCH), plus `synthetic:dma_fill_uninit`, a row this did not author.
415
469
  spillSlotOrder: 'ascending',
470
+ // agbcc reserves the outgoing area with the rest of the frame (`add sp, sp, #-N` covers both)
471
+ // and stages arguments 5+ into it at [sp,#0] upward — thumb.h's ACCUMULATE_OUTGOING_ARGS.
472
+ stagesOutgoingArgsInFrame: true,
416
473
  },
417
474
  };
418
475
 
@@ -436,6 +493,11 @@ export const MIPS_IDO: TargetDescription = {
436
493
  switchAllowsNeqCase: false,
437
494
  // MEASURED — the pair at the field compiles to one load of `p[1]` for every local spelling.
438
495
  reloadsLocalReread: false,
496
+ // MEASURED at `-mips2 -O2 -32 -non_shared -G 0`: the `sll` leads the function for BOTH
497
+ // spellings, so the prologue position cannot decide; a narrow DECLARED parameter is the one that
498
+ // is both homed dead AND widened in its own argument register. raise/paramwidth.ts's header has
499
+ // the disassemblies, including the counterexample for each half alone.
500
+ narrowParamWitness: 'home-store-and-in-place',
439
501
  // MEASURED `descending` (the earlier-declared spilled local takes the HIGHER offset) and NOT
440
502
  // SHIPPED. The probe is COMMITTED — `packages/core/test/corpus/probe-declrank.c` and its
441
503
  // reversed-declaration twin, with this compiler's objects beside them — and a test reads the
@@ -502,9 +564,12 @@ export const MIPS_GCC: TargetDescription = {
502
564
  // break; case 7: *p = 2; break; } return 0; }` cross-jumps the two stores into one and compiles
503
565
  // instruction for instruction identically at -O1 and at -O2, to `beq` / `beql` with the shared
504
566
  // `sw` after both; the same body as an if/else-if ladder puts the first arm's `li v0,1` BETWEEN
505
- // the two tests. Same split not committed as a fixture only because `beql` is an unmodelled
506
- // control transfer, so the row declines before PRE5 and the cross-jumped arms leave no store to
507
- // read the split off. Re-measure it the day branch-likely lands. (A THREE-case body says the
567
+ // the two tests. Both spellings get all the way through on both toolchains — the `switch` one
568
+ // recovers a `switch`, the ladder an `if` nest so what keeps that body out of the fixture set
569
+ // is not a decline. It is that the cross-jumped arms leave no STORE to read the split off: both
570
+ // put their one `sw` after both tests, and the interleaved instruction is the ladder's
571
+ // `li v0,1`. The committed pair keeps its distinct-store arms for that reason. (A THREE-case
572
+ // body says the
508
573
  // same more loudly, the balanced tree's `slti` bound test landing ahead of the bodies with the
509
574
  // rest, but at three cases neither spelling reaches Regime A on this compiler, so that pair
510
575
  // could not also serve as the recovery test.)
@@ -520,6 +585,10 @@ export const MIPS_GCC: TargetDescription = {
520
585
  // MEASURED on BOTH toolchains this description serves (the note above): one load of `p[1]` for
521
586
  // every local spelling of the pair at the field, gcc2.7.2kmc at -O2 and gcc2.7.2 at -O1 alike.
522
587
  reloadsLocalReread: false,
588
+ // MEASURED on BOTH toolchains this description serves: `int f(s8 x){return x;}` and
589
+ // `int f(s32 x){return (s8)x;}` compile to BYTE-IDENTICAL objects, gcc2.7.2kmc at -O2 and
590
+ // gcc2.7.2 at -O1 alike — so the object carries no witness at all and the pass refuses.
591
+ narrowParamWitness: 'none',
523
592
  // MEASURED `ascending` on both toolchains this description serves — 7 of 7 spills each, and
524
593
  // rank → offset unchanged under a reversed declaration list — and NOT SHIPPED, for the same
525
594
  // reason as ido7.1: no row on either tier lifts with two or more spilled user locals. Both
@@ -558,6 +627,10 @@ export const PPC_MWCC: TargetDescription = {
558
627
  // `u8 v = p[3]; if ((v & 0x7f) == 0x7f) { fnA(); p[4] = v; return; }` under an `if (a)`
559
628
  // matches only once `read-behind-effect` stops refusing it (3/24 → MATCH 0/22).
560
629
  reloadsLocalReread: false,
630
+ // The PowerPC prologue widens a declared narrow parameter with `extsb`/`extsh`, which the
631
+ // frontend lifts to the same `sext` op agbcc's shift pair folds to — the position shape, on
632
+ // another ISA. `synthetic:{sextb,tos8}:mwcc_242_81` are its rows, MATCH through that pass.
633
+ narrowParamWitness: 'prologue-extension',
561
634
  // NOT MEASURED, and `'unknown'` is therefore the only honest value here rather than a withheld
562
635
  // one, as it is at MIPS_IDO and MIPS_GCC. No mwcc row lifts with two or more spilled user
563
636
  // locals, and the compiler does not spill the committed declaration-rank probe either: at
@@ -569,6 +642,143 @@ export const PPC_MWCC: TargetDescription = {
569
642
  },
570
643
  };
571
644
 
645
+ /** A toolchain: one compiler binary, named as decomp.me names it. Several toolchains may share a
646
+ * description (`MIPS_GCC` serves two), and each keeps its own evidence. */
647
+ export interface ToolchainTarget {
648
+ family: FlagFamily;
649
+ /** The codegen flags every committed probe of this toolchain was compiled with: the flags a
650
+ * synthetic row compiles at, and the flags a decompile with none given assumes. They are in
651
+ * normal form (`storedFlags` keeps every word), so the words only the harness needs (`-c`, the
652
+ * diagnostics) live beside the binary's paths in @asmlift/toolchains.
653
+ *
654
+ * A toolchain with no SYNTHETIC tier has NONE, and inventing one would be the fiction the field
655
+ * exists to avoid: nothing about the compiler picks a set, every row names the flags its own
656
+ * build compiles that unit with, and the probes behind its description are run at those. A
657
+ * decompile that reaches such a toolchain with no flags from anywhere is refused rather than
658
+ * resolved against a set nobody chose (`resolveFlags`). */
659
+ canonicalFlags?: readonly string[];
660
+ /** What this compiler does. Every flag set of the toolchain decompiles against it: a profile of a
661
+ * compiler inherits its declarations until a probe refutes one there. */
662
+ description: TargetDescription;
663
+ }
664
+
665
+ export const TOOLCHAIN_TARGETS = {
666
+ agbcc: {
667
+ family: 'agbcc',
668
+ canonicalFlags: ['-mthumb-interwork', '-O2', '-fhex-asm', '-fprologue-bugfix'],
669
+ description: ARMV4T_AGBCC,
670
+ },
671
+ 'ido7.1': {
672
+ family: 'ido',
673
+ canonicalFlags: ['-mips2', '-O2', '-32', '-non_shared', '-G', '0'],
674
+ description: MIPS_IDO,
675
+ },
676
+ 'gcc2.7.2kmc': {
677
+ family: 'gcc',
678
+ canonicalFlags: [
679
+ '-mabi=32',
680
+ '-mgp32',
681
+ '-mfp32',
682
+ '-mno-abicalls',
683
+ '-fno-PIC',
684
+ '-G',
685
+ '0',
686
+ '-funsigned-char',
687
+ '-mips3',
688
+ '-EB',
689
+ '-O2',
690
+ '-fno-builtin',
691
+ '-fno-asm',
692
+ ],
693
+ description: MIPS_GCC,
694
+ },
695
+ 'gcc2.7.2': {
696
+ family: 'gcc',
697
+ canonicalFlags: ['-G0', '-mips3', '-mgp32', '-mfp32', '-O1', '-Wa,--vr4300mul-off'],
698
+ description: MIPS_GCC,
699
+ },
700
+ mwcc_242_81: {
701
+ family: 'mwcc',
702
+ canonicalFlags: [
703
+ '-proc',
704
+ 'gekko',
705
+ '-O4,p',
706
+ '-enum',
707
+ 'int',
708
+ '-inline',
709
+ 'auto',
710
+ '-fp',
711
+ 'hard',
712
+ '-Cpp_exceptions',
713
+ 'off',
714
+ ],
715
+ description: PPC_MWCC,
716
+ },
717
+ // The other two CodeWarrior builds the GameCube projects compile with: 2.3.3b163n (Pikmin's whole
718
+ // game tree) and 2.4.7b107 (Mario Party 4's DOL). Both SHARE `PPC_MWCC`, and on their own evidence
719
+ // rather than because they are the same compiler family: `test/matching/ppc-compiler-behaviors.test.ts`
720
+ // re-runs both of its probes on each binary at the flags that build's rows compile at, and each
721
+ // reads as the description says. What moves those readings is the optimisation level — at `-O0,p`
722
+ // the shipped `mwcc_242_81` re-reads the local too — so the difference is one no per-compiler
723
+ // field could carry anyway.
724
+ //
725
+ // NEITHER HAS CANONICAL FLAGS. They have no synthetic tier: every row of theirs is a real one that
726
+ // names its unit's own flags, and Pikmin's `-O4,p -lang=c++` and Mario Party 4's `-O0,p -lang=c`
727
+ // are two different sets, neither of which is "the" one. See `canonicalFlags` above.
728
+ mwcc_233_163n: {
729
+ family: 'mwcc',
730
+ description: PPC_MWCC,
731
+ },
732
+ mwcc_247_107: {
733
+ family: 'mwcc',
734
+ description: PPC_MWCC,
735
+ },
736
+ } as const satisfies Readonly<Record<string, ToolchainTarget>>;
737
+
738
+ export type ToolchainId = keyof typeof TOOLCHAIN_TARGETS;
739
+
740
+ /** A toolchain that HAS canonical flags — every toolchain with a synthetic tier. Computed from the
741
+ * registry, so the set cannot drift from it: a caller that needs a fallback flag set (the
742
+ * playground's target picker, the benchmark's synthetic rows) takes this instead of `ToolchainId`
743
+ * and a real-only toolchain is refused where it is named, not where it is used. */
744
+ export type CanonicalToolchainId = {
745
+ [K in ToolchainId]: (typeof TOOLCHAIN_TARGETS)[K] extends { canonicalFlags: readonly string[] } ? K : never;
746
+ }[ToolchainId];
747
+
748
+ /** A toolchain's canonical flags, or `undefined` where it has none. The ONE place the optional
749
+ * field is read: the registry's literal type says which entries carry it, and every caller that
750
+ * holds a plain `ToolchainId` has to answer for the ones that do not. */
751
+ export function canonicalFlagsOf(id: ToolchainId): readonly string[] | undefined {
752
+ const t: ToolchainTarget = TOOLCHAIN_TARGETS[id];
753
+ return t.canonicalFlags;
754
+ }
755
+
756
+ export function isCanonicalToolchainId(id: ToolchainId): id is CanonicalToolchainId {
757
+ return canonicalFlagsOf(id) !== undefined;
758
+ }
759
+
760
+ export function isToolchainId(id: string): id is ToolchainId {
761
+ return Object.hasOwn(TOOLCHAIN_TARGETS, id);
762
+ }
763
+
764
+ /** A toolchain at one flag set. */
765
+ export interface ResolvedTarget {
766
+ toolchain: ToolchainId;
767
+ /** the flags the function's target and every candidate compile with */
768
+ cflags: readonly string[];
769
+ /** the description asmlift decompiles against: the toolchain's, at every flag set */
770
+ target: TargetDescription;
771
+ /** what the flags make the compiler do */
772
+ profile: CodegenProfile;
773
+ }
774
+
775
+ /** The target a function compiled by `toolchain` at `cflags` decompiles against, and the profile
776
+ * those flags describe. Throws on a level word the toolchain's family cannot read. */
777
+ export function targetFor(toolchain: ToolchainId, cflags: readonly string[]): ResolvedTarget {
778
+ const t: ToolchainTarget = TOOLCHAIN_TARGETS[toolchain];
779
+ return { toolchain, cflags, target: t.description, profile: parseFlags(t.family, cflags) };
780
+ }
781
+
572
782
  /** Build the structurer's options for a target: the function's own `returnsVoid` plus every
573
783
  * `compilerBehaviors` field. The ONE place a target's compiler behaviors flow into the
574
784
  * target-agnostic structurer — a new compiler behavior is a field in `compilerBehaviors`, consumed
package/src/trace.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  // (objdiff score, per-pattern score deltas via the `probeScore` hook, ranked candidates) when a
6
6
  // target object is available; the web playground renders the TraceReport as-is.
7
7
  import { cBackend } from './backend/c';
8
+ import type { CodegenProfile } from './codegen-flags';
8
9
  import type { AsmData } from './frontend/asmdata';
9
10
  import { frontendFor } from './frontend/registry';
10
11
  import type { Fn } from './ir/core';
@@ -16,7 +17,7 @@ import { type OnGap, raiseRecovered, structureChecked, stubResult } from './pipe
16
17
  import { type Prototypes, prototypesFromSymbols } from './proto';
17
18
  import { assumedShapes, inferGlobalArrays, orderLicensedGlobals } from './raise/globalshape';
18
19
  import { type SymbolInfo, type SymbolMap, symbolsByName } from './symbols';
19
- import { type TargetDescription, structureOptionsFor } from './target';
20
+ import { type ResolvedTarget, type TargetDescription, type ToolchainId, structureOptionsFor } from './target';
20
21
 
21
22
  /** EVERY stage dump carries the write-order record (ir/print.ts `PrintOptions`) — not just
22
23
  * `stage:lift`, even though only the frontend measures it. Two reasons: the raising folds MUTATE
@@ -50,6 +51,11 @@ export interface TraceReport {
50
51
  target: {
51
52
  isa: string;
52
53
  compiler: string;
54
+ toolchain: ToolchainId;
55
+ /** the flags the function was compiled with, in the build's order */
56
+ cflags: readonly string[];
57
+ /** what those flags make the compiler do */
58
+ profile: Pick<CodegenProfile, 'slots'>;
53
59
  capabilities: TargetDescription['capabilities'];
54
60
  compilerBehaviors: TargetDescription['compilerBehaviors'];
55
61
  };
@@ -118,19 +124,31 @@ const PRE_RECOVERY_TRACE: Record<string, { stage: string; title: (result: number
118
124
  'struct-arrays': { stage: 'stage:struct-arrays', title: () => 'Struct-array recovery (element stride evidence)' },
119
125
  };
120
126
 
127
+ function reportTarget({ toolchain, cflags, target, profile }: ResolvedTarget): TraceReport['target'] {
128
+ return {
129
+ isa: target.id,
130
+ compiler: target.compiler,
131
+ toolchain,
132
+ cflags,
133
+ profile: { slots: profile.slots },
134
+ capabilities: target.capabilities,
135
+ compilerBehaviors: target.compilerBehaviors,
136
+ };
137
+ }
138
+
121
139
  /** Run the tower while recording a TraceReport. Strict mode throws on any gap (like decompile);
122
140
  * annotate mode never throws — a non-localizable failure degrades to the same stub. */
123
141
  export function decompileTraced(
124
142
  name: string,
125
143
  asm: string,
126
- target: TargetDescription,
144
+ resolved: ResolvedTarget,
127
145
  opts: TraceOptions = {},
128
146
  ): { source: string; report: TraceReport } {
129
147
  if ((opts.onGap ?? 'strict') === 'strict') {
130
- return traceTower(name, asm, target, opts);
148
+ return traceTower(name, asm, resolved, opts);
131
149
  }
132
150
  try {
133
- return traceTower(name, asm, target, opts);
151
+ return traceTower(name, asm, resolved, opts);
134
152
  } catch (e) {
135
153
  // Annotate-mode parity with decompile(): a NON-localizable failure degrades to the SAME
136
154
  // stub (reason + original asm as comments) instead of a throw.
@@ -141,12 +159,7 @@ export function decompileTraced(
141
159
  version: 1,
142
160
  type: 'decompile',
143
161
  symbol: name,
144
- target: {
145
- isa: target.id,
146
- compiler: target.compiler,
147
- capabilities: target.capabilities,
148
- compilerBehaviors: target.compilerBehaviors,
149
- },
162
+ target: reportTarget(resolved),
150
163
  asm,
151
164
  trace: [],
152
165
  patternEvents: [],
@@ -161,9 +174,10 @@ export function decompileTraced(
161
174
  function traceTower(
162
175
  name: string,
163
176
  asm: string,
164
- target: TargetDescription,
177
+ resolved: ResolvedTarget,
165
178
  opts: TraceOptions,
166
179
  ): { source: string; report: TraceReport } {
180
+ const { target } = resolved;
167
181
  const backend = opts.backend ?? cBackend;
168
182
  // Merged exactly as pipeline.ts does: the project's own DWARF signatures fill in what the caller
169
183
  // did not state. A trace that lifted from a different table would explain a run that never
@@ -207,7 +221,7 @@ function traceTower(
207
221
  let scoreBefore = opts.probeScore?.(fn, inferredSymbols);
208
222
  for (const p of active) {
209
223
  const beforeIr = irDump(fn);
210
- const hits = applyPattern(fn, p);
224
+ const hits = applyPattern(fn, p, target);
211
225
  dce(fn);
212
226
  verify(fn);
213
227
  if (hits === 0) {
@@ -307,12 +321,7 @@ function traceTower(
307
321
  version: 1,
308
322
  type: 'decompile',
309
323
  symbol: name,
310
- target: {
311
- isa: target.id,
312
- compiler: target.compiler,
313
- capabilities: target.capabilities,
314
- compilerBehaviors: target.compilerBehaviors,
315
- },
324
+ target: reportTarget(resolved),
316
325
  asm,
317
326
  trace,
318
327
  patternEvents,
@@ -24,6 +24,8 @@
24
24
  //
25
25
  // Pure data: this module stays browser-safe. `offeredWhen` names admission tables by key; their rules
26
26
  // are `variation-gates.ts`, which a reader of a title or a summary never loads.
27
+ import type { FlagFamily } from './codegen-flags';
28
+ import type { CanonicalToolchainId } from './target';
27
29
  import type { GateTableName } from './variation-gates';
28
30
  import type { GatingBehavior, VariationKind, VariationName } from './variation-tokens';
29
31
 
@@ -105,8 +107,18 @@ export const VARIATION_KIND_DEFINITIONS: { readonly [K in VariationKind]: Variat
105
107
  },
106
108
  };
107
109
 
108
- /** A compiler an example is built with, as the benchmark's rows name it. */
109
- export type ExampleCompiler = 'agbcc' | 'ido' | 'gcc' | 'mwcc';
110
+ /** A compiler an example is built with: a compiler family, as the benchmark's rows name it. */
111
+ export type ExampleCompiler = FlagFamily;
112
+
113
+ /** The toolchain each example compiler builds with. An example and a witness are claims at that
114
+ * toolchain's canonical flags (`TOOLCHAIN_TARGETS`), which is what the matching suite compiles them
115
+ * at — so the toolchain named here must be one that HAS them, which the type says. */
116
+ export const EXAMPLE_COMPILER_TOOLCHAINS: { readonly [C in ExampleCompiler]: CanonicalToolchainId } = {
117
+ agbcc: 'agbcc',
118
+ ido: 'ido7.1',
119
+ gcc: 'gcc2.7.2kmc',
120
+ mwcc: 'mwcc_242_81',
121
+ };
110
122
 
111
123
  /** Each example compiler as a reader knows it. */
112
124
  export const EXAMPLE_COMPILER_NAMES: { readonly [C in ExampleCompiler]: string } = {
@@ -1098,6 +1110,44 @@ export const VARIATION_DEFINITIONS: { readonly [N in VariationName]: VariationDe
1098
1110
  implementedIn: l3('scopebase'),
1099
1111
  seeAlso: ['scopebase', 'homesplit', 'vol-store'],
1100
1112
  },
1113
+ argcopy: {
1114
+ title: 'A pointer parameter copied for one region',
1115
+ summary: 'a region copies an incoming pointer parameter into a local and uses the copy',
1116
+ detail:
1117
+ 'A pointer parameter the whole function reads pins its incoming register for the whole body. A source ' +
1118
+ 'that copies it into a local at the head of the block that uses it gives the allocator a second name ' +
1119
+ 'for the same address, which it may home elsewhere — freeing the parameter register for something ' +
1120
+ "else, such as that block's loop counter. Uses outside the chosen region keep naming the parameter, " +
1121
+ 'which is what makes the two ranges separable.',
1122
+ compilerBehavior:
1123
+ 'A braced declaration inside the region and a plain local assigned at its head were both taken through ' +
1124
+ 'agbcc on the row this variation was built for and produced the same bytes, so the copy is emitted as ' +
1125
+ 'a plain local and nothing here carries block scope. The variation has no target gate, so it is offered ' +
1126
+ 'on every compiler — as /scopebase, /regionbase and /coalesce, which rest on the same allocator fact, ' +
1127
+ 'also are. A target gate would have to claim the copy is INERT on some compiler, the way /advance claims ' +
1128
+ 'it of a target that folds a pointer advance; nobody has compiled the pair that would say so, and a gate ' +
1129
+ 'withholding a candidate on an untested guess costs matches rather than fan.',
1130
+ offeredWhen: {
1131
+ judges: 'each pointer parameter, in each nested statement list that reads it',
1132
+ gates: ['ARGCOPY_GATES', 'ARGCOPY_REGION_GATES'],
1133
+ },
1134
+ subject: {
1135
+ meaning:
1136
+ 'The parameter, then the region that copies it, as a path of statement-index/list-index ' +
1137
+ 'PAIRS — one pair per nesting level. `argcopy-a0@0.1` copies `a0` at the head of the second ' +
1138
+ 'nested list of the first statement; `argcopy-a0@0.1.2.0` copies it in the first nested list ' +
1139
+ 'of the third statement of that one.',
1140
+ examples: ['argcopy-a0@0.0', 'argcopy-a0@0.1', 'argcopy-a0@0.1.2.0'],
1141
+ },
1142
+ example: {
1143
+ compiler: 'agbcc',
1144
+ unit: CALLS + 'void example(u8 *a0, u8 a1, s32 c) { u8 *p0; s32 i; @ }',
1145
+ before: 'if (c) { i = 0; do { if (g(a0 + i) != 0) a0[i + 50] = a1; i = i + 1; } while (i <= 3); }',
1146
+ after: 'if (c) { p0 = a0; i = 0; do { if (g(p0 + i) != 0) p0[i + 50] = a1; i = i + 1; } while (i <= 3); }',
1147
+ },
1148
+ implementedIn: l3('argcopy'),
1149
+ seeAlso: ['scopebase', 'regionbase', 'parkfirst', 'coalesce'],
1150
+ },
1101
1151
  coalesce: {
1102
1152
  title: 'Two locals share one variable',
1103
1153
  summary: 'two locals whose lifetimes never overlap are merged into one',
@@ -9,6 +9,7 @@
9
9
  // Kept apart from `variation-definitions.ts`, which stays pure data: that module takes only the
10
10
  // TYPE of a key, so a consumer reading a title or a summary does not load the passes.
11
11
  import { ADVANCE_HEAD_GATES, ADVANCE_MEMBER_GATES } from './l3/advance';
12
+ import { ARGCOPY_GATES, ARGCOPY_REGION_GATES } from './l3/argcopy';
12
13
  import { BASEFOLD_GATES, LIVEBASE_BLOCK_GATES, LIVEBASE_GATES, ORDERBASE_GATES, UNFOLDED_GATES } from './l3/basecse';
13
14
  import { ARM_DISJOINT_GATES, COALESCE_GATES } from './l3/coalesce';
14
15
  import type { Gate } from './l3/gates';
@@ -42,6 +43,8 @@ export interface ReaderRule {
42
43
  export const VARIATION_GATE_TABLES = {
43
44
  ADVANCE_HEAD_GATES,
44
45
  ADVANCE_MEMBER_GATES,
46
+ ARGCOPY_GATES,
47
+ ARGCOPY_REGION_GATES,
45
48
  ARM_DISJOINT_GATES,
46
49
  ARM_REREAD_GATES,
47
50
  BASEFOLD_GATES,
@@ -123,6 +123,7 @@ const TOKENS = [
123
123
  target: { behavior: 'foldsPointerAdvance', declared: false, unlessWith: 'volatile' },
124
124
  },
125
125
  { name: 'parkfirst', variationKind: 'respell' },
126
+ { name: 'argcopy', variationKind: 'respell', subject: /[A-Za-z_]\w*@[\d.]+/ },
126
127
  { name: 'sinkinit', variationKind: 'respell' },
127
128
  { name: 'regcopy', variationKind: 'respell', subject: /ret|ret-fresh/ },
128
129
  { name: 'initfirst', variationKind: 'respell' },