@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.
- package/README.md +48 -24
- package/package.json +1 -1
- package/src/backend/pascal.ts +2 -2
- package/src/codegen-flags.ts +640 -0
- package/src/frontend/disasm.ts +141 -11
- package/src/frontend/high-half.ts +149 -0
- package/src/frontend/mips.ts +458 -209
- package/src/frontend/ppc.ts +332 -67
- package/src/frontend/reloc-symbol.ts +109 -0
- package/src/frontend/splat.ts +56 -18
- package/src/frontend/ssa.ts +126 -29
- package/src/frontend/stackargs.ts +420 -0
- package/src/frontend/thumb.ts +207 -230
- package/src/ir/core.ts +62 -3
- package/src/ir/opcodes.ts +9 -0
- package/src/ir/parse.ts +7 -1
- package/src/l3/advance.ts +2 -2
- package/src/l3/argbase.ts +2 -2
- package/src/l3/argcopy.ts +269 -0
- package/src/l3/ast.ts +45 -1
- package/src/l3/basecse.ts +2 -2
- package/src/l3/coalesce.ts +109 -52
- package/src/l3/scopebase.ts +4 -4
- package/src/l3/tailret.ts +70 -0
- package/src/l3/unmerge.ts +2 -2
- package/src/l3/unreduce.ts +2 -1
- package/src/mangle.ts +49 -0
- package/src/pattern/engine.ts +128 -13
- package/src/pipeline.ts +22 -11
- package/src/raise/extscale.ts +5 -2
- package/src/raise/paramwidth.ts +111 -3
- package/src/raise/pre-recovery.ts +11 -1
- package/src/raise/retsink.ts +8 -4
- package/src/raise/tailsink.ts +17 -2
- package/src/rank-declare.ts +17 -9
- package/src/rank.ts +45 -19
- package/src/structure/retspell.ts +95 -0
- package/src/structure/structure.ts +12 -3
- package/src/structure/switch-recover.ts +1 -1
- package/src/target.ts +224 -14
- package/src/trace.ts +27 -18
- package/src/variation-definitions.ts +52 -2
- package/src/variation-gates.ts +3 -0
- 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
|
-
//
|
|
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
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
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.
|
|
506
|
-
//
|
|
507
|
-
//
|
|
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
|
-
|
|
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,
|
|
148
|
+
return traceTower(name, asm, resolved, opts);
|
|
131
149
|
}
|
|
132
150
|
try {
|
|
133
|
-
return traceTower(name, asm,
|
|
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
|
-
|
|
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 =
|
|
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',
|
package/src/variation-gates.ts
CHANGED
|
@@ -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,
|
package/src/variation-tokens.ts
CHANGED
|
@@ -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' },
|