@asmlift/core 0.6.0 → 0.7.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 +2 -2
- package/package.json +1 -1
- package/src/backend/cfamily.ts +39 -11
- package/src/contracts.ts +60 -11
- package/src/frontend/ssa.ts +1 -1
- package/src/frontend/thumb.ts +2 -2
- package/src/ir/alias.ts +24 -0
- package/src/ir/core.ts +8 -0
- package/src/ir/opcodes.ts +43 -7
- package/src/ir/simplify.ts +1 -1
- package/src/l3/address.ts +2 -2
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +4 -4
- package/src/l3/ast.ts +65 -21
- package/src/l3/basecse.ts +48 -28
- package/src/l3/coalesce.ts +9 -9
- package/src/l3/gates.ts +75 -1
- package/src/l3/hoist.ts +1 -1
- package/src/l3/homesplit.ts +13 -13
- package/src/l3/initfirst.ts +3 -3
- package/src/l3/inlinebase.ts +16 -16
- package/src/l3/mentions.ts +68 -5
- package/src/l3/mulfirst.ts +3 -3
- package/src/l3/nearbase.ts +4 -4
- package/src/l3/offmember.ts +5 -5
- package/src/l3/parkfirst.ts +6 -6
- package/src/l3/pollguard.ts +3 -3
- package/src/l3/ptrfield.ts +4 -4
- package/src/l3/regspell.ts +8 -8
- package/src/l3/reindex.ts +22 -17
- package/src/l3/scopebase.ts +28 -25
- package/src/l3/sinkinit.ts +7 -7
- package/src/l3/slotorder.ts +3 -3
- package/src/l3/storage.ts +1 -1
- package/src/l3/tailmerge.ts +2 -2
- package/src/l3/typing.ts +3 -3
- package/src/l3/unmerge.ts +483 -59
- package/src/l3/unreduce.ts +13 -13
- package/src/l3/volatileptr.ts +11 -11
- package/src/l3/volatileval.ts +11 -11
- package/src/l3/volstore.ts +16 -16
- package/src/l3/zerosub.ts +6 -6
- package/src/pattern/engine.ts +4 -4
- package/src/pipeline.ts +17 -5
- package/src/proto.ts +2 -2
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +32 -12
- package/src/raise/gvn.ts +2 -2
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +4 -4
- package/src/raise/narrowlocal.ts +18 -2
- package/src/raise/paramwidth.ts +24 -2
- package/src/raise/pre-recovery.ts +90 -25
- package/src/raise/retsink.ts +381 -15
- package/src/raise/shortcircuit.ts +595 -34
- package/src/raise/structs.ts +4 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +4 -4
- package/src/{rank-axes.ts → rank-variations.ts} +319 -189
- package/src/rank.ts +1148 -803
- package/src/structure/analysis.ts +87 -90
- package/src/structure/bitfields.ts +130 -30
- package/src/structure/globalaccess.ts +30 -4
- package/src/structure/namecoalesce.ts +32 -13
- package/src/structure/structure.ts +1415 -200
- package/src/structure/switch-recover.ts +100 -7
- package/src/symbols.ts +127 -6
- package/src/target.ts +155 -35
- package/src/trace.ts +1 -1
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
package/src/l3/basecse.ts
CHANGED
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
// question the source answered per BASE — one register file spelled as a pointer local beside
|
|
14
14
|
// scalar cells spelled as bare derefs. The `single-cell` gate is what makes the narrower answer
|
|
15
15
|
// reachable: under `LIVEBASE_BLOCK_GATES` a base every access of which is ONE fixed offset stays
|
|
16
|
-
// inline, and rank's
|
|
16
|
+
// inline, and rank's LIVEBASE_HOISTS roster emits each table's hoist — and every composition onto
|
|
17
17
|
// it — as its own candidate family, for the differ to referee between them. The unit is
|
|
18
18
|
// the (base, width, signedness) KEY, not the base — a base read at two widths is two keys, and the
|
|
19
19
|
// gate can leave one of them inline while the other binds.
|
|
20
20
|
//
|
|
21
21
|
// COVERAGE: the roster (rank.ts) is SEVEN rows over FIVE gate tables — two PAIRS share a table and
|
|
22
22
|
// differ only in placement, `/basefold` with `/basefold/sinkinit` and `/orderbase` with
|
|
23
|
-
// `/orderbase
|
|
23
|
+
// `/orderbase-scoped` — and it is a set of hand-picked SUBSETS rather than a narrowness ranking;
|
|
24
24
|
// only `/livebase` ⊇ `/livebase-block` are ordered by inclusion. A table
|
|
25
25
|
// whose predicate cuts across the others therefore carves out a PARTIAL answer, which is what
|
|
26
26
|
// `UNFOLDED_GATES` does. Measured at ONE stated scope, `decompile()`'s default structuring,
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
// and `ldrb r0, [r4, #0x3]` — agbcc CSEs the symbol reference where it re-materializes the integer
|
|
62
62
|
// — so the inline subscript spelling produces exactly the shape this rule reads as evidence
|
|
63
63
|
// against it. asmlift lifts that asm back to the correct `((u8 *)&gS)[3]` and then offers the
|
|
64
|
-
// named
|
|
64
|
+
// spelling through a named base anyway. Measured reach: of the 21 keys the symbol half newly admits over
|
|
65
65
|
// the artifact's agbcc rows in both symbol-map configurations, 4 are on a base whose address the
|
|
66
66
|
// tree also uses as a value (2 distinct keys, on `kleod:ProcessInputAndUpdateEntities` and
|
|
67
67
|
// `pokeemerald:TrySetCantSelectMoveBattleScript`).
|
|
@@ -74,14 +74,14 @@
|
|
|
74
74
|
// `synthetic:foldhead` its match — which is why `index.operandOff` is carried from the lift
|
|
75
75
|
// instead of re-derived, and why a committed pass that can drop it is worth a test
|
|
76
76
|
// (test/basecse.test.ts, the "`operandOff` is provenance" describe). WHAT EACH ROW IS WORTH is
|
|
77
|
-
// measured in rank.ts's note on `
|
|
77
|
+
// measured in rank.ts's note on `BASEFOLD_HOISTS`, not here, and the two rows are not worth
|
|
78
78
|
// the same thing. Promoting the hint to a default would need this paragraph to say something it
|
|
79
79
|
// does not.
|
|
80
80
|
//
|
|
81
|
-
// It is EVIDENCE and not proof, which is why `BASEFOLD_GATES` below
|
|
81
|
+
// It is EVIDENCE and not proof, which is why `BASEFOLD_GATES` below backs a variation rather than a
|
|
82
82
|
// relaxation of the default table. agbcc folds a subscript but keeps an aggregate MEMBER offset in
|
|
83
83
|
// the memory operand: `((struct S *)0x3001100)->b` emits `.word 0x3001100` + `ldr [r0, #0x4]`,
|
|
84
|
-
// byte-identical to the named
|
|
84
|
+
// byte-identical to the spelling through a named base, and the same holds for a union member and for a
|
|
85
85
|
// store. So the shape has two sources and asmlift can spell only one of them; rank.ts offers both
|
|
86
86
|
// and the differ referees.
|
|
87
87
|
//
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
// AGGREGATE base (F9 spells a SCALAR global as a bare `var`, which is never an `index`-of-leaf, so
|
|
98
98
|
// scalar recovery is untouched). Non-leaf bases (a local, a struct-element `p[a0]`,
|
|
99
99
|
// arithmetic) are excluded: agbcc may re-derive those, so hoisting them can
|
|
100
|
-
// MISMATCH (empirically confirmed) — the differ-refereed `/addr-home`
|
|
100
|
+
// MISMATCH (empirically confirmed) — the differ-refereed `/addr-home` variation
|
|
101
101
|
// (structure/analysis.ts homeSharedAddresses) serves the shared gaddr-free ARITHMETIC bases
|
|
102
102
|
// instead.
|
|
103
103
|
// The hoisted local carries the access's pointer type, so the
|
|
@@ -149,8 +149,8 @@ const keyOf = (base: HoistableBase, width: number, signed: boolean): string => `
|
|
|
149
149
|
/** The key's own grammar, read back — `<leafId>[ <type>] <width> <signed>`.
|
|
150
150
|
*
|
|
151
151
|
* IT LIVES BESIDE `keyOf` BECAUSE THAT IS THE ONLY THING THAT MAKES IT SAFE. The key is a string
|
|
152
|
-
* and its readers are elsewhere — `l3/homesplit.ts` builds a candidate
|
|
153
|
-
*
|
|
152
|
+
* and its readers are elsewhere — `l3/homesplit.ts` builds a candidate's VARIATION out of it, and a
|
|
153
|
+
* candidate's variations are its identity — so a second file knowing this grammar is a collision waiting
|
|
154
154
|
* for the next base kind (`homeSplitTag` states the one the cast form causes).
|
|
155
155
|
*
|
|
156
156
|
* The one space inside a cast's base id is this grammar's own separator, not the type's: every
|
|
@@ -357,7 +357,7 @@ export interface BaseKey {
|
|
|
357
357
|
* what a pointer local's own initializer STATEMENT produces (see raise/globalshape.ts's header
|
|
358
358
|
* for the compile that separates the two), while the inline cast produces the other order — so
|
|
359
359
|
* it is evidence a home is what the source wrote. Read only by `ORDERBASE_GATES` (rank.ts);
|
|
360
|
-
* false for every base a compiler that has not opted in produced, which is what keeps the
|
|
360
|
+
* false for every base a compiler that has not opted in produced, which is what keeps the variation
|
|
361
361
|
* off those targets. */
|
|
362
362
|
orderLicensed: boolean;
|
|
363
363
|
}
|
|
@@ -380,13 +380,13 @@ export const BASECSE_GATES: readonly Gate<BaseKey>[] = [
|
|
|
380
380
|
// Censused at `decompile()`'s default structuring, map-less, one tree per row over the
|
|
381
381
|
// artifact's 404 agbcc rows: `ORDERBASE_GATES` admits 12 keys on 10 rows, 11 of them cast keys.
|
|
382
382
|
id: 'cast-base',
|
|
383
|
-
why: 'a struct element’s reinterpret cast is the inline spelling unless the assembly says the base
|
|
383
|
+
why: 'a struct element’s reinterpret cast is the inline spelling unless the assembly says the base was held in a local',
|
|
384
384
|
sound: false,
|
|
385
385
|
rejects: (c) => c.castBase,
|
|
386
386
|
},
|
|
387
387
|
{
|
|
388
388
|
id: 'single-use',
|
|
389
|
-
why: '
|
|
389
|
+
why: 'a base accessed once is as cheap to load again as to hold in a named local',
|
|
390
390
|
sound: false,
|
|
391
391
|
rejects: reachedOnce,
|
|
392
392
|
},
|
|
@@ -397,8 +397,27 @@ export const BASECSE_GATES: readonly Gate<BaseKey>[] = [
|
|
|
397
397
|
rejects: (c) => c.inLoop,
|
|
398
398
|
},
|
|
399
399
|
{
|
|
400
|
+
// A KNOWN COUNTEREXAMPLE, recorded here rather than fixed. `LIVEBASE_GATES` already names it —
|
|
401
|
+
// an MMIO wait or poll stores and re-reads ONE fixed offset through ONE register the whole
|
|
402
|
+
// time — so this gate rejects a base the target demonstrably held in a register, and the row
|
|
403
|
+
// it costs is carried by `/livebase`, which is this table minus the placement gates.
|
|
404
|
+
//
|
|
405
|
+
// NOT FIXED BY EXCLUDING A VALUELESS READ FROM THE CENSUS: that hides a real access to make
|
|
406
|
+
// the gate right for the wrong reason — the premise is falsified by the function's assembly,
|
|
407
|
+
// not by how the access is spelled. NOT FIXED BY EXEMPTING DEVICE-REGISTER BASES EITHER: that
|
|
408
|
+
// is the sound rule, and its blast radius is every MMIO row in the corpus against a gate this
|
|
409
|
+
// comment records was bought with a real match. It wants its own round and its own zero-flip
|
|
410
|
+
// gate.
|
|
411
|
+
//
|
|
412
|
+
// THE STRUCTURER'S DEAD-READ SPELLING (structure.ts `unreadResult`) INTERACTS, but not on the
|
|
413
|
+
// map-fed default: that spelling requires a qualifier to reach the access, a CAST spelling
|
|
414
|
+
// (`((s32 *)®_DMA3SAD)[2]`) carries none, and a map-fed DMA tree spells it exactly that
|
|
415
|
+
// way — so no statement is emitted there, offset 8 is touched once, and the base local
|
|
416
|
+
// survives. The inhabitant is the `/raw-globals` subtree, where the read IS spelled and this
|
|
417
|
+
// gate DOES demote; that subtree's own winner is a `/livebase` candidate, so nothing on the
|
|
418
|
+
// ranked path loses by it. Stated because the demotion is invisible from either file alone.
|
|
400
419
|
id: 'repeated-const-offset',
|
|
401
|
-
why: 'a fixed offset
|
|
420
|
+
why: 'a fixed offset read and then written is one scalar update, and the compiler loads its address again for it',
|
|
402
421
|
sound: false,
|
|
403
422
|
rejects: (c) => c.repeatedConstOffset,
|
|
404
423
|
},
|
|
@@ -427,14 +446,14 @@ export const BASECSE_GATES: readonly Gate<BaseKey>[] = [
|
|
|
427
446
|
export const BASEFOLD_GATES: readonly Gate<BaseKey>[] = [
|
|
428
447
|
{
|
|
429
448
|
id: 'single-use-unfolded',
|
|
430
|
-
why: '
|
|
449
|
+
why: 'a base accessed once is as cheap to load again as to hold in a named local, unless its offset survived the fold',
|
|
431
450
|
sound: false,
|
|
432
451
|
rejects: (c) => reachedOnce(c) && !c.unfoldedOffset,
|
|
433
452
|
},
|
|
434
453
|
...ablateHeuristic(BASECSE_GATES, 'single-use'),
|
|
435
454
|
];
|
|
436
455
|
|
|
437
|
-
/** The `/livebase`
|
|
456
|
+
/** The `/livebase` variation's admission (rank.ts): the default rules with both PLACEMENT heuristics
|
|
438
457
|
* ablated, keeping only `single-use`. `loop` and `repeated-const-offset` predict which spelling
|
|
439
458
|
* the compiler chose, and both predictions have a counterexample — an MMIO poll (`p[2] = go;
|
|
440
459
|
* while (p[2] & BUSY) {}`) stores and re-reads a fixed offset through ONE register the whole
|
|
@@ -447,7 +466,7 @@ export const LIVEBASE_GATES: readonly Gate<BaseKey>[] = ablateHeuristic(
|
|
|
447
466
|
|
|
448
467
|
/** `/livebase-block`'s admission (rank.ts): `/livebase` plus `single-cell`. The two tables differ
|
|
449
468
|
* by exactly one gate, so `without(LIVEBASE_BLOCK_GATES, 'single-cell')` is `/livebase`'s own
|
|
450
|
-
* admission and this selectivity
|
|
469
|
+
* admission and this selectivity rule prices by ablation like every other.
|
|
451
470
|
*
|
|
452
471
|
* `single-cell` GENERATES a narrower candidate; it does not classify, and taking it for a compiler
|
|
453
472
|
* fact is the way to misuse it. Its counterexample is in this corpus: `synthetic:sizebound`'s
|
|
@@ -470,13 +489,13 @@ export const LIVEBASE_GATES: readonly Gate<BaseKey>[] = ablateHeuristic(
|
|
|
470
489
|
* whenever an admission is added to or removed from the roster.
|
|
471
490
|
* HOW: prefer the edit-free form — import this array and `splice` the gate out of it before the
|
|
472
491
|
* first `enumerateCandidates` call, since the roster holds a reference to this very object. The
|
|
473
|
-
* env-read recipe on
|
|
492
|
+
* env-read recipe on BASEFOLD_HOISTS edits files instead, and a tap reverted underneath a
|
|
474
493
|
* running process reports ZEROES rather than crashing, which reads exactly like "the rule never
|
|
475
494
|
* fires"; if you use it, hash the tree either side of the window and quote both hashes.
|
|
476
495
|
*
|
|
477
|
-
* A CENSUS OVER
|
|
496
|
+
* A CENSUS OVER WINNERS' VARIATIONS CANNOT STAND IN FOR THAT — "only a row whose winner carries
|
|
478
497
|
* `/livebase-block` can move" is unsound for the reason rank.ts's `seen` dedup spells out. This
|
|
479
|
-
* table's own
|
|
498
|
+
* table's own census over winners' variations reads 5 rows and read 7 before `/unfolded` shipped, and the
|
|
480
499
|
* two that left differ: `synthetic:foldpark` by RENAME (byte-identical source, MATCH either
|
|
481
500
|
* side), `synthetic:unfoldpark` because its winning SPELLING changed, 402 bytes at diff:9 to 397
|
|
482
501
|
* at MATCH.
|
|
@@ -597,7 +616,8 @@ export const UNFOLDED_GATES: readonly Gate<BaseKey>[] = [
|
|
|
597
616
|
* reading the population: where `raise/globalshape.ts` shapes a name the structurer usually spells
|
|
598
617
|
* it bare and no key exists here at all — but a shape is ONE element type for the whole name, so an
|
|
599
618
|
* access that strides something else keeps its cast and its key. `kleod:SetupBG3WindowOverlay`'s
|
|
600
|
-
* `gBgInfo`
|
|
619
|
+
* `gBgInfo` (a row retired 2026-09-13) derived `elemSize 4` and still reached this table at stride
|
|
620
|
+
* 28, in both arms.
|
|
601
621
|
*
|
|
602
622
|
* What this table admits, censused over the artifact's 370 agbcc rows: map-less 8 rows / 10 keys,
|
|
603
623
|
* map-ful 10 rows / 12 keys. TWO shapes, and the arms differ:
|
|
@@ -624,8 +644,8 @@ export const UNFOLDED_GATES: readonly Gate<BaseKey>[] = [
|
|
|
624
644
|
* exemption cannot have.
|
|
625
645
|
*
|
|
626
646
|
* `loop` and `repeated-const-offset` STAY. Neither is about the base's identity and both are fan
|
|
627
|
-
* control; ablating them is `/livebase`'s
|
|
628
|
-
*
|
|
647
|
+
* control; ablating them is `/livebase`'s variation, already on the roster, and a row that wants the
|
|
648
|
+
* pairing is one hoist. THE PRICE OF THAT IS A HOLE, and it is named rather than left for a
|
|
629
649
|
* reader to find: a licensed base with a use inside a loop is admitted by NO table on the roster —
|
|
630
650
|
* this one refuses it on `loop`, and every table that ablates `loop` refuses it on `cast-base` or
|
|
631
651
|
* `single-use` — which is the "a base set that is no row's stays unreachable" debt
|
|
@@ -652,7 +672,7 @@ export const ORDERBASE_GATES: readonly Gate<BaseKey>[] = [
|
|
|
652
672
|
...ablateHeuristic(ablateHeuristic(BASECSE_GATES, 'cast-base'), 'single-use'),
|
|
653
673
|
{
|
|
654
674
|
id: 'order-licensed',
|
|
655
|
-
why: 'nothing in the assembly says this base
|
|
675
|
+
why: 'nothing in the assembly says this base was held in a local: the index was scaled first, or the order says nothing',
|
|
656
676
|
sound: false,
|
|
657
677
|
rejects: (c) => !c.orderLicensed,
|
|
658
678
|
},
|
|
@@ -718,13 +738,13 @@ function admit(sfn: SFn, gates: readonly Gate<BaseKey>[]): { c: Collected; keys:
|
|
|
718
738
|
* `scope` DECLINES, and the overload is how a caller is told: `null` means the placement had
|
|
719
739
|
* nothing to say about this function, because no init landed inside a nested list. That tree is
|
|
720
740
|
* byte-for-byte the `first-use` spelling (l3/hoist.ts's `nested`), and the roster withholds the
|
|
721
|
-
* `first-use` row for this table deliberately (rank.ts,
|
|
741
|
+
* `first-use` row for this table deliberately (rank.ts, ORDERBASE_HOISTS) — so returning it
|
|
722
742
|
* ships the withheld candidate under the scoped row's name.
|
|
723
743
|
*
|
|
724
744
|
* IT WITHDRAWS A SPELLING RATHER THAN COLLAPSING A DUPLICATE, which is what the decline costs.
|
|
725
|
-
* `
|
|
745
|
+
* `ORDERBASE_HOISTS` holds exactly two rows, `head` and `scope`, so nothing is ever enumerated
|
|
726
746
|
* at `first-use` for this table and the refused tree has no twin to fold into — its shape and
|
|
727
|
-
* `/volatile`
|
|
747
|
+
* `/volatile` compositions go with it. Over each project's whole `asm` tree, map-ful: of the 48
|
|
728
748
|
* functions `ORDERBASE_GATES` admits, 7 place an init inside a nested list and 41 do not, and for
|
|
729
749
|
* 29 of the 41 the refused spelling is one the `head` row does not already produce. Instrumented
|
|
730
750
|
* on two of those, both map-ful — the `kleod:StreamCmd_SetBGScroll` row (fan 11), and
|
|
@@ -785,13 +805,13 @@ export function hoistBaseLocals(
|
|
|
785
805
|
}
|
|
786
806
|
const out = { ...sfn, body, locals };
|
|
787
807
|
// The two FLAT placements can only put the run in the top-level list, above every use of it by
|
|
788
|
-
// construction. `scope` puts an init inside a nested list, which is where a placing
|
|
808
|
+
// construction. `scope` puts an init inside a nested list, which is where a placing variation can ship
|
|
789
809
|
// the one failure the byte differ rewards — a read of a local whose assignment does not reach it —
|
|
790
810
|
// so the tree it emits is checked rather than argued (contracts.ts).
|
|
791
811
|
//
|
|
792
812
|
// THE POPULATION IS THE MOTION, and `moved` is what the placer says it moved rather than what this
|
|
793
813
|
// function minted. The leading run this pass inherits is the DEFAULT hoist's, committed by
|
|
794
|
-
// `structureChecked` before rank's
|
|
814
|
+
// `structureChecked` before rank's variations see the tree (pipeline.ts), and `scope` moves those
|
|
795
815
|
// inits too — so `newLocals` names less than half of what has to be judged. Real inhabitants, in the
|
|
796
816
|
// CHECKOUTS rather than in a benchmark row — `DecompressAndLoadLevel` in klonoa and `sub_8052474`
|
|
797
817
|
// in sa3, both map-ful — each sink one inherited `p0` beside the minted `p1`.
|
package/src/l3/coalesce.ts
CHANGED
|
@@ -217,7 +217,7 @@ export interface MergePair {
|
|
|
217
217
|
* `const-fed` is what keeps three of them (ablate it and the span path offers 273). A rule
|
|
218
218
|
* refusing every in-loop local would make that bound redundant; this one does not, so any further
|
|
219
219
|
* relaxation of `const-fed` is a multiplier, and two call sites pay it (`/coalesce` and
|
|
220
|
-
* `/scopebase
|
|
220
|
+
* `/scopebase/coalesce`; the `/livebase` pairings enumerate the ARM path and pay
|
|
221
221
|
* ARM_DISJOINT_GATES' `arm-init` instead). */
|
|
222
222
|
export const COALESCE_GATES: readonly Gate<MergePair>[] = [
|
|
223
223
|
{
|
|
@@ -234,7 +234,7 @@ export const COALESCE_GATES: readonly Gate<MergePair>[] = [
|
|
|
234
234
|
},
|
|
235
235
|
{
|
|
236
236
|
id: 'volatile',
|
|
237
|
-
why: 'a volatile qualifier
|
|
237
|
+
why: 'a `volatile` qualifier, on the variable or on what it points to, is observable, and merging would drop or add it',
|
|
238
238
|
sound: true,
|
|
239
239
|
guardedBy: 'coalesce.test.ts: a volatile pair never merges',
|
|
240
240
|
rejects: (c) => c.eitherIsVolatile,
|
|
@@ -252,7 +252,7 @@ export const COALESCE_GATES: readonly Gate<MergePair>[] = [
|
|
|
252
252
|
},
|
|
253
253
|
{
|
|
254
254
|
id: 'const-fed',
|
|
255
|
-
why: 'a
|
|
255
|
+
why: 'a local set from a memory load, other than a `for` loop counter, is one the compiler had a reason to keep where it was',
|
|
256
256
|
sound: false,
|
|
257
257
|
rejects: (c) => !c.x.constFed || !c.y.constFed,
|
|
258
258
|
},
|
|
@@ -265,20 +265,20 @@ export const COALESCE_GATES: readonly Gate<MergePair>[] = [
|
|
|
265
265
|
},
|
|
266
266
|
{
|
|
267
267
|
id: 'first-is-write',
|
|
268
|
-
why: 'a survivor first
|
|
268
|
+
why: 'a survivor whose first mention is a read would see the absorbed value there',
|
|
269
269
|
sound: false,
|
|
270
270
|
rejects: (c) => !c.y.firstIsWrite,
|
|
271
271
|
},
|
|
272
272
|
];
|
|
273
273
|
|
|
274
|
-
/** Every legal single merge, each as its own tree — NOT one committed
|
|
274
|
+
/** Every legal single merge, each as its own tree — NOT one committed decision.
|
|
275
275
|
*
|
|
276
276
|
* Which pair a register allocator coalesced is not derivable from the L3 tree, and first-fit gets
|
|
277
277
|
* it wrong. Run kleod:UpdateHUDCounterDisplay's published repro script (results.json carries it)
|
|
278
278
|
* and read the candidate table: of its two legal merges, one scores WORSE than not merging at all
|
|
279
279
|
* and declaration order is the one that picks it. Emitting no merges at all costs that row its
|
|
280
280
|
* match, which is what guards this file. `rank.ts` already has the idiom for exactly this —
|
|
281
|
-
* `/regcopy`'s "the tail
|
|
281
|
+
* `/regcopy`'s "the tail decision is allocator-ambiguous, so both are ranked" — so every candidate is
|
|
282
282
|
* emitted and the differ referees.
|
|
283
283
|
*
|
|
284
284
|
* ACCEPTED, NOT FIXED: a survivor assigned only on SOME paths still absorbs the other's value on
|
|
@@ -356,7 +356,7 @@ export const ARM_DISJOINT_GATES: readonly Gate<ArmPair>[] = [
|
|
|
356
356
|
},
|
|
357
357
|
{
|
|
358
358
|
id: 'volatile',
|
|
359
|
-
why: 'a volatile qualifier
|
|
359
|
+
why: 'a `volatile` qualifier, on the variable or on what it points to, is observable, and merging would drop or add it',
|
|
360
360
|
sound: true,
|
|
361
361
|
guardedBy: 'coalesce.test.ts: a volatile pair never merges',
|
|
362
362
|
rejects: (c) => c.eitherIsVolatile,
|
|
@@ -370,7 +370,7 @@ export const ARM_DISJOINT_GATES: readonly Gate<ArmPair>[] = [
|
|
|
370
370
|
},
|
|
371
371
|
{
|
|
372
372
|
id: 'arm-init',
|
|
373
|
-
why: 'a local not
|
|
373
|
+
why: 'a local its arm does not first set to a constant is one the compiler had a reason to keep apart',
|
|
374
374
|
sound: false,
|
|
375
375
|
rejects: (c) => !c.bothArmConstInit,
|
|
376
376
|
},
|
|
@@ -378,7 +378,7 @@ export const ARM_DISJOINT_GATES: readonly Gate<ArmPair>[] = [
|
|
|
378
378
|
|
|
379
379
|
/** The arm-disjoint merges alone — the class the livebase pairings enumerate (rank.ts): the
|
|
380
380
|
* demanding row's shared counter is arm-disjoint, and the span-model merges already ride the
|
|
381
|
-
* plain /coalesce
|
|
381
|
+
* plain /coalesce variation, so pairing them too would multiply candidates with no row behind it. */
|
|
382
382
|
export function armDisjointCandidates(sfn: SFn): { merged: string; sfn: SFn }[] {
|
|
383
383
|
return armDisjointUnder(ARM_DISJOINT_GATES, sfn).candidates;
|
|
384
384
|
}
|
package/src/l3/gates.ts
CHANGED
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
//
|
|
8
8
|
// `why` is a LABEL, one line. The argument for why the rule is correct belongs in the file header,
|
|
9
9
|
// which has room; duplicating it here is how a table stops paying for itself.
|
|
10
|
+
//
|
|
11
|
+
// A table a variation's definition names (`variation-gates.ts`) shows its `why` to a reader, in the
|
|
12
|
+
// webapp's variation drawer, so there the label is plain prose: no tag, file or function name, or
|
|
13
|
+
// shouted word. `variation-offers.test.ts` holds it to that.
|
|
10
14
|
export interface Gate<Ctx> {
|
|
11
15
|
/** stable, kebab-case; appears in test names and in the contract report */
|
|
12
16
|
readonly id: string;
|
|
13
|
-
/** one line: the reason the rule exists */
|
|
17
|
+
/** one line: the reason the rule exists, readable without the code */
|
|
14
18
|
readonly why: string;
|
|
15
19
|
/** Remove it and some candidate is WRONG, not merely worse. Everything else is a codegen
|
|
16
20
|
* heuristic the differ still referees. This flag is what makes `guardedBy` mandatory. */
|
|
@@ -86,3 +90,73 @@ export function gateTableDefects<Ctx>(gates: readonly Gate<Ctx>[]): string[] {
|
|
|
86
90
|
}
|
|
87
91
|
return out;
|
|
88
92
|
}
|
|
93
|
+
|
|
94
|
+
/** A gate table that counts its own refusals — {@link tallying}'s return. */
|
|
95
|
+
export interface Tallied<Ctx> {
|
|
96
|
+
/** Hand this to the pass, in place of the table it wraps. */
|
|
97
|
+
readonly gates: readonly Gate<Ctx>[];
|
|
98
|
+
/** The census so far, most-refused first, ties in table order. A snapshot: counts keep
|
|
99
|
+
* accumulating across every later call, which is what a corpus-wide census wants.
|
|
100
|
+
*
|
|
101
|
+
* THERE IS NO RESET, deliberately — a per-row census is two snapshots DIFFED, not a fresh
|
|
102
|
+
* wrapper per row, because a wrapper is a new table IDENTITY and a reader that keys on one
|
|
103
|
+
* (`rank.ts`'s `censuses` memo) sees a fresh key every row. Counting is keyed by `g.id`, so a
|
|
104
|
+
* table COMPOSED from several should be run past `gateTableDefects` first: two rules sharing an
|
|
105
|
+
* id sum into one number, and the contract test only checks the tables on its own roster. */
|
|
106
|
+
readonly refusals: () => readonly (readonly [string, number])[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The same table, wrapping each `rejects` in a counter — so a caller OUTSIDE core can obtain the
|
|
110
|
+
* per-id census that `l3/coalesce.ts` and `structure/namecoalesce.ts` hand-roll into their return
|
|
111
|
+
* type (`l3/scopebase.ts` reports the same attribution per KEY), from any pass that takes its
|
|
112
|
+
* table as a parameter:
|
|
113
|
+
*
|
|
114
|
+
* const t = tallying(UNMERGE_SITE_GATES);
|
|
115
|
+
* unmergeJoins(sfn, { site: t.gates });
|
|
116
|
+
* console.log(t.refusals()); // [['empty-arm', 168], ['no-merge-name', 80]]
|
|
117
|
+
*
|
|
118
|
+
* THAT IS THE API AND NOT YET A CENSUS: nothing exports a corpus of trees to loop over, and a
|
|
119
|
+
* tabled pass's only shipped caller is normally inside core. Taking the census off a REAL
|
|
120
|
+
* enumeration is `pnpm bench gates --pass <id>`, whose header
|
|
121
|
+
* (`apps/benchmark/src/run/gate-census.ts`) holds the measured reasons it is a subcommand rather
|
|
122
|
+
* than a script to copy, and what a SECOND censusable pass costs.
|
|
123
|
+
*
|
|
124
|
+
* WHAT IT COUNTS IS AN EVALUATION THAT ANSWERED TRUE, not a site. Under `firstRejection` — which
|
|
125
|
+
* short-circuits — that is the FIRST rejecter, so this produces the same census the hand-rolled
|
|
126
|
+
* maps do, with the same reading: an id absent from it is starved OR REDUNDANT WITH an earlier
|
|
127
|
+
* rule, and telling the two apart takes the same rule run with the rest of the table empty
|
|
128
|
+
* (`grep -n "ON ITS OWN" packages/core/src/raise/globalshape.ts`, whose dated table ships three
|
|
129
|
+
* rules of the second kind). A consumer that asks the table something else — `.some`, `.filter` —
|
|
130
|
+
* gets one count per evaluation instead, which is a different question and rarely the one wanted.
|
|
131
|
+
*
|
|
132
|
+
* AND IT COUNTS REFUSALS, WHICH IS NOT REACH. A rule can refuse hundreds of times and still change
|
|
133
|
+
* no output, because a later rule or a narrowing outside the table would have refused the same
|
|
134
|
+
* sites: that is the MOVED column, it costs an ablation rather than a census, and `l3/unmerge.ts`'s
|
|
135
|
+
* header carries the worked example of the two disagreeing.
|
|
136
|
+
*
|
|
137
|
+
* IT CHANGES NO BEHAVIOUR: each wrapper's predicate IS the original's, `id`/`why`/`sound`/
|
|
138
|
+
* `guardedBy` are carried, so `without`, `ablateHeuristic` and `gateTableDefects` all still hold
|
|
139
|
+
* over the result. What it does change is the table's IDENTITY, and some readers key on that —
|
|
140
|
+
* `rank.ts`'s `censuses` memo is a `Map` over `Gate<BaseKey>[]` instances — so wrap once and reuse
|
|
141
|
+
* `gates`, rather than per call. */
|
|
142
|
+
export function tallying<Ctx>(gates: readonly Gate<Ctx>[]): Tallied<Ctx> {
|
|
143
|
+
const counts = new Map<string, number>();
|
|
144
|
+
const order = new Map(gates.map((g, i) => [g.id, i]));
|
|
145
|
+
return {
|
|
146
|
+
gates: gates.map((g) => ({
|
|
147
|
+
...g,
|
|
148
|
+
rejects: (c: Ctx) => {
|
|
149
|
+
const r = g.rejects(c);
|
|
150
|
+
if (r) {
|
|
151
|
+
counts.set(g.id, (counts.get(g.id) ?? 0) + 1);
|
|
152
|
+
}
|
|
153
|
+
return r;
|
|
154
|
+
},
|
|
155
|
+
})),
|
|
156
|
+
refusals: () =>
|
|
157
|
+
[...counts].sort((a, b) => b[1] - a[1] || (order.get(a[0]) ?? 0) - (order.get(b[0]) ?? 0)) as readonly (readonly [
|
|
158
|
+
string,
|
|
159
|
+
number,
|
|
160
|
+
])[],
|
|
161
|
+
};
|
|
162
|
+
}
|
package/src/l3/hoist.ts
CHANGED
|
@@ -208,7 +208,7 @@ function scopeSite(list: Stmt[], name: string): { list: Stmt[]; at: number } | n
|
|
|
208
208
|
* different bytes. Where no nested list holds every mention this IS `first-use`, which is what
|
|
209
209
|
* makes it a placement rather than a second policy.
|
|
210
210
|
*
|
|
211
|
-
* These three are the
|
|
211
|
+
* These three are the positions a hoist may state (rank.ts) and the only values
|
|
212
212
|
* `hoistBaseLocals` accepts. */
|
|
213
213
|
export type HoistPlacement = 'head' | 'first-use' | 'scope';
|
|
214
214
|
|
package/src/l3/homesplit.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The `/livebase-block × /regionbase` PAIRING (rank.ts `/livebase-block/homesplit`): one base kept
|
|
2
2
|
// at the function head and a SECOND base split into one local per region, in the same function.
|
|
3
3
|
//
|
|
4
|
-
// WHY IT IS NOT REACHABLE FROM EITHER
|
|
4
|
+
// WHY IT IS NOT REACHABLE FROM EITHER VARIATION. Both are whole-FUNCTION policies over the bases they
|
|
5
5
|
// bind: `hoistBaseLocals` homes every key its table admits at one placement, and
|
|
6
6
|
// `hoistScopedBases` splits every key its region rule admits. A function whose two bases want
|
|
7
7
|
// OPPOSITE answers is spelled by neither. `synthetic:dmapoll` is that function and its endpoint is
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
// over ONE input is the shape that would collide, and nothing here does it.
|
|
20
20
|
//
|
|
21
21
|
// WHICH KEY IS WITHHELD IS NOT DERIVABLE, so every admitted key is offered as its own candidate,
|
|
22
|
-
//
|
|
23
|
-
// take toward each other. The
|
|
24
|
-
// IDENTITY: `bench diff` and docs/ranked-repro.md compare candidates by
|
|
22
|
+
// NAMED BY THAT KEY, and the differ referees — the same posture `/scopebase` and `/regionbase`
|
|
23
|
+
// take toward each other. The variation carries `homeSplitTag(key)` because a candidate's variations are its
|
|
24
|
+
// IDENTITY: `bench diff` and docs/ranked-repro.md compare candidates by them, so one name over two
|
|
25
25
|
// withholds would hide a program swap from both. The withhold itself is DATA: one rejection
|
|
26
26
|
// prepended to the caller's own admission table, in the `Gate<BaseKey>` type that table already
|
|
27
27
|
// has, so `firstRejection` attributes a refusal to it, `without` ablates it, and the composed table
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// EXACTLY ONE KEY IS WITHHELD, and the arity is a claim rather than an oversight. A two-key withhold
|
|
31
31
|
// exists only where the caller's table binds three — `homesplit-fan-cap` admits no more — and there
|
|
32
32
|
// it is the three further pairs, each carrying the three respells rank.ts derives from one pipe:
|
|
33
|
-
// NINE more candidates per
|
|
33
|
+
// NINE more candidates per structure setting on those functions alone, before any stacked variation, and no
|
|
34
34
|
// row asks for one. `l3/volatileptr.ts`'s `volatileSubsetCandidates` enumerates every proper subset
|
|
35
35
|
// under the same cap; it does that because a row demanded each of them. Widen this the same way,
|
|
36
36
|
// on a row.
|
|
@@ -59,7 +59,7 @@ export const withholdingKey = (gates: readonly Gate<BaseKey>[], key: string): re
|
|
|
59
59
|
...gates,
|
|
60
60
|
];
|
|
61
61
|
|
|
62
|
-
/** The withheld key as a
|
|
62
|
+
/** The withheld key as a variation's SUBJECT: `c:67109076 4 true` → `0x40000d4.4s`. Width and signedness
|
|
63
63
|
* ride because they are part of the key — two keys over one address are two different spellings.
|
|
64
64
|
*
|
|
65
65
|
* PARSED BY THE KEY'S PRODUCER (`l3/basecse.ts`'s `parseBaseKey`, beside `keyOf`), because a base
|
|
@@ -68,9 +68,9 @@ export const withholdingKey = (gates: readonly Gate<BaseKey>[], key: string): re
|
|
|
68
68
|
* separator rather than the type's. Split from the FRONT, the type reads as the width and the
|
|
69
69
|
* width as the signedness; split from the END, the `a:` form comes out right and the `c:` form
|
|
70
70
|
* runs `Number` over `67109076 <u16*>`, tagging every cast over a numeric base `0xNaN` and
|
|
71
|
-
* collapsing distinct keys onto one
|
|
71
|
+
* collapsing distinct keys onto one name. No shipped table admits a cast base outside
|
|
72
72
|
* `/orderbase`, and `/orderbase` carries `pairings: false`, so no such key reaches this function
|
|
73
|
-
* today — but a candidate
|
|
73
|
+
* today — but a candidate's variations are its identity (`bench diff` and docs/ranked-repro.md compare
|
|
74
74
|
* candidates by it), and one roster line is all that stands between the two. The sibling half of
|
|
75
75
|
* the same hazard is already guarded in `splitHomeBases`, which translates a cast base to no
|
|
76
76
|
* region key and declines. */
|
|
@@ -79,7 +79,7 @@ export function homeSplitTag(key: string): string {
|
|
|
79
79
|
const base = leaf.startsWith('c:') ? `0x${Number(leaf.slice(2)).toString(16)}` : leaf.slice(leaf.indexOf(':') + 1);
|
|
80
80
|
// The cast's element type stays in the token — it is part of the key's identity, since two casts
|
|
81
81
|
// over one symbol are two locals that stride differently. Whitespace is squeezed defensively
|
|
82
|
-
// rather than because any type spells one: a
|
|
82
|
+
// rather than because any type spells one: a variation is one whitespace-free word everywhere it is
|
|
83
83
|
// read, and that has to hold whatever `typeToString` grows.
|
|
84
84
|
const type = castType === null ? '' : `<${castType}>`;
|
|
85
85
|
return `${`${base}${type}`.replace(/\s+/g, '')}.${width}${signed ? 's' : 'u'}`;
|
|
@@ -98,13 +98,13 @@ export interface HomeSplitFanCtx {
|
|
|
98
98
|
export const HOMESPLIT_FAN_GATES: readonly Gate<HomeSplitFanCtx>[] = [
|
|
99
99
|
{
|
|
100
100
|
id: 'homesplit-degenerate',
|
|
101
|
-
why: '
|
|
101
|
+
why: 'with one hoistable base there is nothing new: withholding it is `regionbase`, and withholding none is `livebase-block`',
|
|
102
102
|
sound: false,
|
|
103
103
|
rejects: (c) => c.hoistableKeys < 2,
|
|
104
104
|
},
|
|
105
105
|
{
|
|
106
106
|
id: 'homesplit-fan-cap',
|
|
107
|
-
why: '
|
|
107
|
+
why: 'more than three hoistable bases would add a candidate for each, times the volatile pairings',
|
|
108
108
|
sound: false,
|
|
109
109
|
rejects: (c) => c.hoistableKeys > 3,
|
|
110
110
|
},
|
|
@@ -143,13 +143,13 @@ export interface HomeSplitCtx {
|
|
|
143
143
|
export const HOMESPLIT_GATES: readonly Gate<HomeSplitCtx>[] = [
|
|
144
144
|
{
|
|
145
145
|
id: 'homesplit-no-region',
|
|
146
|
-
why: 'a
|
|
146
|
+
why: 'a base held back from the function-top local that no per-region local takes instead leaves the spelling the default carries',
|
|
147
147
|
sound: false,
|
|
148
148
|
rejects: (c) => !c.withheldSplits,
|
|
149
149
|
},
|
|
150
150
|
{
|
|
151
151
|
id: 'homesplit-drops-device-volatile',
|
|
152
|
-
why: 'a device read left inline
|
|
152
|
+
why: 'a device read left inline would carry neither `volatile` nor `vol-store`',
|
|
153
153
|
sound: false,
|
|
154
154
|
rejects: (c) => c.inlineDeviceRead,
|
|
155
155
|
},
|
package/src/l3/initfirst.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: a loop INIT moves above the guard that encloses it, and the guard reads
|
|
2
2
|
// the initialized variable.
|
|
3
3
|
//
|
|
4
4
|
// `for (i = 0; i < n; i++)` compiles with the init BEFORE the zero-trip test (`mov r3,#0` then
|
|
5
5
|
// `cmp r3, r5`), while `if (0 < n) { i = 0; do … }` compiles with the init behind the branch.
|
|
6
6
|
// Both source forms lift to the SAME IR — a const has no position — so which the original spelled
|
|
7
|
-
// is not recoverable; this
|
|
7
|
+
// is not recoverable; this variation emits the init-first sibling and the differ referees:
|
|
8
8
|
//
|
|
9
9
|
// if (0 < n) { v = 0; … } → v = 0; if (v < n) { … }
|
|
10
10
|
//
|
|
@@ -61,7 +61,7 @@ const stripWideIntCast = (e: Expr): Expr =>
|
|
|
61
61
|
|
|
62
62
|
/** Every deref rooted at a var through casts only. The var-root rule is a TWO-WORLD argument, not a
|
|
63
63
|
* volatility proof: a deref through a plain-declared pointer local may still be MMIO, but the
|
|
64
|
-
* /volatile
|
|
64
|
+
* /volatile variation enumerates the qualified sibling — where this variation refuses — so both worlds reach
|
|
65
65
|
* the differ and collapsing reads here is the plain world's own premise. A raw `*(u16 *)CONST` deref
|
|
66
66
|
* has NO local for /volatile to qualify, so no sibling carries the volatile world and the collapse
|
|
67
67
|
* would silently discard it. */
|
package/src/l3/inlinebase.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: DELETE a pointer local holding a CONSTANT address and spell each access
|
|
2
2
|
// through it as the cast constant (`*(u16 *)0x4000208` rather than `p = (u16 *)0x4000208; *p`).
|
|
3
3
|
//
|
|
4
4
|
// The local is structure/analysis.ts's value-home spelling for a `const` with 2+ consumers that
|
|
@@ -25,34 +25,34 @@
|
|
|
25
25
|
// separate at 11 against 12 on their own, and are BYTE-IDENTICAL once the slot qualifier is
|
|
26
26
|
// there too — the shape that matches (agbcc 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit
|
|
27
27
|
// -fhex-asm -fprologue-bugfix`; `.s` diff empty, `.o` identical under cmp). So rank.ts emits
|
|
28
|
-
// the qualified spelling as a second OUTPUT of this
|
|
28
|
+
// the qualified spelling as a second OUTPUT of this variation, `/volatile` narrowed to the locals
|
|
29
29
|
// it deletes. Each cast this mints carries the qualifier; a use whose width does not stride the
|
|
30
30
|
// declared pointee renders through the C-family printer's reinterpret cast instead, which
|
|
31
31
|
// carries it too (backend/cfamily.ts) — in C the access takes the OUTER type, so a plain cast
|
|
32
32
|
// there would spell exactly the silent drop this paragraph exists to prevent.
|
|
33
33
|
//
|
|
34
34
|
// GATE (INLINEBASE_GATES) — the local must be all of: pointer-typed; initialized by a bare
|
|
35
|
-
// NONZERO `const` (a `(T *)base` CAST initializer is l3/basecse.ts's reuse hoist, whose own
|
|
35
|
+
// NONZERO `const` (a `(T *)base` CAST initializer is l3/basecse.ts's reuse hoist, whose own variation
|
|
36
36
|
// family owns that question; `0` is NULL, never an address, which is also the sibling qualifier
|
|
37
|
-
//
|
|
37
|
+
// variation's rule); assigned exactly once, by a statement at the body's TOP LEVEL that no earlier
|
|
38
38
|
// statement's mention precedes; never address-taken; used only as the base of an `index`, at 2+
|
|
39
39
|
// sites (one use is not the reused address this exists for); not object-`volatile` (a
|
|
40
40
|
// `T *volatile p` has no inhabitant, and cfamily.ts prints that flag in the pointee's position);
|
|
41
41
|
// and not `frame` (a slot is an asm fact — see the SFn.locals doc). Anything else, and nothing
|
|
42
42
|
// qualifying at all, DECLINES (null) rather than approximating.
|
|
43
43
|
//
|
|
44
|
-
// A
|
|
45
|
-
// underdetermination one (docs/level-tower.md, "a third fork sits inside the ranked
|
|
44
|
+
// A RESPELL VARIATION RATHER THAN A STRUCTURE VARIATION, which is a cost decision and not the
|
|
45
|
+
// underdetermination one (docs/level-tower.md, "a third fork sits inside the ranked variations").
|
|
46
46
|
// The question — does a `const` with 2+ consumers live across a call in a named local — is the one
|
|
47
47
|
// `/reread-globals`, `/addr-home`, `/expr-home` and `/derived-home` each answer as a
|
|
48
|
-
//
|
|
48
|
+
// STRUCTURE_VARIATIONS entry. A structure variation here would double the enumeration on every function it admits;
|
|
49
49
|
// substituting on the already-homed tree costs 766 candidates over 47058 (+1.6%) across the 33 of
|
|
50
50
|
// 69 klonoa functions that lift with no symbol map.
|
|
51
51
|
//
|
|
52
52
|
// KNOWN GAP, and it is the price of that choice rather than an oversight: only `index` bases are
|
|
53
53
|
// re-spelled, so the same L2 home passed to a callee or standing as a `field` base is out of
|
|
54
54
|
// reach — `otherUses` refuses it. Reaching those needs the un-homed tree, which is
|
|
55
|
-
// structure/analysis.ts's decision; the day a row demands one, this becomes
|
|
55
|
+
// structure/analysis.ts's decision; the day a row demands one, this becomes a structure variation.
|
|
56
56
|
//
|
|
57
57
|
// The idiom it recovers is every GBA project's register macro: `*(vu16 *)0x4000208` is what
|
|
58
58
|
// `REG_IME` expands to, so the deleted local is not merely an undone home.
|
|
@@ -71,20 +71,20 @@ interface BaseCtx {
|
|
|
71
71
|
export const INLINEBASE_GATES: readonly Gate<BaseCtx>[] = [
|
|
72
72
|
{
|
|
73
73
|
id: 'non-pointer',
|
|
74
|
-
why: '
|
|
74
|
+
why: 'only a local holding an address is substituted; a local holding a scalar value is a different question',
|
|
75
75
|
sound: false,
|
|
76
76
|
rejects: (c) => !c.isPointer,
|
|
77
77
|
},
|
|
78
78
|
{
|
|
79
79
|
id: 'object-volatile',
|
|
80
|
-
why: 'the substitution
|
|
80
|
+
why: 'the substitution keeps only the `volatile` of what the pointer points to, so a pointer that is itself volatile would lose its own',
|
|
81
81
|
sound: true,
|
|
82
82
|
guardedBy: 'inlinebase.test.ts: an object-volatile or frame local declines',
|
|
83
83
|
rejects: (c) => c.objectVolatile,
|
|
84
84
|
},
|
|
85
85
|
{
|
|
86
86
|
id: 'frame',
|
|
87
|
-
why: 'a slot the
|
|
87
|
+
why: 'a stack slot the assembly used is a fact about the function, not a spelling to undo',
|
|
88
88
|
sound: false,
|
|
89
89
|
rejects: (c) => c.hasFrame,
|
|
90
90
|
},
|
|
@@ -104,7 +104,7 @@ export const INLINEBASE_GATES: readonly Gate<BaseCtx>[] = [
|
|
|
104
104
|
},
|
|
105
105
|
{
|
|
106
106
|
id: 'null-base',
|
|
107
|
-
why: '`0` is
|
|
107
|
+
why: 'a base of `0` is a null pointer, never an address, and `volatile` refuses it too',
|
|
108
108
|
sound: false,
|
|
109
109
|
rejects: (c) => c.m.constValue === 0,
|
|
110
110
|
},
|
|
@@ -126,12 +126,12 @@ export const INLINEBASE_GATES: readonly Gate<BaseCtx>[] = [
|
|
|
126
126
|
id: 'other-uses',
|
|
127
127
|
why: 'a use the substitution cannot reach would name the deleted local',
|
|
128
128
|
sound: true,
|
|
129
|
-
guardedBy: 'inlinebase.test.ts: a use that is not an `index` base is outside what the
|
|
129
|
+
guardedBy: 'inlinebase.test.ts: a use that is not an `index` base is outside what the variation re-spells',
|
|
130
130
|
rejects: (c) => c.m.otherUses !== 0,
|
|
131
131
|
},
|
|
132
132
|
{
|
|
133
133
|
id: 'single-use',
|
|
134
|
-
why: 'one use is not the reused address this
|
|
134
|
+
why: 'one use is not the reused address this variation exists for',
|
|
135
135
|
sound: false,
|
|
136
136
|
rejects: (c) => c.m.baseUses < 2,
|
|
137
137
|
},
|
|
@@ -164,8 +164,8 @@ function plan(sfn: SFn): Map<string, Extract<Expr, { k: 'cast' }>> {
|
|
|
164
164
|
return out;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
/** Which locals this
|
|
168
|
-
* pairing, so the qualified output never qualifies a pointer the
|
|
167
|
+
/** Which locals this variation would delete — rank.ts narrows `/volatile` to exactly these before
|
|
168
|
+
* pairing, so the qualified output never qualifies a pointer the variation leaves standing. */
|
|
169
169
|
export function inlinableConstBases(sfn: SFn): string[] {
|
|
170
170
|
return [...plan(sfn).keys()];
|
|
171
171
|
}
|